/* ================================================================== Admin → Classes & Subjects ------------------------------------------------------------------ Manage the class_sections and subjects rows that drive every test creation picker, every roster import, and the teacher-assignment matrix. Earlier they only grew (auto-inserted by test creation / roster import) and any typo ("hi" as a subject) lingered forever. This screen lets the admin rename or delete them, with delete blocked when something still references the row. ================================================================== */ const { Icon: TxIcon } = window.AppUI; const TaxonomyScreen = () => { return (
Admin · Catalog

Classes & Subjects

Edit the catalog used by every test-creation picker and the teacher-assignment matrix. Delete is blocked when an entry is still referenced by tests, students, or concepts.

); }; /* --------------------- shared bits --------------------- */ const useList = (path) => { const [items, setItems] = React.useState([]); const [loading, setLoading] = React.useState(true); const [err, setErr] = React.useState(null); const refresh = React.useCallback(async () => { setLoading(true); setErr(null); try { setItems(await window.AppApi.get(path)); } catch (e) { setErr(String(e.message || e)); } finally { setLoading(false); } }, [path]); React.useEffect(() => { refresh(); }, [refresh]); return { items, loading, err, refresh }; }; const ErrBanner = ({ err, onClear }) => err ? (
{err} {onClear && }
) : null; /* --------------------- classes panel --------------------- */ const ClassesPanel = () => { const { items, loading, err, refresh } = useList("/admin/class-sections"); const [actionErr, setActionErr] = React.useState(null); const [adding, setAdding] = React.useState(false); const [editingId, setEditingId] = React.useState(null); const create = async (draft) => { setActionErr(null); try { await window.AppApi.post("/admin/class-sections", draft); setAdding(false); await refresh(); } catch (e) { setActionErr(String(e.message || e)); } }; const update = async (id, draft) => { setActionErr(null); try { await window.AppApi.patch(`/admin/class-sections/${id}`, draft); setEditingId(null); await refresh(); } catch (e) { setActionErr(String(e.message || e)); } }; const remove = async (row) => { if (!window.confirm(`Delete class "${row.label}" (${row.academic_year})?`)) return; setActionErr(null); try { await window.AppApi.del(`/admin/class-sections/${row.id}`); await refresh(); } catch (e) { setActionErr(String(e.message || e)); } }; return (
Classes {items.length}
setActionErr(null)}/> {adding && ( setAdding(false)} onSave={create}/> )} {loading &&
Loading…
} {!loading && items.length === 0 && !adding && (
No classes yet.
)} {items.map(row => editingId === row.id ? setEditingId(null)} onSave={(d) => update(row.id, d)}/> : setEditingId(row.id)} onDelete={() => remove(row)}/> )}
); }; const ClassRow = ({ row, onEdit, onDelete }) => { const safeToDelete = row.test_count === 0 && row.student_count === 0; return (
{row.label} {row.academic_year}
{row.test_count} test{row.test_count === 1 ? "" : "s"} · {row.student_count} student{row.student_count === 1 ? "" : "s"} · {row.assignment_count} scope{row.assignment_count === 1 ? "" : "s"}
); }; const ClassEditor = ({ initial, onCancel, onSave }) => { const [classLevel, setClassLevel] = React.useState(initial?.class_level || ""); const [section, setSection] = React.useState(initial?.section || ""); const [academicYear, setAcademicYear] = React.useState(initial?.academic_year || ""); const [busy, setBusy] = React.useState(false); const submit = async () => { if (!classLevel.trim()) return; setBusy(true); try { await onSave({ class_level: classLevel.trim(), section: section.trim() || null, ...(academicYear.trim() ? { academic_year: academicYear.trim() } : {}), }); } finally { setBusy(false); } }; return (
setClassLevel(e.target.value)} placeholder="e.g. X" style={{ width: "100%" }}/>
setSection(e.target.value)} placeholder="e.g. A" style={{ width: "100%" }}/>
setAcademicYear(e.target.value)} placeholder="defaults to school year" style={{ width: "100%" }}/>
); }; /* --------------------- subjects panel --------------------- */ const SubjectsPanel = () => { const { items, loading, err, refresh } = useList("/admin/subjects"); const [actionErr, setActionErr] = React.useState(null); const [adding, setAdding] = React.useState(false); const [editingId, setEditingId] = React.useState(null); const create = async (draft) => { setActionErr(null); try { await window.AppApi.post("/admin/subjects", draft); setAdding(false); await refresh(); } catch (e) { setActionErr(String(e.message || e)); } }; const update = async (id, draft) => { setActionErr(null); try { await window.AppApi.patch(`/admin/subjects/${id}`, draft); setEditingId(null); await refresh(); } catch (e) { setActionErr(String(e.message || e)); } }; const remove = async (row) => { if (!window.confirm(`Delete subject "${row.name}"?`)) return; setActionErr(null); try { await window.AppApi.del(`/admin/subjects/${row.id}`); await refresh(); } catch (e) { setActionErr(String(e.message || e)); } }; return (
Subjects {items.length}
setActionErr(null)}/> {adding && ( setAdding(false)} onSave={create}/> )} {loading &&
Loading…
} {!loading && items.length === 0 && !adding && (
No subjects yet.
)} {items.map(row => editingId === row.id ? setEditingId(null)} onSave={(d) => update(row.id, d)}/> : setEditingId(row.id)} onDelete={() => remove(row)}/> )}
); }; const SubjectRow = ({ row, onEdit, onDelete }) => { const safeToDelete = row.test_count === 0 && row.concept_count === 0; return (
{row.name} {row.short_code && {row.short_code}}
{row.test_count} test{row.test_count === 1 ? "" : "s"} · {row.assignment_count} scope{row.assignment_count === 1 ? "" : "s"} · {row.concept_count} concept{row.concept_count === 1 ? "" : "s"}
); }; const SubjectEditor = ({ initial, onCancel, onSave }) => { const [name, setName] = React.useState(initial?.name || ""); const [shortCode, setShortCode] = React.useState(initial?.short_code || ""); const [busy, setBusy] = React.useState(false); const submit = async () => { if (!name.trim()) return; setBusy(true); try { await onSave({ name: name.trim(), short_code: shortCode.trim() || null }); } finally { setBusy(false); } }; return (
setName(e.target.value)} placeholder="e.g. Chemistry" style={{ width: "100%" }}/>
setShortCode(e.target.value)} placeholder="e.g. CHEM" style={{ width: "100%" }}/>
); }; window.TaxonomyScreen = TaxonomyScreen;