From 10593e3cfd5a75b5b87719de20ace7b264cdaf60 Mon Sep 17 00:00:00 2001
From: bes-r <27369668+bes-r@users.noreply.github.com>
Date: Mon, 15 Jun 2026 21:55:39 +0200
Subject: [PATCH] Maak navigatie voor structuurbeheer overzichtelijker
---
frontend/src/pages.jsx | 529 ++++++++++++++++++++++------------------
frontend/src/styles.css | 14 ++
2 files changed, 312 insertions(+), 231 deletions(-)
diff --git a/frontend/src/pages.jsx b/frontend/src/pages.jsx
index 9f0d98a..b6b256e 100644
--- a/frontend/src/pages.jsx
+++ b/frontend/src/pages.jsx
@@ -196,65 +196,7 @@ export function LocatiesPage() {
/* --------------------------------- Groepen -------------------------------- */
export function GroepenPage() {
- const { items, error, loading, reload, setError } = useList("groepen");
- const leeg = { naam: "", omschrijving: "" };
- const [form, setForm] = useState(leeg);
- const [editId, setEditId] = useState(null);
-
- async function opslaan(e) {
- e.preventDefault();
- try {
- if (editId) await updateResource("groepen", editId, form);
- else await createResource("groepen", form);
- setForm(leeg);
- setEditId(null);
- await reload();
- } catch (err) {
- setError(err.message);
- }
- }
- async function verwijder(id) {
- if (!confirm("Deze groep verwijderen?")) return;
- try {
- await deleteResource("groepen", id);
- await reload();
- } catch (err) {
- setError(err.message);
- }
- }
-
- return (
-
- Groepen
- Klassen waar leerlingen bij horen.
-
-
- {loading ? Laden…
: (
-
- Naam Omschrijving Leerlingen
-
- {items.map((g) => (
-
- {g.naam}
- {g.omschrijving || "—"}
- {g.aantal_leerlingen}
-
- { setEditId(g.id); setForm({ naam: g.naam, omschrijving: g.omschrijving }); }}>bewerken
- verwijder(g.id)}>verwijderen
-
-
- ))}
- {items.length === 0 && Nog geen groepen. }
-
-
- )}
-
- );
+ return ;
}
/* -------------------------------- Personen -------------------------------- */
@@ -501,106 +443,7 @@ export function PersonenPage() {
/* ------------------------------- Subgroepen ------------------------------- */
export function SubgroepenPage() {
- const { items, error, loading, reload, setError } = useList("subgroepen");
- const [leerlingen, setLeerlingen] = useState([]);
- const [groepen, setGroepen] = useState([]);
- const [medewerkers, setMedewerkers] = useState([]);
- const leeg = { naam: "", omschrijving: "", groep: "", leden: [], begeleiders: [] };
- const [form, setForm] = useState(leeg);
- const [editId, setEditId] = useState(null);
-
- useEffect(() => {
- listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
- listResource("groepen").then(setGroepen).catch(() => {});
- listResource("personen").then((ps) => setMedewerkers(ps.filter((p) => p.rol !== "leerling"))).catch(() => {});
- }, []);
-
- function toggle(veld, id) {
- setForm((f) => f[veld].includes(id) ? { ...f, [veld]: f[veld].filter((x) => x !== id) } : { ...f, [veld]: [...f[veld], id] });
- }
-
- async function opslaan(e) {
- e.preventDefault();
- const payload = { ...form, groep: form.groep || null };
- try {
- if (editId) await updateResource("subgroepen", editId, payload);
- else await createResource("subgroepen", payload);
- setForm(leeg);
- setEditId(null);
- await reload();
- } catch (err) {
- setError(err.message);
- }
- }
- async function verwijder(id) {
- if (!confirm("Deze subgroep verwijderen?")) return;
- try {
- await deleteResource("subgroepen", id);
- await reload();
- } catch (err) {
- setError(err.message);
- }
- }
-
- return (
-
- Subgroepen
- Groepjes leerlingen binnen een groep (bv. niveaugroep rekenen), met eigen begeleiders.
-
-
- {loading ? Laden…
: (
-
- Naam Groep Leden Begeleiders
-
- {items.map((s) => (
-
- {s.naam}{s.omschrijving && {s.omschrijving}
}
- {s.groep_naam || "—"}
- {s.aantal_leden} {s.leden_namen.map((x) => x.naam).join(", ")}
- {s.begeleiders_namen.map((x) => x.naam).join(", ") || "—"}
-
- { setEditId(s.id); setForm({ naam: s.naam, omschrijving: s.omschrijving, groep: s.groep || "", leden: s.leden_namen.map((x) => x.id), begeleiders: s.begeleiders_namen.map((x) => x.id) }); }}>bewerken
- verwijder(s.id)}>verwijderen
-
-
- ))}
- {items.length === 0 && Nog geen subgroepen. }
-
-
- )}
-
- );
+ return ;
}
/* ------------------------------- Tijdsloten ------------------------------- */
@@ -856,107 +699,331 @@ export function AfwezigheidPage() {
}
/* -------------------------------- Leerplein ------------------------------- */
-// Module-scherm: koppel subgroepen aan groepen en bundel groepen, subgroepen
-// en losse leerlingen onder een overkoepelend leerplein.
+// Gezamenlijk structuurbeheer: leerplein -> groep -> subgroep.
export function LeerpleinPage() {
- const { items, error, loading, reload, setError } = useList("leerpleinen");
+ const [leerpleinen, setLeerpleinen] = useState([]);
const [groepen, setGroepen] = useState([]);
const [subgroepen, setSubgroepen] = useState([]);
const [leerlingen, setLeerlingen] = useState([]);
const [medewerkers, setMedewerkers] = useState([]);
- const leeg = { naam: "", omschrijving: "", groepen: [], subgroepen: [], leerlingen: [], medewerkers: [] };
- const [form, setForm] = useState(leeg);
- const [editId, setEditId] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
- useEffect(() => {
- listResource("groepen").then(setGroepen).catch(() => {});
- listResource("subgroepen").then(setSubgroepen).catch(() => {});
- listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
- listResource("personen").then((ps) => setMedewerkers(ps.filter((p) => p.rol !== "leerling"))).catch(() => {});
- }, []);
+ const leegLeerplein = { naam: "", omschrijving: "", leerlingen: [], medewerkers: [] };
+ const leegGroep = { naam: "", omschrijving: "", leerplein: "", leerlingen: [] };
+ const leegSubgroep = { naam: "", omschrijving: "", groep: "", leerplein: "", ledenBron: "groep", leden: [], begeleiders: [] };
+ const [leerpleinForm, setLeerpleinForm] = useState(leegLeerplein);
+ const [groepForm, setGroepForm] = useState(leegGroep);
+ const [subgroepForm, setSubgroepForm] = useState(leegSubgroep);
+ const [editLeerpleinId, setEditLeerpleinId] = useState(null);
+ const [editGroepId, setEditGroepId] = useState(null);
+ const [editSubgroepId, setEditSubgroepId] = useState(null);
- function toggle(veld, id) {
- setForm((f) => f[veld].includes(id) ? { ...f, [veld]: f[veld].filter((x) => x !== id) } : { ...f, [veld]: [...f[veld], id] });
+ async function laadAlles() {
+ setLoading(true);
+ try {
+ const [lp, gr, sg, ll, ps] = await Promise.all([
+ listResource("leerpleinen"),
+ listResource("groepen"),
+ listResource("subgroepen"),
+ listResource("personen", "?rol=leerling"),
+ listResource("personen"),
+ ]);
+ setLeerpleinen(lp);
+ setGroepen(gr);
+ setSubgroepen(sg);
+ setLeerlingen(ll);
+ setMedewerkers(ps.filter((p) => p.rol !== "leerling"));
+ setError(null);
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
}
- async function opslaan(e) {
+ useEffect(() => { laadAlles(); }, []);
+
+ const toggleArray = (lijst, id) => lijst.includes(id) ? lijst.filter((x) => x !== id) : [...lijst, id];
+ const groepNaam = (id) => groepen.find((g) => g.id === id)?.naam || "—";
+ const leerpleinNaam = (id) => leerpleinen.find((lp) => lp.id === id)?.naam || "—";
+ const leerpleinVanGroep = (groepId) => leerpleinen.find((lp) => (lp.groepen || []).includes(Number(groepId)));
+ const leerpleinVanSubgroep = (subgroepId) => leerpleinen.find((lp) => (lp.subgroepen || []).includes(Number(subgroepId)));
+ const leerlingenVanGroep = (groepId) => leerlingen.filter((l) => l.groep === Number(groepId));
+ function leerlingenVanLeerplein(leerpleinId) {
+ const lp = leerpleinen.find((x) => x.id === Number(leerpleinId));
+ if (!lp) return [];
+ const ids = new Set(lp.leerlingen || []);
+ leerlingen.forEach((l) => { if ((lp.groepen || []).includes(l.groep)) ids.add(l.id); });
+ (lp.subgroepen || []).forEach((sid) => {
+ const s = subgroepen.find((x) => x.id === sid);
+ (s?.leden_namen || []).forEach((lid) => ids.add(lid.id));
+ });
+ return leerlingen.filter((l) => ids.has(l.id));
+ }
+ const subgroepLeerlingen = subgroepForm.ledenBron === "leerplein"
+ ? leerlingenVanLeerplein(subgroepForm.leerplein)
+ : leerlingenVanGroep(subgroepForm.groep);
+
+ function Vinkjes({ items, geselecteerd, toggle, leeg = "Geen leerlingen gevonden." }) {
+ return (
+
+ {items.map((item) => (
+
+ toggle(item.id)} />
+ {item.volledige_naam || item.naam}
+
+ ))}
+ {items.length === 0 && {leeg} }
+
+ );
+ }
+
+ async function syncGroepLeerplein(groepId, leerpleinId) {
+ const gid = Number(groepId);
+ const lpid = Number(leerpleinId);
+ for (const lp of leerpleinen) {
+ const heeft = (lp.groepen || []).includes(gid);
+ const moet = lp.id === lpid;
+ if (moet && !heeft) await updateResource("leerpleinen", lp.id, { groepen: [...(lp.groepen || []), gid] });
+ if (!moet && heeft) await updateResource("leerpleinen", lp.id, { groepen: (lp.groepen || []).filter((id) => id !== gid) });
+ }
+ }
+ async function syncSubgroepLeerplein(subgroepId, leerpleinId) {
+ const sid = Number(subgroepId);
+ const lpid = leerpleinId ? Number(leerpleinId) : null;
+ for (const lp of leerpleinen) {
+ const heeft = (lp.subgroepen || []).includes(sid);
+ const moet = lp.id === lpid;
+ if (moet && !heeft) await updateResource("leerpleinen", lp.id, { subgroepen: [...(lp.subgroepen || []), sid] });
+ if (!moet && heeft) await updateResource("leerpleinen", lp.id, { subgroepen: (lp.subgroepen || []).filter((id) => id !== sid) });
+ }
+ }
+ async function syncGroepLeerlingen(groepId, gekozenIds) {
+ const gid = Number(groepId);
+ for (const l of leerlingen) {
+ const gekozen = gekozenIds.includes(l.id);
+ if (gekozen && l.groep !== gid) await updateResource("personen", l.id, { groep: gid });
+ if (!gekozen && l.groep === gid) await updateResource("personen", l.id, { groep: null });
+ }
+ }
+
+ async function opslaanLeerplein(e) {
e.preventDefault();
try {
- if (editId) await updateResource("leerpleinen", editId, form);
- else await createResource("leerpleinen", form);
- setForm(leeg); setEditId(null); await reload();
+ const payload = { ...leerpleinForm };
+ if (editLeerpleinId) await updateResource("leerpleinen", editLeerpleinId, payload);
+ else await createResource("leerpleinen", payload);
+ setLeerpleinForm(leegLeerplein);
+ setEditLeerpleinId(null);
+ await laadAlles();
} catch (err) { setError(err.message); }
}
- async function verwijder(id) {
+ async function opslaanGroep(e) {
+ e.preventDefault();
+ if (!groepForm.leerplein) { setError("Kies een bovenliggend leerplein voor deze groep."); return; }
+ try {
+ const payload = { naam: groepForm.naam, omschrijving: groepForm.omschrijving };
+ const opgeslagen = editGroepId ? await updateResource("groepen", editGroepId, payload) : await createResource("groepen", payload);
+ await syncGroepLeerplein(opgeslagen.id, groepForm.leerplein);
+ await syncGroepLeerlingen(opgeslagen.id, groepForm.leerlingen);
+ setGroepForm(leegGroep);
+ setEditGroepId(null);
+ await laadAlles();
+ } catch (err) { setError(err.message); }
+ }
+ async function opslaanSubgroep(e) {
+ e.preventDefault();
+ if (!subgroepForm.groep) { setError("Kies een groep waar deze subgroep onder valt."); return; }
+ if (subgroepForm.ledenBron === "leerplein" && !subgroepForm.leerplein) { setError("Kies een leerplein voor leden uit het hele leerplein."); return; }
+ try {
+ const payload = {
+ naam: subgroepForm.naam,
+ omschrijving: subgroepForm.omschrijving,
+ groep: subgroepForm.groep,
+ leden: subgroepForm.leden,
+ begeleiders: subgroepForm.begeleiders,
+ };
+ const opgeslagen = editSubgroepId ? await updateResource("subgroepen", editSubgroepId, payload) : await createResource("subgroepen", payload);
+ await syncSubgroepLeerplein(opgeslagen.id, subgroepForm.leerplein);
+ setSubgroepForm(leegSubgroep);
+ setEditSubgroepId(null);
+ await laadAlles();
+ } catch (err) { setError(err.message); }
+ }
+ async function verwijderLeerplein(id) {
+ const lp = leerpleinen.find((x) => x.id === id);
+ if ((lp?.groepen || []).length > 0) { setError("Verplaats of verwijder eerst de groepen onder dit leerplein."); return; }
if (!confirm("Dit leerplein verwijderen?")) return;
- try { await deleteResource("leerpleinen", id); await reload(); } catch (err) { setError(err.message); }
+ try { await deleteResource("leerpleinen", id); await laadAlles(); } catch (err) { setError(err.message); }
+ }
+ async function verwijderGroep(id) {
+ if (!confirm("Deze groep verwijderen?")) return;
+ try { await deleteResource("groepen", id); await laadAlles(); } catch (err) { setError(err.message); }
+ }
+ async function verwijderSubgroep(id) {
+ if (!confirm("Deze subgroep verwijderen?")) return;
+ try { await syncSubgroepLeerplein(id, null); await deleteResource("subgroepen", id); await laadAlles(); } catch (err) { setError(err.message); }
+ }
+ function bewerkLeerplein(lp) {
+ setEditLeerpleinId(lp.id);
+ setLeerpleinForm({ naam: lp.naam, omschrijving: lp.omschrijving || "", leerlingen: lp.leerlingen || [], medewerkers: lp.medewerkers || [] });
+ }
+ function bewerkGroep(g) {
+ const lp = leerpleinVanGroep(g.id);
+ setEditGroepId(g.id);
+ setGroepForm({ naam: g.naam, omschrijving: g.omschrijving || "", leerplein: lp?.id || "", leerlingen: leerlingen.filter((l) => l.groep === g.id).map((l) => l.id) });
+ }
+ function bewerkSubgroep(s) {
+ const lp = leerpleinVanSubgroep(s.id) || leerpleinVanGroep(s.groep);
+ setEditSubgroepId(s.id);
+ setSubgroepForm({
+ naam: s.naam, omschrijving: s.omschrijving || "", groep: s.groep || "",
+ leerplein: lp?.id || "", ledenBron: lp ? "leerplein" : "groep",
+ leden: s.leden_namen.map((x) => x.id), begeleiders: s.begeleiders_namen.map((x) => x.id),
+ });
}
return (
- Leerplein
- Bundel groepen, subgroepen en losse leerlingen onder een overkoepelend leerplein.
+ Groepenstructuur
+ Beheer leerpleinen, groepen en subgroepen op één plek. Een groep hoort altijd onder een leerplein; een subgroep hoort altijd onder een groep.
-
+
+
+
+
+
{loading ? Laden…
: (
-
- Naam Inhoud
-
- {items.map((lp) => (
-
- {lp.naam}{lp.omschrijving && {lp.omschrijving}
}
-
- {lp.groepen_namen.length > 0 && 🗂️ {lp.groepen_namen.map((x) => x.naam).join(", ")}
}
- {lp.subgroepen_namen.length > 0 && 🔹 {lp.subgroepen_namen.map((x) => x.naam).join(", ")}
}
- {lp.leerlingen_namen.length > 0 && 👤 {lp.leerlingen_namen.map((x) => x.naam).join(", ")}
}
- {(lp.medewerkers_namen || []).length > 0 && 🧑🏫 {lp.medewerkers_namen.map((x) => x.naam).join(", ")}
}
- {lp.groepen_namen.length + lp.subgroepen_namen.length + lp.leerlingen_namen.length + (lp.medewerkers_namen || []).length === 0 && leeg }
-
-
- { setEditId(lp.id); setForm({ naam: lp.naam, omschrijving: lp.omschrijving, groepen: lp.groepen_namen.map((x) => x.id), subgroepen: lp.subgroepen_namen.map((x) => x.id), leerlingen: lp.leerlingen_namen.map((x) => x.id), medewerkers: (lp.medewerkers_namen || []).map((x) => x.id) }); }}>bewerken
- verwijder(lp.id)}>verwijderen
-
-
- ))}
- {items.length === 0 && Nog geen leerpleinen. }
-
-
+
+ {leerpleinen.map((lp) => {
+ const lpGroepen = groepen.filter((g) => (lp.groepen || []).includes(g.id));
+ const lpSubgroepen = subgroepen.filter((s) => (lp.subgroepen || []).includes(s.id) || lpGroepen.some((g) => g.id === s.groep));
+ return (
+
+
+
{lp.naam} {lp.omschrijving &&
{lp.omschrijving}
}
+
bewerkLeerplein(lp)}>bewerken verwijderLeerplein(lp.id)}>verwijderen
+
+
+ {lpGroepen.map((g) => {
+ const groepLeerlingen = leerlingenVanGroep(g.id);
+ const groepSubs = lpSubgroepen.filter((s) => s.groep === g.id);
+ return (
+
+
+
{g.naam} {groepLeerlingen.length} leerling(en)
+
bewerkGroep(g)}>bewerken verwijderGroep(g.id)}>verwijderen
+
+
{groepLeerlingen.map((l) => l.volledige_naam).join(", ") || "Geen leerlingen"}
+
+ {groepSubs.map((s) => (
+
+
+
{s.naam}
+
bewerkSubgroep(s)}>bewerken verwijderSubgroep(s.id)}>verwijderen
+
+
{s.aantal_leden} leerling(en): {s.leden_namen.map((x) => x.naam).join(", ") || "geen"}
+
+ ))}
+ {groepSubs.length === 0 &&
Geen subgroepen.
}
+
+
+ );
+ })}
+ {lpGroepen.length === 0 &&
Nog geen groepen onder dit leerplein.
}
+
+
+ );
+ })}
+ {leerpleinen.length === 0 &&
Nog geen leerpleinen.
}
+ {groepen.filter((g) => !leerpleinVanGroep(g.id)).length > 0 && (
+
Er zijn groepen zonder leerplein. Bewerk ze hieronder en kies een bovenliggend leerplein: {groepen.filter((g) => !leerpleinVanGroep(g.id)).map((g) => g.naam).join(", ")}
+ )}
+
)}
);
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index 29d2360..2f32dbd 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -207,6 +207,20 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
.split-preview-children::before { content: ""; position: absolute; top: 0; left: 12px; right: 12px; border-top: 2px solid #bfdbfe; }
.split-preview-children span { position: relative; padding: 5px 8px; border-radius: 6px; background: var(--brand-soft); color: var(--brand); font-size: 12px; font-weight: 700; }
.split-preview-children span::before { content: ""; position: absolute; top: -13px; left: 50%; width: 2px; height: 13px; background: #bfdbfe; }
+.structuur-form h2 { margin: 0 0 10px; font-size: 17px; }
+.check-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 7px; margin-top: 8px; }
+.check-item { display: flex; align-items: center; gap: 7px; min-width: 0; border: 1px solid var(--line); border-radius: 7px; background: #fff; padding: 7px 9px; font-size: 13px; }
+.check-item input { min-width: auto; }
+.check-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.lijst-kop { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
+.structuur-overzicht { display: grid; gap: 14px; margin-top: 18px; }
+.structuur-card { border: 1px solid #bfdbfe; border-radius: 8px; background: #f8fbff; padding: 12px; }
+.structuur-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
+.structuur-head.compact { align-items: center; margin-bottom: 6px; }
+.structuur-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }
+.structuur-groep { border: 1px solid var(--line); border-top: 3px solid var(--brand); border-radius: 8px; background: #fff; padding: 10px; }
+.structuur-subgroepen { display: grid; gap: 7px; margin-top: 9px; padding-top: 9px; border-top: 1px dashed var(--line); }
+.structuur-subgroep { border-left: 3px solid #93c5fd; border-radius: 6px; background: #f9fafb; padding: 7px 9px; }
.modal-overlay { position: fixed; inset: 0; background: rgba(15,23,42,.45); display: flex; align-items: flex-start; justify-content: center; padding: 40px 16px; z-index: 900; overflow: auto; }
.modal { background: var(--panel); border-radius: 12px; padding: 18px 20px; width: min(560px, 100%); box-shadow: 0 20px 50px rgba(0,0,0,.3); }
.modal-kop { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }