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.

- -
- setForm({ ...form, naam: e.target.value })} /> - setForm({ ...form, omschrijving: e.target.value })} /> - - {editId && } -
- {loading ?

Laden…

: ( - - - - {items.map((g) => ( - - - - - - - ))} - {items.length === 0 && } - -
NaamOmschrijvingLeerlingen
{g.naam}{g.omschrijving || "—"}{g.aantal_leerlingen} - - -
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.

- -
-
- setForm({ ...form, naam: e.target.value })} /> - setForm({ ...form, omschrijving: e.target.value })} /> - -
-
-
Leden (leerlingen):
-
- {leerlingen.map((l) => ( - - ))} - {leerlingen.length === 0 && Nog geen leerlingen ingevoerd.} -
-
-
-
Begeleiders:
-
- {medewerkers.map((m) => ( - - ))} - {medewerkers.length === 0 && Nog geen medewerkers.} -
-
-
- - {editId && } -
-
- {loading ?

Laden…

: ( - - - - {items.map((s) => ( - - - - - - - - ))} - {items.length === 0 && } - -
NaamGroepLedenBegeleiders
{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(", ") || "—"} - - -
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) => ( + + ))} + {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.

-
+ + +

Leerplein

- setForm({ ...form, naam: e.target.value })} /> - setForm({ ...form, omschrijving: e.target.value })} /> + setLeerpleinForm({ ...leerpleinForm, naam: e.target.value })} /> + setLeerpleinForm({ ...leerpleinForm, omschrijving: e.target.value })} />
-
Groepen:
-
- {groepen.map((g) => )} - {groepen.length === 0 && Geen groepen.} -
-
-
-
Subgroepen:
-
- {subgroepen.map((s) => )} - {subgroepen.length === 0 && Geen subgroepen.} -
-
-
-
Losse leerlingen:
-
- {leerlingen.map((l) => )} - {leerlingen.length === 0 && Geen leerlingen.} -
+
Losse leerlingen op leerpleinniveau:
+ setLeerpleinForm((f) => ({ ...f, leerlingen: toggleArray(f.leerlingen, id) }))} />
Medewerkers:
-
- {medewerkers.map((m) => )} - {medewerkers.length === 0 && Geen medewerkers.} -
+ setLeerpleinForm((f) => ({ ...f, medewerkers: toggleArray(f.medewerkers, id) }))} leeg="Geen medewerkers gevonden." />
- - {editId && } + + {editLeerpleinId && }
+ +
+

Groep

+
+ + setGroepForm({ ...groepForm, naam: e.target.value })} /> + setGroepForm({ ...groepForm, omschrijving: e.target.value })} /> +
+
+
+ Leerlingen op schoolniveau in deze groep: + + + + +
+ setGroepForm((f) => ({ ...f, leerlingen: toggleArray(f.leerlingen, id) }))} /> +
+
+ + {editGroepId && } +
+
+ +
+

Subgroep

+
+ + + setSubgroepForm({ ...subgroepForm, naam: e.target.value })} /> + setSubgroepForm({ ...subgroepForm, omschrijving: e.target.value })} /> +
+
+ + +
+
+
+ Leerlingen in subgroep: + + + + +
+ setSubgroepForm((f) => ({ ...f, leden: toggleArray(f.leden, id) }))} /> +
+
+
Begeleiders:
+ setSubgroepForm((f) => ({ ...f, begeleiders: toggleArray(f.begeleiders, id) }))} leeg="Geen medewerkers gevonden." /> +
+
+ + {editSubgroepId && } +
+
+ {loading ?

Laden…

: ( - - - - {items.map((lp) => ( - - - - - - ))} - {items.length === 0 && } - -
NaamInhoud
{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} -
- - -
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}
}
+
+
+
+ {lpGroepen.map((g) => { + const groepLeerlingen = leerlingenVanGroep(g.id); + const groepSubs = lpSubgroepen.filter((s) => s.groep === g.id); + return ( +
+
+
{g.naam}
{groepLeerlingen.length} leerling(en)
+
+
+
{groepLeerlingen.map((l) => l.volledige_naam).join(", ") || "Geen leerlingen"}
+
+ {groepSubs.map((s) => ( +
+
+ {s.naam} +
+
+
{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; }