/* ==================================================================
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 (
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}
setAdding(true)} disabled={adding}>
Add class
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 (
Cancel
{busy ? "Saving…" : initial ? "Save" : "Add class"}
);
};
/* --------------------- 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}
setAdding(true)} disabled={adding}>
Add subject
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 (
Cancel
{busy ? "Saving…" : initial ? "Save" : "Add subject"}
);
};
window.TaxonomyScreen = TaxonomyScreen;