Skip to content

Commit

Permalink
feat: implement initial admin dashboard (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
BastiDood committed Aug 9, 2024
2 parents 5eb5aeb + 4ca9e32 commit c0a38dc
Show file tree
Hide file tree
Showing 8 changed files with 361 additions and 107 deletions.
2 changes: 1 addition & 1 deletion app/src/lib/users/Student.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
$: ({ email, given_name, family_name, avatar, student_number, labs, lab_id } = user);
</script>

<a href="mailto:{email}" class="grid w-full grid-cols-[auto_1fr] items-center gap-1 p-4">
<a href="mailto:{email}" class="grid w-full grid-cols-[auto_1fr] items-center gap-2 p-4">
<span><Avatar src={avatar} width="w-20" /></span>
<span class="flex flex-col">
{#if given_name.length > 0 && family_name.length > 0}
Expand Down
27 changes: 27 additions & 0 deletions app/src/routes/dashboard/(admin)/drafts/+layout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script>
import { format } from 'date-fns';
// eslint-disable-next-line init-declarations
export let data;
$: ({ draft } = data);
</script>

{#if draft !== null}
{@const { draft_id, curr_round, max_rounds, active_period_start } = draft}
{@const startDate = format(active_period_start, 'PPP')}
{@const startTime = format(active_period_start, 'pp')}
<div class="card prose dark:prose-invert max-w-none p-4">
<p>
{#if curr_round === null}
<strong>Draft &num;{draft_id}</strong> (which opened last <strong>{startDate}</strong> at
<strong>{startTime}</strong>) has recently finished the main drafting process. It is currently in the
lottery rounds.
{:else}
<strong>Draft &num;{draft_id}</strong> is currently on Round <strong>{curr_round}</strong>
of <strong>{max_rounds}</strong>. It opened last <strong>{startDate}</strong> at
<strong>{startTime}</strong>.
{/if}
</p>
</div>
{/if}
<slot />
10 changes: 7 additions & 3 deletions app/src/routes/dashboard/(admin)/drafts/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ export async function load({ locals: { db }, parent }) {
if (!user.is_admin || user.user_id === null || user.lab_id !== null) error(403);

const labs = await db.getLabRegistry();
if (draft === null) return { draft: null, labs };
if (draft === null) return { draft: null, labs, available: [], selected: [], records: [] };

const [students, records] = await Promise.all([
db.getStudentsInDraftTaggedByLab(draft.draft_id),
db.getFacultyChoiceRecords(draft.draft_id),
]);

const students = await db.getStudentsInDraftTaggedByLab(draft.draft_id);
const { available, selected } = groupBy(students, ({ lab_id }) => (lab_id === null ? 'available' : 'selected'));
return { draft, labs, available: available ?? [], selected: selected ?? [] };
return { draft, labs, available: available ?? [], selected: selected ?? [], records };
}

function* mapRowTuples(data: FormData) {
Expand Down
182 changes: 79 additions & 103 deletions app/src/routes/dashboard/(admin)/drafts/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
<script>
import Dashboard from './Dashboard.svelte';
import Student from '$lib/users/Student.svelte';
import { format } from 'date-fns';
import ErrorAlert from '$lib/alerts/Error.svelte';
import WarningAlert from '$lib/alerts/Warning.svelte';
import ConcludeForm from './ConcludeForm.svelte';
Expand All @@ -12,10 +10,10 @@
// eslint-disable-next-line init-declarations
export let data;
$: ({ labs } = data);
$: ({ draft, labs, records, available, selected } = data);
</script>

{#if data.draft === null}
{#if draft === null}
{#if labs.some(({ quota }) => quota > 0)}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-[auto_1fr]">
<div class="prose dark:prose-invert">
Expand All @@ -32,91 +30,70 @@
<InitForm />
</div>
{:else}
<ErrorAlert
<WarningAlert
>The total quota of all labs is currently zero. Please <a href="/dashboard/labs/" class="anchor">allocate</a
> at least one slot to a lab to proceed.</ErrorAlert
> at least one slot to a lab to proceed.</WarningAlert
>
{/if}
{:else}
{@const {
draft: { draft_id, curr_round, max_rounds, active_period_start },
available,
selected,
} = data}
{@const startDate = format(active_period_start, 'PPP')}
{@const startTime = format(active_period_start, 'pp')}
<div class="card prose dark:prose-invert max-w-none p-4">
<p>
{#if curr_round === null}
<strong>Draft &num;{draft_id}</strong> (which opened last <strong>{startDate}</strong> at
<strong>{startTime}</strong>) has recently finished the main drafting process. It is currently in the
lottery rounds.
{:else}
<strong>Draft &num;{draft_id}</strong> is currently on Round <strong>{curr_round}</strong>
of <strong>{max_rounds}</strong>. It opened last <strong>{startDate}</strong> at
<strong>{startTime}</strong>.
{/if}
</p>
</div>
{#if curr_round === null}
<div class="grid grid-cols-1 gap-4 md:grid-cols-[auto_1fr]">
<div class="prose dark:prose-invert">
<h3>Lottery</h3>
<p>
Draft &num;{draft_id} is almost done! The final stage is the lottery phase, where the remaining undrafted
students are randomly assigned to their labs. Before the system automatically randomizes anything, administrators
are given a final chance to manually intervene with the draft results.
</p>
<ul>
<li>
The <strong>"Eligible for Lottery"</strong> section features a list of the remaining undrafted students.
Administrators may negotiate with the lab heads on how to manually assign and distribute these students
fairly among interested labs.
</li>
<li>
Meanwhile, the <strong>"Already Drafted"</strong> section features an <em>immutable</em> list of
students who have already been drafted into their respective labs. These are considered final.
</li>
{:else if draft.curr_round === null}
<div class="grid grid-cols-1 gap-4 md:grid-cols-[auto_1fr]">
<div class="prose dark:prose-invert">
<h3>Lottery</h3>
<p>
Draft &num;{draft.draft_id} is almost done! The final stage is the lottery phase, where the remaining undrafted
students are randomly assigned to their labs. Before the system automatically randomizes anything, administrators
are given a final chance to manually intervene with the draft results.
</p>
<ul>
<li>
The <strong>"Eligible for Lottery"</strong> section features a list of the remaining undrafted students.
Administrators may negotiate with the lab heads on how to manually assign and distribute these students
fairly among interested labs.
</li>
<li>
Meanwhile, the <strong>"Already Drafted"</strong> section features an <em>immutable</em> list of students
who have already been drafted into their respective labs. These are considered final.
</li>
</ul>
<p>
<!-- TODO: Add reminder about resetting the lab quota. -->
When ready, administrators can press the <strong>"Conclude Draft"</strong> button to proceed with the randomization
stage. The list of students will be randomly shuffled and distributed among the labs in a round-robin fashion.
To uphold fairness, it is important that uneven distributions are manually resolved beforehand.
</p>
<p>
After the randomization stage, the draft process is officially complete. All students, lab heads, and
administrators are notified of the final results.
</p>
<ConcludeForm draft={draft.draft_id} />
</div>
<div class="min-w-max space-y-2">
<nav class="card list-nav variant-ghost-warning space-y-4 p-4">
<h3 class="h3">Eligible for Lottery ({available.length})</h3>
{#if available.length > 0}
<InterveneForm draft={draft.draft_id} {labs} students={available} />
{:else}
<p class="prose dark:prose-invert max-w-none">
Congratulations! All participants have been drafted. No action is needed here.
</p>
{/if}
</nav>
<nav class="card list-nav variant-ghost-success space-y-4 p-4">
<h3 class="h3">Already Drafted ({selected.length})</h3>
<ul class="list">
{#each selected as user (user.email)}
<li><Student {user} /></li>
{/each}
</ul>
<p>
<!-- TODO: Add reminder about resetting the lab quota. -->
When ready, administrators can press the <strong>"Conclude Draft"</strong> button to proceed with the
randomization stage. The list of students will be randomly shuffled and distributed among the labs in
a round-robin fashion. To uphold fairness, it is important that uneven distributions are manually resolved
beforehand.
</p>
<p>
After the randomization stage, the draft process is officially complete. All students, lab heads,
and administrators are notified of the final results.
</p>
<ConcludeForm draft={draft_id} />
</div>
<div class="min-w-max space-y-2">
<nav class="card list-nav variant-ghost-warning space-y-4 p-4">
<h3 class="h3">Eligible for Lottery ({available.length})</h3>
{#if available.length > 0}
<InterveneForm draft={draft_id} {labs} students={available} />
{:else}
<p class="prose dark:prose-invert max-w-none">
Congratulations! All participants have been drafted. No action is needed here.
</p>
{/if}
</nav>
<nav class="card list-nav variant-ghost-success space-y-4 p-4">
<h3 class="h3">Already Drafted ({selected.length})</h3>
<ul class="list">
{#each selected as user (user.email)}
<li><Student {user} /></li>
{/each}
</ul>
</nav>
</div>
</nav>
</div>
{:else if curr_round > 0}
<!-- TODO: Ongoing Draft -->
{:else if available.length > 0}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-[auto_1fr]">
<div class="space-y-4">
</div>
{:else if draft.curr_round > 0}
<Dashboard {labs} {records} {available} {selected} round={draft.curr_round} />
{:else}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-[auto_1fr]">
<div class="space-y-4">
{#if available.length > 0}
<section class="prose dark:prose-invert">
<h2>Registered Students</h2>
<p>
Expand All @@ -127,25 +104,24 @@
<p>
Lab heads will be notified when the first round begins. The draft proceeds to the next round
when all lab heads have submitted their preferences. This process repeats until the configured
maximum number of rounds has elapsed, after which the draft pauses until an administrator <em
>manually</em
> proceeds with the lottery stage.
maximum number of rounds has elapsed, after which the draft pauses until an administrator
<em>manually</em> proceeds with the lottery stage.
</p>
</section>
<StartForm draft={draft_id} />
</div>
<nav class="list-nav w-full">
<ul class="list">
{#each available as user (user.email)}
<li><Student {user} /></li>
{/each}
</ul>
</nav>
<StartForm draft={draft.draft_id} />
{:else}
<WarningAlert
>No students have registered for this draft yet. The draft cannot proceed until at least one student
participates.</WarningAlert
>
{/if}
</div>
{:else}
<WarningAlert
>No students have registered for this draft yet. This draft cannot proceed to the next round until at least
one student registers.</WarningAlert
>
{/if}
<nav class="list-nav w-full">
<ul class="list">
{#each available as user (user.email)}
<li><Student {user} /></li>
{/each}
</ul>
</nav>
</div>
{/if}
86 changes: 86 additions & 0 deletions app/src/routes/dashboard/(admin)/drafts/Dashboard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<script lang="ts" context="module">
const enum TabType {
Students,
Labs,
Logs,
}
</script>

<script lang="ts">
import { AcademicCap, Beaker, CheckCircle, PaperClip, QuestionMarkCircle } from '@steeze-ui/heroicons';
import { Accordion, AccordionItem, Tab, TabGroup } from '@skeletonlabs/skeleton';
import ErrorAlert from '$lib/alerts/Error.svelte';
import { Icon } from '@steeze-ui/svelte-icon';
import LabAccordionItem from './LabAccordionItem.svelte';
import Student from '$lib/users/Student.svelte';
import SystemLogsTab from './SystemLogsTab.svelte';
import type { ChoiceRecords, TaggedStudentsWithLabs } from 'drap-database';
import type { Lab } from 'drap-model/lab';
// eslint-disable-next-line init-declarations
export let round: number;
// eslint-disable-next-line init-declarations
export let labs: Lab[];
// eslint-disable-next-line init-declarations
export let records: ChoiceRecords;
// eslint-disable-next-line init-declarations
export let available: TaggedStudentsWithLabs;
// eslint-disable-next-line init-declarations
export let selected: TaggedStudentsWithLabs;
$: total = available.length + selected.length;
let group = TabType.Students;
</script>

<TabGroup>
<Tab bind:group name="students" value={TabType.Students}>
<Icon src={AcademicCap} slot="lead" class="h-8" />
<span>Registered Students</span>
</Tab>
<Tab bind:group name="labs" value={TabType.Labs}>
<Icon src={Beaker} slot="lead" class="h-8" />
<span>Laboratories</span>
</Tab>
<Tab bind:group name="logs" value={TabType.Logs}>
<Icon src={PaperClip} slot="lead" class="h-8" />
<span>System Logs</span>
</Tab>
<svelte:fragment slot="panel">
{#if group === TabType.Students}
<Accordion>
<AccordionItem open>
<Icon src={CheckCircle} slot="lead" class="h-8" />
<span slot="summary">Pending Selection ({available.length}/{total})</span>
<svelte:fragment slot="content">
{#each available as student}
<Student user={student} />
{/each}
</svelte:fragment>
</AccordionItem>
<AccordionItem>
<Icon src={QuestionMarkCircle} slot="lead" class="h-8" />
<span slot="summary">Already Drafted ({selected.length}/{total})</span>
<svelte:fragment slot="content">
{#each selected as student}
<Student user={student} />
{/each}
</svelte:fragment>
</AccordionItem>
</Accordion>
{:else if group === TabType.Labs}
<Accordion>
{#each labs as lab (lab.lab_id)}
<div class="card space-y-4 p-4">
<LabAccordionItem {lab} {round} {available} {selected} />
</div>
{/each}
</Accordion>
{:else if group === TabType.Logs}
<SystemLogsTab {records} />
{:else}
<ErrorAlert>This is an unexpected tab state. Kindly report this bug.</ErrorAlert>
{/if}
</svelte:fragment>
</TabGroup>
Loading

0 comments on commit c0a38dc

Please sign in to comment.