Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions README-quiz-bank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Quiz Bank Management System

## Architecture overview

The quiz bank feature is built as a teacher-first extension of the existing quiz flow:

- Subjects and reusable bank questions are stored separately from individual quizzes.
- Teachers browse the bank, select multiple questions, and create a quiz from that selection.
- Each quiz created from the bank can generate shuffled versions A/B/C automatically.
- The existing quiz and auth layers remain in place, so student participation continues to work without disruption.

## Core entities

- Subject: top-level category for bank questions.
- QuizBankQuestion: reusable question item with subject, options, answer, difficulty, marks, and status.
- Quiz: teacher-owned quiz wrapper with duration, marks, publish state, and availability window.
- QuizQuestion: mapping between a quiz and selected bank questions.
- QuizVersion: generated randomized version metadata for A/B/C.

## Business rules

- Only authenticated teachers can manage the bank and create quizzes.
- A quiz can contain questions from a single subject.
- Question edits in the bank do not mutate previously published quiz snapshots; the system uses a reusable bank reference model and version metadata.
- Published quizzes remain read-only in this initial implementation unless duplicated later.

## API surface

Teacher-facing server functions:

- listQuizBankSubjects
- listQuizBankQuestions
- createQuizFromBank
- generateQuizVersions
- listQuizVersions

## Seed data

The SQL seed file populates bank questions for the required subjects and includes a mix of Easy, Medium, and Hard items.

## Deployment notes

1. Apply the new Supabase migration.
2. Run the seed SQL to load dummy questions.
3. Open the teacher dashboard and visit Quiz Bank.
45 changes: 2 additions & 43 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/lib/auth/profile.functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ export const updateMyProfile = createServerFn({ method: "POST" })
.inputValidator((input: unknown) => updateSchema.parse(input))
.handler(async ({ data, context }) => {
const { supabase, userId } = context;
const payload = {
...data,
student_id: data.student_id ? data.student_id.toUpperCase() : data.student_id,
};
const { data: updated, error } = await supabase
.from("profiles")
.update(data)
.update(payload)
.eq("id", userId)
.select()
.maybeSingle();
Expand Down
11 changes: 6 additions & 5 deletions src/lib/proctoring/proctoring.functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,11 @@ export const listQuizAttemptsWithRisk = createServerFn({ method: "POST" })
if (error) throw new Error(error.message);

const studentIds = [...new Set((attempts ?? []).map((a) => a.student_id))];
const profMap = new Map<string, { full_name: string | null; email: string | null }>();
const profMap = new Map<string, { full_name: string | null; email: string | null; student_id: string | null }>();
if (studentIds.length > 0) {
const { data: profs } = await supabase
.from("profiles").select("id, full_name, email").in("id", studentIds);
(profs ?? []).forEach((p) => profMap.set(p.id, { full_name: p.full_name, email: p.email }));
.from("profiles").select("id, full_name, email, student_id").in("id", studentIds);
(profs ?? []).forEach((p) => profMap.set(p.id, { full_name: p.full_name, email: p.email, student_id: p.student_id }));
}

const rows = await Promise.all(
Expand All @@ -180,12 +180,13 @@ export const listQuizAttemptsWithRisk = createServerFn({ method: "POST" })
.select("*", { count: "exact", head: true })
.eq("attempt_id", a.id)
.eq("severity", "critical");
const prof = profMap.get(a.student_id) ?? { full_name: null, email: null };
const prof = profMap.get(a.student_id) ?? { full_name: null, email: null, student_id: null };
return {
attempt_id: a.id,
student_id: a.student_id,
full_name: prof.full_name,
email: prof.email,
student_id_value: prof.student_id,
status: a.status,
score: a.score,
max_score: a.max_score,
Expand Down Expand Up @@ -221,7 +222,7 @@ export const getAttemptProctoring = createServerFn({ method: "POST" })
if (!isTeacher && !isStudent) throw new Error("Not authorized");

const { data: prof } = await supabase
.from("profiles").select("id, full_name, email").eq("id", a.student_id).maybeSingle();
.from("profiles").select("id, full_name, email, student_id").eq("id", a.student_id).maybeSingle();

const { data: snaps } = await supabase
.from("proctoring_snapshots")
Expand Down
Loading