Skip to content

Commit

Permalink
feat(admin): present results of main draft before lottery phase begins
Browse files Browse the repository at this point in the history
  • Loading branch information
BastiDood committed Jul 11, 2024
1 parent bb224b0 commit 6b53eef
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 59 deletions.
14 changes: 8 additions & 6 deletions src/lib/server/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { fail, strictEqual } from 'node:assert/strict';
import type { Logger } from 'pino';
import postgres from 'postgres';

import { FacultyChoice, FacultyChoiceEmail } from '$lib/models/faculty-choice';
import { Pending, Session } from '$lib/server/models/session';
import { Draft } from '$lib/models/draft';
import { FacultyChoice } from '$lib/models/faculty-choice';
import { Lab } from '$lib/models/lab';
import { StudentRank } from '$lib/models/student-rank';
import { User } from '$lib/models/user';
Expand Down Expand Up @@ -35,15 +35,17 @@ const QueriedStudentRank = object({
});
const RegisteredLabs = array(Lab);
const StudentsWithLabPreference = array(pick(User, ['email', 'given_name', 'family_name', 'avatar', 'student_number']));
const StudentsWithLabs = array(
const TaggedStudentsWithLabs = array(
object({
...pick(User, ['email', 'given_name', 'family_name', 'avatar', 'student_number']).entries,
...pick(StudentRank, ['labs']).entries,
lab_id: nullable(FacultyChoiceEmail.entries.lab_id),
}),
);

export type AvailableLabs = InferOutput<typeof AvailableLabs>;
export type QueriedFaculty = InferOutput<typeof QueriedFaculty>;
export type TaggedStudentsWithLabs = InferOutput<typeof TaggedStudentsWithLabs>;

export type Sql = postgres.Sql<{ bigint: bigint }>;

Expand Down Expand Up @@ -211,11 +213,11 @@ export class Database implements Loggable {
return typeof first === 'undefined' ? null : parse(DraftMaxRounds, first).max_rounds;
}

@timed async getStudentsInDraft(draft: Draft['draft_id']) {
@timed async getStudentsInDraftTaggedByLab(draft: Draft['draft_id']) {
const sql = this.#sql;
const users =
await sql`SELECT email, given_name, family_name, avatar, student_number, labs FROM drap.student_ranks JOIN drap.drafts USING (draft_id) JOIN drap.users USING (email) WHERE draft_id = ${draft}`;
return parse(StudentsWithLabs, users);
const students =
await sql`SELECT email, given_name, family_name, avatar, student_number, labs, fce.lab_id FROM drap.student_ranks sr JOIN drap.users u USING (email) LEFT JOIN drap.faculty_choices_emails fce ON u.email = student_email WHERE sr.draft_id = ${draft}`;
return parse(TaggedStudentsWithLabs, students);
}

@timed async getLabAndRemainingStudentsInDraftWithLabPreference(
Expand Down
19 changes: 19 additions & 0 deletions src/lib/users/Faculty.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts">
import { Avatar } from '@skeletonlabs/skeleton';
import type { QueriedFaculty } from '$lib/server/database';
// eslint-disable-next-line init-declarations
export let user: QueriedFaculty[number];
$: ({ email, given_name, family_name, avatar, lab_name } = user);
</script>

<a href="mailto:{email}">
<Avatar src={avatar} width="w-14" />
<span class="flex flex-col">
<strong><span class="uppercase">{family_name}</span>, {given_name}</strong>
{#if lab_name !== null}
<span class="text-sm opacity-50">{lab_name}</span>
{/if}
<span class="text-xs opacity-50">{email}</span>
</span>
</a>
28 changes: 28 additions & 0 deletions src/lib/users/Student.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
import { Avatar } from '@skeletonlabs/skeleton';
import type { TaggedStudentsWithLabs } from '$lib/server/database';
// eslint-disable-next-line init-declarations
export let user: TaggedStudentsWithLabs[number];
$: ({ 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">
<Avatar src={avatar} width="w-20" />
<div class="flex flex-col">
<strong><span class="uppercase">{family_name}</span>, {given_name}</strong>
{#if student_number !== null}
<span class="text-sm opacity-50">{student_number}</span>
{/if}
<span class="text-xs opacity-50">{email}</span>
<div class="space-x-1">
{#if lab_id === null}
{#each labs as lab (lab)}
<span class="variant-ghost-tertiary badge text-xs">{lab}</span>
{/each}
{:else}
<span class="variant-ghost-primary badge text-xs">{lab_id}</span>
{/if}
</div>
</div>
</a>
7 changes: 6 additions & 1 deletion src/routes/dashboard/drafts/+page.server.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import assert from 'node:assert/strict';
import { error } from '@sveltejs/kit';
import groupBy from 'just-group-by';
import { validateString } from '$lib/forms';

export async function load({ locals: { db }, parent }) {
const { user } = await parent();
if (!user.is_admin || user.user_id === null || user.lab_id !== null) error(403);

const draft = await db.getLatestDraft();
if (draft === null) error(499);
return { draft, students: await db.getStudentsInDraft(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, available: available ?? [], selected: selected ?? [] };
}

export const actions = {
Expand Down
73 changes: 47 additions & 26 deletions src/routes/dashboard/drafts/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<script>
import { Avatar } from '@skeletonlabs/skeleton';
import ErrorAlert from '$lib/alerts/Error.svelte';
import Student from '$lib/users/Student.svelte';
import { assert } from '$lib/assert';
import { enhance } from '$app/forms';
import { format } from 'date-fns';
// eslint-disable-next-line
// eslint-disable-next-line init-declarations
export let data;
$: ({
draft: { draft_id, curr_round, max_rounds, active_period_start },
students,
available,
selected,
} = data);
$: startDate = format(active_period_start, 'PPP');
$: startTime = format(active_period_start, 'pp');
Expand All @@ -28,14 +29,50 @@
{/if}
</p>
</div>
{#if curr_round > 0}
<!-- TODO -->
{#if curr_round > max_rounds}
<div class="prose max-w-none 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>"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>
<li>
Meanwhile, the <strong>"Eligible for Lottery"</strong> section
</li>
</ul>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<nav class="card list-nav variant-ghost-success space-y-4 p-4">
<h3 class="h3">Already Drafted</h3>
<ul class="list">
{#each selected as user (user.email)}
<li><Student {user} /></li>
{/each}
</ul>
</nav>
<nav class="card list-nav variant-ghost-warning space-y-4 p-4">
<h3 class="h3">Eligible for Lottery</h3>
<ul class="list">
{#each available as user (user.email)}
<li><Student {user} /></li>
{/each}
</ul>
</nav>
</div>
{:else if curr_round > 0}
<!-- TODO: Ongoing Draft -->
{:else}
<div class="prose max-w-none dark:prose-invert">
<h2>Registered Students</h2>
{#if students.length > 0}
{#if available.length > 0}
<p>
There are currently <strong>{students.length}</strong> students who have registered for this draft.
There are currently <strong>{available.length}</strong> students who have registered for this draft.
Press the <strong>"Start Draft"</strong> button to close registration and start the draft automation.
Lab heads will be notified about the first round. The draft proceeds to the next round when all lab
heads have submitted their preferences. This process repeats until the configured maximum number of
Expand All @@ -49,7 +86,7 @@
>
{/if}
</div>
{#if students.length > 0}
{#if available.length > 0}
<form
method="post"
action="?/start"
Expand All @@ -72,24 +109,8 @@
</form>
<nav class="list-nav">
<ul class="list">
{#each students as { email, given_name, family_name, avatar, student_number, labs } (email)}
<li>
<a href="mailto:{email}" class="grid w-full grid-cols-[auto_1fr] items-center gap-4 p-4">
<Avatar src={avatar} width="w-20" />
<div class="flex flex-col">
<strong><span class="uppercase">{family_name}</span>, {given_name}</strong>
{#if student_number !== null}
<span class="text-sm opacity-50">{student_number}</span>
{/if}
<span class="text-xs opacity-50">{email}</span>
<div class="space-x-1">
{#each labs as lab (lab)}
<span class="variant-ghost-primary badge text-xs">{lab}</span>
{/each}
</div>
</div>
</a>
</li>
{#each available as user (user.email)}
<li><Student {user} /></li>
{/each}
</ul>
</nav>
Expand Down
10 changes: 5 additions & 5 deletions src/routes/dashboard/users/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script>
import Faculty from '$lib/users/Faculty.svelte';
import { Icon } from '@steeze-ui/svelte-icon';
import ListItem from './ListItem.svelte';
import { PaperAirplane } from '@steeze-ui/heroicons';
import { assert } from '$lib/assert';
import { enhance } from '$app/forms';
Expand Down Expand Up @@ -79,15 +79,15 @@
<h3 class="h3">Invited</h3>
<ul>
{#each invitedHeads as head (head.email)}
<ListItem user={head} />
<li><Faculty user={head} /></li>
{/each}
</ul>
</nav>
<nav class="list-nav space-y-2">
<h3 class="h3">Registered</h3>
<ul>
{#each registeredHeads as head (head.email)}
<ListItem user={head} />
<li><Faculty user={head} /></li>
{/each}
</ul>
</nav>
Expand Down Expand Up @@ -140,15 +140,15 @@
<h3 class="h3">Invited</h3>
<ul>
{#each invitedAdmins as head (head.email)}
<ListItem user={head} />
<li><Faculty user={head} /></li>
{/each}
</ul>
</nav>
<nav class="list-nav space-y-2">
<h3 class="h3">Registered</h3>
<ul>
{#each registeredAdmins as head (head.email)}
<ListItem user={head} />
<li><Faculty user={head} /></li>
{/each}
</ul>
</nav>
Expand Down
21 changes: 0 additions & 21 deletions src/routes/dashboard/users/ListItem.svelte

This file was deleted.

0 comments on commit 6b53eef

Please sign in to comment.