/* ================================================================== Admin → Announcements ------------------------------------------------------------------ Authoring surface for school-wide announcements that flow to the Parent app. Class-specific announcements can be authored here too, or by teachers from the Teacher app. Composer + history list. Soft-delete on the trash button (sets deleted_at; the row stays for audit). WHERE A NOTICE GOES. Every announcement reaches parents in the app — a row in their Notices tab, a push on their phone — the moment it is published. "Also on the website" additionally lists it on the public notice board at {BRAND.website}/notices. It is an opt-in per notice and school-wide only: a class notice is addressed to particular families, and the website is public and indexed. The server and a table CHECK enforce the same rule, so this screen only has to explain it. ================================================================== */ const { Icon: AnnIcon } = window.AppUI; const AdminAnnouncementsScreen = () => { const [rows, setRows] = React.useState([]); const [classSections, setSections] = React.useState([]); const [loading, setLoading] = React.useState(true); const [err, setErr] = React.useState(null); const [scope, setScope] = React.useState("school"); const [classSectionId, setCsId] = React.useState(""); const [title, setTitle] = React.useState(""); const [body, setBody] = React.useState(""); const [pinned, setPinned] = React.useState(false); const [onWebsite, setOnWebsite] = React.useState(false); const [busy, setBusy] = React.useState(false); const [flash, setFlash] = React.useState(null); const load = React.useCallback(async () => { setLoading(true); setErr(null); try { setRows(await window.AppApi.get("/admin/announcements")); } catch (e) { setErr(String(e.message || e)); } finally { setLoading(false); } }, []); React.useEffect(() => { load(); }, [load]); React.useEffect(() => { window.AppApi.get("/admin/class-sections").then(setSections).catch(() => {}); }, []); const reset = () => { setTitle(""); setBody(""); setPinned(false); setOnWebsite(false); }; // A class notice can never be public, so leaving "school-wide" drops the // website choice rather than carrying a selection the server would refuse. const changeScope = (next) => { setScope(next); if (next !== "school") setOnWebsite(false); }; const publish = async (e) => { e.preventDefault(); if (!title.trim() || !body.trim()) { window.alert("Title and body are required."); return; } if (scope !== "school" && !classSectionId) { window.alert("Pick a class."); return; } setBusy(true); setFlash(null); try { // pinned_until = +7 days when toggled on const pinned_until = pinned ? new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() : null; await window.AppApi.post("/admin/announcements", { scope, title: title.trim(), body: body.trim(), class_section_id: scope === "school" ? null : classSectionId, pinned_until, publish_to_website: scope === "school" && onWebsite, }); setFlash(scope === "school" && onWebsite ? "Published — parents notified in the app, and live on the website." : "Published — parents notified in the app."); reset(); await load(); } catch (e) { window.alert("Could not publish: " + (e.message || e)); } finally { setBusy(false); } }; const remove = async (id, t) => { if (!window.confirm(`Delete announcement "${t}"? Parents will stop seeing it.`)) return; try { await window.AppApi.del(`/admin/announcements/${id}`); await load(); } catch (e) { window.alert("Could not delete: " + (e.message || e)); } }; // Put a notice on the website, or take it off, without touching what parents // already have. Deleting would do the latter too, which is rarely the intent. const setWebsite = async (r, on) => { const q = on ? `Show "${r.title}" on the public website? Anyone can read it there.` : `Take "${r.title}" off the website? Parents keep it in the app.`; if (!window.confirm(q)) return; try { await window.AppApi.patch(`/admin/announcements/${r.id}/website`, { publish_to_website: on }); await load(); } catch (e) { window.alert("Could not update: " + (e.message || e)); } }; const sectionLabel = (cs) => `${cs.label}${cs.academic_year ? ` · ${cs.academic_year}` : ""}`; const fmt = (iso) => new Date(iso).toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); return (
Author school-wide announcements that reach every approved parent. For class-specific notices, teachers can also write from the teacher app — both flow to the same parent feed.
{err && (