From 3b6caa0ec1a3c0ba4e34cb8345492a225f3a11ae Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 22 Aug 2026 23:08:10 +0000 Subject: [PATCH] Terminal --- src/components/gradebook.tsx | 361 ----------------------------------- 1 file changed, 361 deletions(-) delete mode 100644 src/components/gradebook.tsx diff --git a/src/components/gradebook.tsx b/src/components/gradebook.tsx deleted file mode 100644 index 00af8ec..0000000 --- a/src/components/gradebook.tsx +++ /dev/null @@ -1,361 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { supabase } from "@/integrations/supabase/client"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Plus, Trash2 } from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; -import { studentPercent, letterFor, fmtPct, type Cat, type Scale, type Assn, type Grd } from "@/lib/grades"; - -// Effective config for a class = its own rows if any, otherwise the school defaults (class_id NULL). -export function useEffectiveConfig(classId: string) { - const { data } = useQuery({ - queryKey: ["grading-config", classId], - queryFn: async () => { - const [ownC, defC, ownS, defS] = await Promise.all([ - supabase.from("grade_categories").select("*").eq("class_id", classId).order("sort_order"), - supabase.from("grade_categories").select("*").is("class_id", null).order("sort_order"), - supabase.from("grade_scale").select("*").eq("class_id", classId).order("min_percent", { ascending: false }), - supabase.from("grade_scale").select("*").is("class_id", null).order("min_percent", { ascending: false }), - ]); - return { - categories: ((ownC.data?.length ? ownC.data : defC.data) ?? []) as Cat[], - scale: ((ownS.data?.length ? ownS.data : defS.data) ?? []) as Scale[], - classCategories: !!ownC.data?.length, - classScale: !!ownS.data?.length, - }; - }, - }); - return data ?? { categories: [] as Cat[], scale: [] as Scale[], classCategories: false, classScale: false }; -} - -// ── Grading config editor. classId = null edits the school defaults. ────────── -export function GradingConfigEditor({ classId }: { classId: string | null }) { - const qc = useQueryClient(); - const key = classId ?? "default"; - const catQ = useQuery({ - queryKey: ["gc-cats", key], - queryFn: async () => { - const q = supabase.from("grade_categories").select("*").order("sort_order"); - return ((classId ? await q.eq("class_id", classId) : await q.is("class_id", null)).data ?? []); - }, - }); - const defCatsQ = useQuery({ - queryKey: ["gc-cats", "default"], - enabled: !!classId, - queryFn: async () => (await supabase.from("grade_categories").select("*").is("class_id", null).order("sort_order")).data ?? [], - }); - const scaleQ = useQuery({ - queryKey: ["gc-scale", key], - queryFn: async () => { - const q = supabase.from("grade_scale").select("*").order("min_percent", { ascending: false }); - return ((classId ? await q.eq("class_id", classId) : await q.is("class_id", null)).data ?? []); - }, - }); - - const invalidate = () => { - qc.invalidateQueries({ queryKey: ["gc-cats", key] }); - qc.invalidateQueries({ queryKey: ["gc-scale", key] }); - if (classId) qc.invalidateQueries({ queryKey: ["grading-config", classId] }); - }; - - const cats = catQ.data ?? []; - const scale = scaleQ.data ?? []; - const usingDefaults = !!classId && cats.length === 0; - - const addCat = useMutation({ - mutationFn: async () => { const { error } = await supabase.from("grade_categories").insert({ class_id: classId, name: "New category", weight: 0, sort_order: cats.length }); if (error) throw error; }, - onSuccess: invalidate, onError: (e: Error) => toast.error(e.message), - }); - const customize = useMutation({ - mutationFn: async () => { - const rows = (defCatsQ.data ?? []).map((d, i) => ({ class_id: classId, name: d.name, weight: d.weight, sort_order: i })); - if (rows.length) { const { error } = await supabase.from("grade_categories").insert(rows); if (error) throw error; } - }, - onSuccess: invalidate, onError: (e: Error) => toast.error(e.message), - }); - const addScale = useMutation({ - mutationFn: async () => { const { error } = await supabase.from("grade_scale").insert({ class_id: classId, letter: "?", min_percent: 0 }); if (error) throw error; }, - onSuccess: invalidate, onError: (e: Error) => toast.error(e.message), - }); - - const totalWeight = cats.reduce((s, c) => s + Number(c.weight), 0); - - return ( -
-
-
-
-

Categories & weights

- {usingDefaults ?

Using school defaults. Customize to override for this class.

- :

Weights total {totalWeight}% {totalWeight !== 100 ? "(should be 100%)" : ""}

} -
- {usingDefaults ? - : } -
- {(usingDefaults ? (defCatsQ.data ?? []) : cats).map((c) => ( - - ))} -
- -
-
-

Letter grade scale

{usingDefaults &&

Using school defaults.

}
- {!usingDefaults && } -
-
- {(usingDefaults ? [] : scale).map((r) => )} - {usingDefaults &&

Customize categories above to also override the scale.

} -
-
-
- ); -} - -function CategoryRow({ c, readOnly, onChange }: { c: { id: string; name: string; weight: number }; readOnly: boolean; onChange: () => void }) { - const [name, setName] = useState(c.name); - const [weight, setWeight] = useState(String(c.weight)); - const save = useMutation({ - mutationFn: async () => { const { error } = await supabase.from("grade_categories").update({ name, weight: Number(weight) || 0 }).eq("id", c.id); if (error) throw error; }, - onSuccess: onChange, onError: (e: Error) => toast.error(e.message), - }); - const del = useMutation({ mutationFn: async () => { const { error } = await supabase.from("grade_categories").delete().eq("id", c.id); if (error) throw error; }, onSuccess: onChange }); - if (readOnly) return
{c.name}{Number(c.weight)}%
; - return ( -
- setName(e.target.value)} onBlur={() => save.mutate()} /> -
setWeight(e.target.value)} onBlur={() => save.mutate()} />%
- -
- ); -} - -function ScaleRowEdit({ r, onChange }: { r: { id: string; letter: string; min_percent: number }; onChange: () => void }) { - const [letter, setLetter] = useState(r.letter); - const [min, setMin] = useState(String(r.min_percent)); - const save = useMutation({ - mutationFn: async () => { const { error } = await supabase.from("grade_scale").update({ letter, min_percent: Number(min) || 0 }).eq("id", r.id); if (error) throw error; }, - onSuccess: onChange, onError: (e: Error) => toast.error(e.message), - }); - const del = useMutation({ mutationFn: async () => { const { error } = await supabase.from("grade_scale").delete().eq("id", r.id); if (error) throw error; }, onSuccess: onChange }); - return ( -
- setLetter(e.target.value)} onBlur={() => save.mutate()} /> - ≥ - setMin(e.target.value)} onBlur={() => save.mutate()} /> - -
- ); -} - -// ── Roster: which students are in this class (admin assigns). ───────────────── -export function RosterManager({ classId, canAssign }: { classId: string; canAssign: boolean }) { - const qc = useQueryClient(); - const { data: roster } = useQuery({ - queryKey: ["roster", classId], - queryFn: async () => (await supabase.from("students").select("id, first_name, last_name").eq("class_id", classId).order("last_name")).data ?? [], - }); - const { data: available } = useQuery({ - queryKey: ["roster-available", classId], - enabled: canAssign, - queryFn: async () => (await supabase.from("students").select("id, first_name, last_name, class_id").order("last_name")).data ?? [], - }); - const [pick, setPick] = useState(""); - const assign = useMutation({ - mutationFn: async (studentId: string) => { const { error } = await supabase.from("students").update({ class_id: classId }).eq("id", studentId); if (error) throw error; }, - onSuccess: () => { setPick(""); qc.invalidateQueries({ queryKey: ["roster", classId] }); qc.invalidateQueries({ queryKey: ["roster-available", classId] }); qc.invalidateQueries({ queryKey: ["students"] }); }, - onError: (e: Error) => toast.error(e.message), - }); - const remove = useMutation({ - mutationFn: async (studentId: string) => { const { error } = await supabase.from("students").update({ class_id: null }).eq("id", studentId); if (error) throw error; }, - onSuccess: () => { qc.invalidateQueries({ queryKey: ["roster", classId] }); qc.invalidateQueries({ queryKey: ["roster-available", classId] }); }, - onError: (e: Error) => toast.error(e.message), - }); - const addable = (available ?? []).filter((s) => s.class_id !== classId); - - return ( -
- {canAssign && ( -
-
- -
- -
- )} -
- {(roster ?? []).map((s) => ( -
- {s.last_name}, {s.first_name} - {canAssign && } -
- ))} - {roster?.length === 0 &&
No students in this class yet.
} -
-
- ); -} - -// ── Gradebook grid ──────────────────────────────────────────────────────────── -export function GradebookGrid({ classId, canEdit }: { classId: string; canEdit: boolean }) { - const qc = useQueryClient(); - const cfg = useEffectiveConfig(classId); - const { data: roster } = useQuery({ - queryKey: ["roster", classId], - queryFn: async () => (await supabase.from("students").select("id, first_name, last_name").eq("class_id", classId).order("last_name")).data ?? [], - }); - const { data: assignments } = useQuery({ - queryKey: ["assignments", classId], - queryFn: async () => (await supabase.from("assignments").select("*").eq("class_id", classId).order("date", { ascending: true, nullsFirst: true })).data ?? [], - }); - const aIds = (assignments ?? []).map((a) => a.id); - const { data: grades } = useQuery({ - queryKey: ["grades", classId, aIds.length], - enabled: aIds.length >= 0, - queryFn: async () => aIds.length ? ((await supabase.from("grades").select("assignment_id, student_id, points").in("assignment_id", aIds)).data ?? []) : [], - }); - - const [na, setNa] = useState({ name: "", category_id: "", max_points: "100", date: "" }); - const [dateKey, setDateKey] = useState(0); // bump to remount the uncontrolled date input after adding - const addAssignment = useMutation({ - mutationFn: async () => { - if (!na.name.trim()) throw new Error("Assignment name required"); - const { data: u } = await supabase.auth.getUser(); - const { error } = await supabase.from("assignments").insert({ - class_id: classId, name: na.name.trim(), category_id: na.category_id || null, - max_points: Number(na.max_points) || 100, date: na.date || null, created_by: u.user?.id, - }); - if (error) throw error; - }, - onSuccess: () => { setNa({ name: "", category_id: "", max_points: "100", date: "" }); setDateKey((k) => k + 1); qc.invalidateQueries({ queryKey: ["assignments", classId] }); toast.success("Assignment added"); }, - onError: (e: Error) => toast.error(e.message), - }); - const delAssignment = useMutation({ - mutationFn: async (id: string) => { const { error } = await supabase.from("assignments").delete().eq("id", id); if (error) throw error; }, - onSuccess: () => { qc.invalidateQueries({ queryKey: ["assignments", classId] }); qc.invalidateQueries({ queryKey: ["grades", classId] }); }, - }); - - const catName = (id: string | null) => cfg.categories.find((c) => c.id === id)?.name ?? "—"; - - return ( -
- {canEdit && ( -
-
New assignment
-
- setNa({ ...na, name: e.target.value })} /> - - setNa({ ...na, max_points: e.target.value })} /> - setNa({ ...na, date: e.target.value })} /> - -
-
- )} - -
- - - - - {(assignments ?? []).map((a) => ( - - ))} - - - - - {(roster ?? []).map((s) => { - const pct = studentPercent(s.id, (assignments ?? []) as Assn[], (grades ?? []) as Grd[], cfg.categories); - return ( - - - {(assignments ?? []).map((a) => ( - - ))} - - - ); - })} - {roster?.length === 0 && } - -
Student -
{a.name}
-
{catName(a.category_id)} · /{Number(a.max_points)}
- {canEdit && } -
Overall
{s.last_name}, {s.first_name} - - {fmtPct(pct)} {letterFor(pct, cfg.scale)}
No students in this class. Add them in the Roster tab.
-
-
- ); -} - -function GradeCell({ classId, assignmentId, studentId, maxPoints, grades, canEdit }: { classId: string; assignmentId: string; studentId: string; maxPoints: number; grades: Grd[]; canEdit: boolean }) { - const qc = useQueryClient(); - const existing = grades.find((g) => g.assignment_id === assignmentId && g.student_id === studentId); - const [v, setV] = useState(existing?.points != null ? String(existing.points) : ""); - const save = useMutation({ - mutationFn: async () => { - const points = v.trim() === "" ? null : Number(v); - const { error } = await supabase.from("grades").upsert({ assignment_id: assignmentId, student_id: studentId, points }, { onConflict: "assignment_id,student_id" }); - if (error) throw error; - }, - onSuccess: () => qc.invalidateQueries({ queryKey: ["grades", classId] }), - onError: (e: Error) => toast.error(e.message), - }); - if (!canEdit) return {existing?.points != null ? Number(existing.points) : "—"}; - return setV(e.target.value)} onBlur={() => save.mutate()} title={`out of ${maxPoints}`} />; -} - -// ── Read-only grade report for one student (parent/student/staff view). ─────── -export function StudentGradeReport({ studentId, classId }: { studentId: string; classId: string | null }) { - const cfg = useEffectiveConfig(classId ?? ""); - const { data: assignments } = useQuery({ - queryKey: ["assignments", classId], - enabled: !!classId, - queryFn: async () => classId ? ((await supabase.from("assignments").select("*").eq("class_id", classId).order("date", { ascending: true, nullsFirst: true })).data ?? []) : [], - }); - const aIds = (assignments ?? []).map((a) => a.id); - const { data: grades } = useQuery({ - queryKey: ["grades-student", studentId, aIds.length], - enabled: aIds.length > 0, - queryFn: async () => (await supabase.from("grades").select("assignment_id, student_id, points").eq("student_id", studentId).in("assignment_id", aIds)).data ?? [], - }); - - if (!classId) return

This student isn't assigned to a class yet.

; - const gr = (grades ?? []) as Grd[]; - const pct = studentPercent(studentId, (assignments ?? []) as Assn[], gr, cfg.categories); - - return ( -
-
-
Overall grade
-
{fmtPct(pct)} {letterFor(pct, cfg.scale)}
-
- {cfg.categories.map((cat) => { - const items = (assignments ?? []).filter((a) => a.category_id === cat.id); - if (items.length === 0) return null; - return ( -
-
{cat.name}{Number(cat.weight)}%
- - {items.map((a) => { - const g = gr.find((x) => x.assignment_id === a.id); - return ; - })} -
{a.name}{g?.points != null ? `${Number(g.points)} / ${Number(a.max_points)}` : "—"}
-
- ); - })} - {(assignments ?? []).length === 0 &&

No assignments yet.

} -
- ); -}