Gradebook: classes, weighted grading, assignments, grades, student role
- Migration: student role, students.user_id, grade_categories/grade_scale (school defaults + per-class override), assignments, grades + RLS - Classes area (nav): list/create classes, class page with Gradebook / Roster / Grading setup tabs; teachers see their classes, admins all - Weighted-category grade calc -> letter scale (shared helper) - Roster assignment (admin), assignment + grade entry (teacher/admin) - Student profile: view-only Grades tab (parents/students) - Portal access: create parent OR student logins linked to the student Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
// Weighted-category grade calculation shared by the gradebook and the grade views.
|
||||
export type Cat = { id: string; name: string; weight: number };
|
||||
export type Scale = { letter: string; min_percent: number };
|
||||
export type Assn = { id: string; category_id: string | null; max_points: number };
|
||||
export type Grd = { assignment_id: string; student_id: string; points: number | null };
|
||||
|
||||
// Overall percent for a student: weighted average across categories that have graded work.
|
||||
export function studentPercent(studentId: string, assignments: Assn[], grades: Grd[], categories: Cat[]): number | null {
|
||||
const byCat: Record<string, { earned: number; possible: number }> = {};
|
||||
for (const a of assignments) {
|
||||
if (!a.category_id) continue;
|
||||
const g = grades.find((x) => x.assignment_id === a.id && x.student_id === studentId && x.points != null);
|
||||
if (!g) continue;
|
||||
const b = byCat[a.category_id] ?? { earned: 0, possible: 0 };
|
||||
b.earned += Number(g.points);
|
||||
b.possible += Number(a.max_points);
|
||||
byCat[a.category_id] = b;
|
||||
}
|
||||
let weighted = 0, totalW = 0;
|
||||
for (const c of categories) {
|
||||
const b = byCat[c.id];
|
||||
if (!b || b.possible === 0) continue;
|
||||
weighted += (b.earned / b.possible) * 100 * Number(c.weight);
|
||||
totalW += Number(c.weight);
|
||||
}
|
||||
return totalW === 0 ? null : weighted / totalW;
|
||||
}
|
||||
|
||||
export function letterFor(pct: number | null, scale: Scale[]): string {
|
||||
if (pct == null) return "—";
|
||||
const s = [...scale].sort((a, b) => Number(b.min_percent) - Number(a.min_percent));
|
||||
for (const r of s) if (pct >= Number(r.min_percent)) return r.letter;
|
||||
return s.length ? s[s.length - 1].letter : "—";
|
||||
}
|
||||
|
||||
export const fmtPct = (pct: number | null) => (pct == null ? "—" : `${pct.toFixed(1)}%`);
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { requireSupabaseAuth } from "@/integrations/supabase/auth-middleware";
|
||||
|
||||
type Role = "admin" | "teacher" | "parent";
|
||||
type Role = "admin" | "teacher" | "parent" | "student";
|
||||
|
||||
// Admin-only: create a user account (service-role, server-side), set its role,
|
||||
// and optionally link it to a student (for parent hand-off).
|
||||
@@ -42,6 +42,11 @@ export const createUserFn = createServerFn({ method: "POST" })
|
||||
.from("parent_students").upsert({ parent_id: uid, student_id: data.linkStudentId }, { onConflict: "parent_id,student_id" });
|
||||
if (lErr) throw new Error(lErr.message);
|
||||
}
|
||||
if (data.linkStudentId && data.role === "student") {
|
||||
const { error: sErr } = await supabaseAdmin
|
||||
.from("students").update({ user_id: uid }).eq("id", data.linkStudentId);
|
||||
if (sErr) throw new Error(sErr.message);
|
||||
}
|
||||
|
||||
return { id: uid, email };
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user