diff --git a/.gitignore b/.gitignore index 1d41d45..86b310e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ CLAUDE.md .agents/ .claude/ skills-lock.json +.DS_Store \ No newline at end of file diff --git a/src/lib/admin/AdminListView.svelte b/src/lib/admin/AdminListView.svelte new file mode 100644 index 0000000..d9b154d --- /dev/null +++ b/src/lib/admin/AdminListView.svelte @@ -0,0 +1,60 @@ + + +
+

{title}

+ +
+
+ +
+ +
+ + +
diff --git a/src/lib/admin/FilterDropdown.svelte b/src/lib/admin/FilterDropdown.svelte index 6e40f8a..1c2b897 100644 --- a/src/lib/admin/FilterDropdown.svelte +++ b/src/lib/admin/FilterDropdown.svelte @@ -1,4 +1,6 @@ {#if totalPages > 1} -
+
- + Page {currentPage} of {totalPages} diff --git a/src/lib/admin/SortDropdown.svelte b/src/lib/admin/SortDropdown.svelte deleted file mode 100644 index e24c6d2..0000000 --- a/src/lib/admin/SortDropdown.svelte +++ /dev/null @@ -1,71 +0,0 @@ - - - - -
- - {#if open} -
- {#each columns as col (col.key)} - - {/each} -
- {/if} -
diff --git a/src/lib/admin/types.ts b/src/lib/admin/types.ts index add7fe2..0a94875 100644 --- a/src/lib/admin/types.ts +++ b/src/lib/admin/types.ts @@ -61,3 +61,11 @@ export interface GradeInput { max_score: number; remarks?: string; } + +/** Row shape for the constiquiz respondent list page (P02-001). */ +export interface QuizRespondent { + user_id: string; + full_name: string; + status: 'Not Started' | 'In Progress' | 'Completed'; + current_score: number; +} diff --git a/src/lib/server/admin-queries.ts b/src/lib/server/admin-queries.ts index dbf8875..cf4f023 100644 --- a/src/lib/server/admin-queries.ts +++ b/src/lib/server/admin-queries.ts @@ -1,6 +1,7 @@ import type { ApplicantProfile, QuizAnswerDetail, + QuizRespondent, QuizResultDetail, QuizResultSummary, SigsheetProgressSummary, @@ -134,6 +135,71 @@ export async function fetchSigsheetProgress( }; } +export async function fetchQuizRespondents( + supabase: SupabaseClient, +): Promise<{ respondents: QuizRespondent[]; max_score: number }> { + const [applicantsRes, submissionsRes, answersRes, questionsRes] = await Promise.all([ + supabase.from('profiles').select('id, full_name').eq('role', 'applicant'), + supabase.from('constiquiz-submissions').select('user_id'), + supabase.from('constiquiz-answers').select('user_id, points'), + supabase.from('constiquiz-questions').select('point_value'), + ]); + + if (applicantsRes.error) { + throw new Error(applicantsRes.error.message); + } + if (submissionsRes.error) { + throw new Error(submissionsRes.error.message); + } + if (answersRes.error) { + throw new Error(answersRes.error.message); + } + if (questionsRes.error) { + throw new Error(questionsRes.error.message); + } + + const submittedUserIds = new Set(); + for (const row of (submissionsRes.data as Record[] | null) ?? []) { + const uid = row.user_id as string | null; + if (uid) { + submittedUserIds.add(uid); + } + } + + const scoreByUser: Record = {}; + const hasAnswerByUser = new Set(); + for (const row of (answersRes.data as Record[] | null) ?? []) { + const uid = row.user_id as string | null; + if (!uid) continue; + scoreByUser[uid] = (scoreByUser[uid] ?? 0) + ((row.points as number | null) ?? 0); + hasAnswerByUser.add(uid); + } + + const max_score = ((questionsRes.data as Record[] | null) ?? []).reduce( + (sum, q) => sum + ((q.point_value as number | null) ?? 0), + 0, + ); + + const respondents: QuizRespondent[] = ((applicantsRes.data as Record[] | null) ?? []).map(p => { + const uid = p.id as string; + let status: QuizRespondent['status'] = 'Not Started'; + if (submittedUserIds.has(uid)) { + status = 'Completed'; + } else if (hasAnswerByUser.has(uid)) { + status = 'In Progress'; + } + + return { + user_id: uid, + full_name: (p.full_name as string | null) ?? '', + status, + current_score: scoreByUser[uid] ?? 0, + }; + }); + + return { respondents, max_score }; +} + export async function fetchSigsheetDetail( supabase: SupabaseClient, userId: string, diff --git a/src/routes/admin/constiquiz/+page.server.ts b/src/routes/admin/constiquiz/+page.server.ts new file mode 100644 index 0000000..51f8053 --- /dev/null +++ b/src/routes/admin/constiquiz/+page.server.ts @@ -0,0 +1,8 @@ +import type { PageServerLoad } from './$types'; +import { fetchQuizRespondents } from '$lib/server/admin-queries'; +import { getSupabaseAdmin } from '$lib/server/supabaseAdmin'; + +export const load: PageServerLoad = async () => { + const supabase = getSupabaseAdmin(); + return await fetchQuizRespondents(supabase); +}; diff --git a/src/routes/admin/constiquiz/+page.svelte b/src/routes/admin/constiquiz/+page.svelte new file mode 100644 index 0000000..388408f --- /dev/null +++ b/src/routes/admin/constiquiz/+page.svelte @@ -0,0 +1,47 @@ + + +[]} + {columns} + rowKey="user_id" + filterKey="status" + onRowClick={goToDetail} +> + {#snippet cell({ row, column })} + {#if column.key === 'status'} + + {:else if column.key === 'current_score'} + + {row.current_score} / {data.max_score} + + {:else if column.key === 'responses'} + e.stopPropagation()} + > + View response + + {:else} + {String(row[column.key] ?? '')} + {/if} + {/snippet} +