Maak navigatie voor structuurbeheer overzichtelijker

This commit is contained in:
bes-r 2026-06-15 21:55:39 +02:00
parent d16b3b0e3a
commit 10593e3cfd
2 changed files with 312 additions and 231 deletions

View file

@ -196,65 +196,7 @@ export function LocatiesPage() {
/* --------------------------------- Groepen -------------------------------- */ /* --------------------------------- Groepen -------------------------------- */
export function GroepenPage() { export function GroepenPage() {
const { items, error, loading, reload, setError } = useList("groepen"); return <LeerpleinPage />;
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 (
<section>
<h1>Groepen</h1>
<p className="muted">Klassen waar leerlingen bij horen.</p>
<Foutmelding msg={error} />
<form className="form-row" onSubmit={opslaan}>
<input placeholder="Naam" value={form.naam} required onChange={(e) => setForm({ ...form, naam: e.target.value })} />
<input placeholder="Omschrijving (optioneel)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} />
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
{editId && <button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>}
</form>
{loading ? <p>Laden</p> : (
<table className="grid">
<thead><tr><th>Naam</th><th>Omschrijving</th><th>Leerlingen</th><th></th></tr></thead>
<tbody>
{items.map((g) => (
<tr key={g.id}>
<td>{g.naam}</td>
<td className="muted">{g.omschrijving || "—"}</td>
<td>{g.aantal_leerlingen}</td>
<td className="row-actions">
<button className="link" onClick={() => { setEditId(g.id); setForm({ naam: g.naam, omschrijving: g.omschrijving }); }}>bewerken</button>
<button className="link danger" onClick={() => verwijder(g.id)}>verwijderen</button>
</td>
</tr>
))}
{items.length === 0 && <tr><td colSpan="4" className="muted">Nog geen groepen.</td></tr>}
</tbody>
</table>
)}
</section>
);
} }
/* -------------------------------- Personen -------------------------------- */ /* -------------------------------- Personen -------------------------------- */
@ -501,106 +443,7 @@ export function PersonenPage() {
/* ------------------------------- Subgroepen ------------------------------- */ /* ------------------------------- Subgroepen ------------------------------- */
export function SubgroepenPage() { export function SubgroepenPage() {
const { items, error, loading, reload, setError } = useList("subgroepen"); return <LeerpleinPage />;
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 (
<section>
<h1>Subgroepen</h1>
<p className="muted">Groepjes leerlingen binnen een groep (bv. niveaugroep rekenen), met eigen begeleiders.</p>
<Foutmelding msg={error} />
<form className="form-card" onSubmit={opslaan}>
<div className="form-row">
<input placeholder="Naam" value={form.naam} required onChange={(e) => setForm({ ...form, naam: e.target.value })} />
<input placeholder="Omschrijving (optioneel)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} />
<select value={form.groep || ""} onChange={(e) => setForm({ ...form, groep: e.target.value })}>
<option value=""> groep </option>
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam}</option>)}
</select>
</div>
<div className="leden-keuze">
<div className="muted small">Leden (leerlingen):</div>
<div className="chips">
{leerlingen.map((l) => (
<button type="button" key={l.id} className={form.leden.includes(l.id) ? "chip on" : "chip"} onClick={() => toggle("leden", l.id)}>{l.volledige_naam}</button>
))}
{leerlingen.length === 0 && <span className="muted small">Nog geen leerlingen ingevoerd.</span>}
</div>
</div>
<div className="leden-keuze">
<div className="muted small">Begeleiders:</div>
<div className="chips">
{medewerkers.map((m) => (
<button type="button" key={m.id} className={form.begeleiders.includes(m.id) ? "chip on" : "chip"} onClick={() => toggle("begeleiders", m.id)}>{m.volledige_naam}</button>
))}
{medewerkers.length === 0 && <span className="muted small">Nog geen medewerkers.</span>}
</div>
</div>
<div className="form-row">
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
{editId && <button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>}
</div>
</form>
{loading ? <p>Laden</p> : (
<table className="grid">
<thead><tr><th>Naam</th><th>Groep</th><th>Leden</th><th>Begeleiders</th><th></th></tr></thead>
<tbody>
{items.map((s) => (
<tr key={s.id}>
<td>{s.naam}{s.omschrijving && <div className="muted small">{s.omschrijving}</div>}</td>
<td className="muted">{s.groep_naam || "—"}</td>
<td><span className="badge">{s.aantal_leden}</span> <span className="muted small">{s.leden_namen.map((x) => x.naam).join(", ")}</span></td>
<td className="muted small">{s.begeleiders_namen.map((x) => x.naam).join(", ") || "—"}</td>
<td className="row-actions">
<button className="link" onClick={() => { 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</button>
<button className="link danger" onClick={() => verwijder(s.id)}>verwijderen</button>
</td>
</tr>
))}
{items.length === 0 && <tr><td colSpan="5" className="muted">Nog geen subgroepen.</td></tr>}
</tbody>
</table>
)}
</section>
);
} }
/* ------------------------------- Tijdsloten ------------------------------- */ /* ------------------------------- Tijdsloten ------------------------------- */
@ -856,107 +699,331 @@ export function AfwezigheidPage() {
} }
/* -------------------------------- Leerplein ------------------------------- */ /* -------------------------------- Leerplein ------------------------------- */
// Module-scherm: koppel subgroepen aan groepen en bundel groepen, subgroepen // Gezamenlijk structuurbeheer: leerplein -> groep -> subgroep.
// en losse leerlingen onder een overkoepelend leerplein.
export function LeerpleinPage() { export function LeerpleinPage() {
const { items, error, loading, reload, setError } = useList("leerpleinen"); const [leerpleinen, setLeerpleinen] = useState([]);
const [groepen, setGroepen] = useState([]); const [groepen, setGroepen] = useState([]);
const [subgroepen, setSubgroepen] = useState([]); const [subgroepen, setSubgroepen] = useState([]);
const [leerlingen, setLeerlingen] = useState([]); const [leerlingen, setLeerlingen] = useState([]);
const [medewerkers, setMedewerkers] = useState([]); const [medewerkers, setMedewerkers] = useState([]);
const leeg = { naam: "", omschrijving: "", groepen: [], subgroepen: [], leerlingen: [], medewerkers: [] }; const [loading, setLoading] = useState(true);
const [form, setForm] = useState(leeg); const [error, setError] = useState(null);
const [editId, setEditId] = useState(null);
useEffect(() => { const leegLeerplein = { naam: "", omschrijving: "", leerlingen: [], medewerkers: [] };
listResource("groepen").then(setGroepen).catch(() => {}); const leegGroep = { naam: "", omschrijving: "", leerplein: "", leerlingen: [] };
listResource("subgroepen").then(setSubgroepen).catch(() => {}); const leegSubgroep = { naam: "", omschrijving: "", groep: "", leerplein: "", ledenBron: "groep", leden: [], begeleiders: [] };
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {}); const [leerpleinForm, setLeerpleinForm] = useState(leegLeerplein);
listResource("personen").then((ps) => setMedewerkers(ps.filter((p) => p.rol !== "leerling"))).catch(() => {}); 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) { async function laadAlles() {
setForm((f) => f[veld].includes(id) ? { ...f, [veld]: f[veld].filter((x) => x !== id) } : { ...f, [veld]: [...f[veld], id] }); 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 (
<div className="check-grid">
{items.map((item) => (
<label className="check-item" key={item.id}>
<input type="checkbox" checked={geselecteerd.includes(item.id)} onChange={() => toggle(item.id)} />
<span>{item.volledige_naam || item.naam}</span>
</label>
))}
{items.length === 0 && <span className="muted small">{leeg}</span>}
</div>
);
}
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(); e.preventDefault();
try { try {
if (editId) await updateResource("leerpleinen", editId, form); const payload = { ...leerpleinForm };
else await createResource("leerpleinen", form); if (editLeerpleinId) await updateResource("leerpleinen", editLeerpleinId, payload);
setForm(leeg); setEditId(null); await reload(); else await createResource("leerpleinen", payload);
setLeerpleinForm(leegLeerplein);
setEditLeerpleinId(null);
await laadAlles();
} catch (err) { setError(err.message); } } 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; 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 ( return (
<section> <section>
<h1>Leerplein</h1> <h1>Groepenstructuur</h1>
<p className="muted">Bundel groepen, subgroepen en losse leerlingen onder een overkoepelend leerplein.</p> <p className="muted">Beheer leerpleinen, groepen en subgroepen op één plek. Een groep hoort altijd onder een leerplein; een subgroep hoort altijd onder een groep.</p>
<Foutmelding msg={error} /> <Foutmelding msg={error} />
<form className="form-card" onSubmit={opslaan}>
<form className="form-card structuur-form" onSubmit={opslaanLeerplein}>
<h2>Leerplein</h2>
<div className="form-row"> <div className="form-row">
<input placeholder="Naam" value={form.naam} required onChange={(e) => setForm({ ...form, naam: e.target.value })} /> <input placeholder="Naam leerplein" value={leerpleinForm.naam} required onChange={(e) => setLeerpleinForm({ ...leerpleinForm, naam: e.target.value })} />
<input placeholder="Omschrijving (optioneel)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} /> <input placeholder="Omschrijving (optioneel)" value={leerpleinForm.omschrijving} onChange={(e) => setLeerpleinForm({ ...leerpleinForm, omschrijving: e.target.value })} />
</div> </div>
<div className="leden-keuze"> <div className="leden-keuze">
<div className="muted small">Groepen:</div> <div className="muted small">Losse leerlingen op leerpleinniveau:</div>
<div className="chips"> <Vinkjes items={leerlingen} geselecteerd={leerpleinForm.leerlingen} toggle={(id) => setLeerpleinForm((f) => ({ ...f, leerlingen: toggleArray(f.leerlingen, id) }))} />
{groepen.map((g) => <button type="button" key={g.id} className={form.groepen.includes(g.id) ? "chip on" : "chip"} onClick={() => toggle("groepen", g.id)}>{g.naam}</button>)}
{groepen.length === 0 && <span className="muted small">Geen groepen.</span>}
</div>
</div>
<div className="leden-keuze">
<div className="muted small">Subgroepen:</div>
<div className="chips">
{subgroepen.map((s) => <button type="button" key={s.id} className={form.subgroepen.includes(s.id) ? "chip on" : "chip"} onClick={() => toggle("subgroepen", s.id)}>{s.naam}</button>)}
{subgroepen.length === 0 && <span className="muted small">Geen subgroepen.</span>}
</div>
</div>
<div className="leden-keuze">
<div className="muted small">Losse leerlingen:</div>
<div className="chips">
{leerlingen.map((l) => <button type="button" key={l.id} className={form.leerlingen.includes(l.id) ? "chip on" : "chip"} onClick={() => toggle("leerlingen", l.id)}>{l.volledige_naam}</button>)}
{leerlingen.length === 0 && <span className="muted small">Geen leerlingen.</span>}
</div>
</div> </div>
<div className="leden-keuze"> <div className="leden-keuze">
<div className="muted small">Medewerkers:</div> <div className="muted small">Medewerkers:</div>
<div className="chips"> <Vinkjes items={medewerkers} geselecteerd={leerpleinForm.medewerkers} toggle={(id) => setLeerpleinForm((f) => ({ ...f, medewerkers: toggleArray(f.medewerkers, id) }))} leeg="Geen medewerkers gevonden." />
{medewerkers.map((m) => <button type="button" key={m.id} className={form.medewerkers.includes(m.id) ? "chip on" : "chip"} onClick={() => toggle("medewerkers", m.id)}>{m.volledige_naam}</button>)}
{medewerkers.length === 0 && <span className="muted small">Geen medewerkers.</span>}
</div>
</div> </div>
<div className="form-row"> <div className="form-row">
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button> <button className="btn primary" type="submit">{editLeerpleinId ? "Leerplein opslaan" : "Leerplein toevoegen"}</button>
{editId && <button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>} {editLeerpleinId && <button type="button" className="btn" onClick={() => { setEditLeerpleinId(null); setLeerpleinForm(leegLeerplein); }}>Annuleren</button>}
</div> </div>
</form> </form>
<form className="form-card structuur-form" onSubmit={opslaanGroep}>
<h2>Groep</h2>
<div className="form-row">
<select value={groepForm.leerplein} required onChange={(e) => setGroepForm({ ...groepForm, leerplein: e.target.value })}>
<option value=""> bovenliggend leerplein </option>
{leerpleinen.map((lp) => <option key={lp.id} value={lp.id}>{lp.naam}</option>)}
</select>
<input placeholder="Naam groep" value={groepForm.naam} required onChange={(e) => setGroepForm({ ...groepForm, naam: e.target.value })} />
<input placeholder="Omschrijving (optioneel)" value={groepForm.omschrijving} onChange={(e) => setGroepForm({ ...groepForm, omschrijving: e.target.value })} />
</div>
<div className="leden-keuze">
<div className="lijst-kop">
<span className="muted small">Leerlingen op schoolniveau in deze groep:</span>
<span>
<button type="button" className="link small" onClick={() => setGroepForm((f) => ({ ...f, leerlingen: leerlingen.map((l) => l.id) }))}>alles</button>
<button type="button" className="link small" onClick={() => setGroepForm((f) => ({ ...f, leerlingen: [] }))}>geen</button>
</span>
</div>
<Vinkjes items={leerlingen} geselecteerd={groepForm.leerlingen} toggle={(id) => setGroepForm((f) => ({ ...f, leerlingen: toggleArray(f.leerlingen, id) }))} />
</div>
<div className="form-row">
<button className="btn primary" type="submit">{editGroepId ? "Groep opslaan" : "Groep toevoegen"}</button>
{editGroepId && <button type="button" className="btn" onClick={() => { setEditGroepId(null); setGroepForm(leegGroep); }}>Annuleren</button>}
</div>
</form>
<form className="form-card structuur-form" onSubmit={opslaanSubgroep}>
<h2>Subgroep</h2>
<div className="form-row">
<select value={subgroepForm.groep} required onChange={(e) => {
const groepId = e.target.value;
const lp = leerpleinVanGroep(groepId);
setSubgroepForm({ ...subgroepForm, groep: groepId, leerplein: subgroepForm.leerplein || lp?.id || "" });
}}>
<option value=""> groep </option>
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam} ({leerpleinVanGroep(g.id)?.naam || "geen leerplein"})</option>)}
</select>
<select value={subgroepForm.leerplein} onChange={(e) => setSubgroepForm({ ...subgroepForm, leerplein: e.target.value })}>
<option value=""> koppel ook aan leerplein </option>
{leerpleinen.map((lp) => <option key={lp.id} value={lp.id}>{lp.naam}</option>)}
</select>
<input placeholder="Naam subgroep" value={subgroepForm.naam} required onChange={(e) => setSubgroepForm({ ...subgroepForm, naam: e.target.value })} />
<input placeholder="Omschrijving (optioneel)" value={subgroepForm.omschrijving} onChange={(e) => setSubgroepForm({ ...subgroepForm, omschrijving: e.target.value })} />
</div>
<div className="form-row">
<label className="veld"><input type="radio" checked={subgroepForm.ledenBron === "groep"} onChange={() => setSubgroepForm({ ...subgroepForm, ledenBron: "groep" })} /> leerlingen uit groep</label>
<label className="veld"><input type="radio" checked={subgroepForm.ledenBron === "leerplein"} onChange={() => setSubgroepForm({ ...subgroepForm, ledenBron: "leerplein" })} /> leerlingen uit heel leerplein</label>
</div>
<div className="leden-keuze">
<div className="lijst-kop">
<span className="muted small">Leerlingen in subgroep:</span>
<span>
<button type="button" className="link small" onClick={() => setSubgroepForm((f) => ({ ...f, leden: subgroepLeerlingen.map((l) => l.id) }))}>alles</button>
<button type="button" className="link small" onClick={() => setSubgroepForm((f) => ({ ...f, leden: [] }))}>geen</button>
</span>
</div>
<Vinkjes items={subgroepLeerlingen} geselecteerd={subgroepForm.leden} toggle={(id) => setSubgroepForm((f) => ({ ...f, leden: toggleArray(f.leden, id) }))} />
</div>
<div className="leden-keuze">
<div className="muted small">Begeleiders:</div>
<Vinkjes items={medewerkers} geselecteerd={subgroepForm.begeleiders} toggle={(id) => setSubgroepForm((f) => ({ ...f, begeleiders: toggleArray(f.begeleiders, id) }))} leeg="Geen medewerkers gevonden." />
</div>
<div className="form-row">
<button className="btn primary" type="submit">{editSubgroepId ? "Subgroep opslaan" : "Subgroep toevoegen"}</button>
{editSubgroepId && <button type="button" className="btn" onClick={() => { setEditSubgroepId(null); setSubgroepForm(leegSubgroep); }}>Annuleren</button>}
</div>
</form>
{loading ? <p>Laden</p> : ( {loading ? <p>Laden</p> : (
<table className="grid"> <div className="structuur-overzicht">
<thead><tr><th>Naam</th><th>Inhoud</th><th></th></tr></thead> {leerpleinen.map((lp) => {
<tbody> const lpGroepen = groepen.filter((g) => (lp.groepen || []).includes(g.id));
{items.map((lp) => ( const lpSubgroepen = subgroepen.filter((s) => (lp.subgroepen || []).includes(s.id) || lpGroepen.some((g) => g.id === s.groep));
<tr key={lp.id}> return (
<td>{lp.naam}{lp.omschrijving && <div className="muted small">{lp.omschrijving}</div>}</td> <div className="structuur-card" key={lp.id}>
<td className="small"> <div className="structuur-head">
{lp.groepen_namen.length > 0 && <div>🗂 {lp.groepen_namen.map((x) => x.naam).join(", ")}</div>} <div><strong>{lp.naam}</strong>{lp.omschrijving && <div className="muted small">{lp.omschrijving}</div>}</div>
{lp.subgroepen_namen.length > 0 && <div>🔹 {lp.subgroepen_namen.map((x) => x.naam).join(", ")}</div>} <div className="row-actions"><button className="link" onClick={() => bewerkLeerplein(lp)}>bewerken</button><button className="link danger" onClick={() => verwijderLeerplein(lp.id)}>verwijderen</button></div>
{lp.leerlingen_namen.length > 0 && <div>👤 {lp.leerlingen_namen.map((x) => x.naam).join(", ")}</div>} </div>
{(lp.medewerkers_namen || []).length > 0 && <div>🧑🏫 {lp.medewerkers_namen.map((x) => x.naam).join(", ")}</div>} <div className="structuur-grid">
{lp.groepen_namen.length + lp.subgroepen_namen.length + lp.leerlingen_namen.length + (lp.medewerkers_namen || []).length === 0 && <span className="muted">leeg</span>} {lpGroepen.map((g) => {
</td> const groepLeerlingen = leerlingenVanGroep(g.id);
<td className="row-actions"> const groepSubs = lpSubgroepen.filter((s) => s.groep === g.id);
<button className="link" onClick={() => { 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</button> return (
<button className="link danger" onClick={() => verwijder(lp.id)}>verwijderen</button> <div className="structuur-groep" key={g.id}>
</td> <div className="structuur-head compact">
</tr> <div><strong>{g.naam}</strong><div className="muted small">{groepLeerlingen.length} leerling(en)</div></div>
<div className="row-actions"><button className="link" onClick={() => bewerkGroep(g)}>bewerken</button><button className="link danger" onClick={() => verwijderGroep(g.id)}>verwijderen</button></div>
</div>
<div className="muted small">{groepLeerlingen.map((l) => l.volledige_naam).join(", ") || "Geen leerlingen"}</div>
<div className="structuur-subgroepen">
{groepSubs.map((s) => (
<div className="structuur-subgroep" key={s.id}>
<div className="structuur-head compact">
<strong>{s.naam}</strong>
<div className="row-actions"><button className="link" onClick={() => bewerkSubgroep(s)}>bewerken</button><button className="link danger" onClick={() => verwijderSubgroep(s.id)}>verwijderen</button></div>
</div>
<div className="muted small">{s.aantal_leden} leerling(en): {s.leden_namen.map((x) => x.naam).join(", ") || "geen"}</div>
</div>
))} ))}
{items.length === 0 && <tr><td colSpan="3" className="muted">Nog geen leerpleinen.</td></tr>} {groepSubs.length === 0 && <div className="muted small">Geen subgroepen.</div>}
</tbody> </div>
</table> </div>
);
})}
{lpGroepen.length === 0 && <div className="muted">Nog geen groepen onder dit leerplein.</div>}
</div>
</div>
);
})}
{leerpleinen.length === 0 && <div className="muted">Nog geen leerpleinen.</div>}
{groepen.filter((g) => !leerpleinVanGroep(g.id)).length > 0 && (
<div className="banner error">Er zijn groepen zonder leerplein. Bewerk ze hieronder en kies een bovenliggend leerplein: {groepen.filter((g) => !leerpleinVanGroep(g.id)).map((g) => g.naam).join(", ")}</div>
)}
</div>
)} )}
</section> </section>
); );

View file

@ -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::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 { 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; } .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-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 { 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; } .modal-kop { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }