Maak navigatie voor structuurbeheer overzichtelijker
This commit is contained in:
parent
d16b3b0e3a
commit
10593e3cfd
2 changed files with 312 additions and 231 deletions
|
|
@ -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 (
|
||||
<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>
|
||||
);
|
||||
return <LeerpleinPage />;
|
||||
}
|
||||
|
||||
/* -------------------------------- 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 (
|
||||
<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>
|
||||
);
|
||||
return <LeerpleinPage />;
|
||||
}
|
||||
|
||||
/* ------------------------------- 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 (
|
||||
<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();
|
||||
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 (
|
||||
<section>
|
||||
<h1>Leerplein</h1>
|
||||
<p className="muted">Bundel groepen, subgroepen en losse leerlingen onder een overkoepelend leerplein.</p>
|
||||
<h1>Groepenstructuur</h1>
|
||||
<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} />
|
||||
<form className="form-card" onSubmit={opslaan}>
|
||||
|
||||
<form className="form-card structuur-form" onSubmit={opslaanLeerplein}>
|
||||
<h2>Leerplein</h2>
|
||||
<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 })} />
|
||||
<input placeholder="Naam leerplein" value={leerpleinForm.naam} required onChange={(e) => setLeerpleinForm({ ...leerpleinForm, naam: e.target.value })} />
|
||||
<input placeholder="Omschrijving (optioneel)" value={leerpleinForm.omschrijving} onChange={(e) => setLeerpleinForm({ ...leerpleinForm, omschrijving: e.target.value })} />
|
||||
</div>
|
||||
<div className="leden-keuze">
|
||||
<div className="muted small">Groepen:</div>
|
||||
<div className="chips">
|
||||
{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 className="muted small">Losse leerlingen op leerpleinniveau:</div>
|
||||
<Vinkjes items={leerlingen} geselecteerd={leerpleinForm.leerlingen} toggle={(id) => setLeerpleinForm((f) => ({ ...f, leerlingen: toggleArray(f.leerlingen, id) }))} />
|
||||
</div>
|
||||
<div className="leden-keuze">
|
||||
<div className="muted small">Medewerkers:</div>
|
||||
<div className="chips">
|
||||
{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>
|
||||
<Vinkjes items={medewerkers} geselecteerd={leerpleinForm.medewerkers} toggle={(id) => setLeerpleinForm((f) => ({ ...f, medewerkers: toggleArray(f.medewerkers, id) }))} leeg="Geen medewerkers gevonden." />
|
||||
</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>}
|
||||
<button className="btn primary" type="submit">{editLeerpleinId ? "Leerplein opslaan" : "Leerplein toevoegen"}</button>
|
||||
{editLeerpleinId && <button type="button" className="btn" onClick={() => { setEditLeerpleinId(null); setLeerpleinForm(leegLeerplein); }}>Annuleren</button>}
|
||||
</div>
|
||||
</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> : (
|
||||
<table className="grid">
|
||||
<thead><tr><th>Naam</th><th>Inhoud</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{items.map((lp) => (
|
||||
<tr key={lp.id}>
|
||||
<td>{lp.naam}{lp.omschrijving && <div className="muted small">{lp.omschrijving}</div>}</td>
|
||||
<td className="small">
|
||||
{lp.groepen_namen.length > 0 && <div>🗂️ {lp.groepen_namen.map((x) => x.naam).join(", ")}</div>}
|
||||
{lp.subgroepen_namen.length > 0 && <div>🔹 {lp.subgroepen_namen.map((x) => x.naam).join(", ")}</div>}
|
||||
{lp.leerlingen_namen.length > 0 && <div>👤 {lp.leerlingen_namen.map((x) => x.naam).join(", ")}</div>}
|
||||
{(lp.medewerkers_namen || []).length > 0 && <div>🧑🏫 {lp.medewerkers_namen.map((x) => x.naam).join(", ")}</div>}
|
||||
{lp.groepen_namen.length + lp.subgroepen_namen.length + lp.leerlingen_namen.length + (lp.medewerkers_namen || []).length === 0 && <span className="muted">leeg</span>}
|
||||
</td>
|
||||
<td className="row-actions">
|
||||
<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>
|
||||
<button className="link danger" onClick={() => verwijder(lp.id)}>verwijderen</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{items.length === 0 && <tr><td colSpan="3" className="muted">Nog geen leerpleinen.</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="structuur-overzicht">
|
||||
{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 (
|
||||
<div className="structuur-card" key={lp.id}>
|
||||
<div className="structuur-head">
|
||||
<div><strong>{lp.naam}</strong>{lp.omschrijving && <div className="muted small">{lp.omschrijving}</div>}</div>
|
||||
<div className="row-actions"><button className="link" onClick={() => bewerkLeerplein(lp)}>bewerken</button><button className="link danger" onClick={() => verwijderLeerplein(lp.id)}>verwijderen</button></div>
|
||||
</div>
|
||||
<div className="structuur-grid">
|
||||
{lpGroepen.map((g) => {
|
||||
const groepLeerlingen = leerlingenVanGroep(g.id);
|
||||
const groepSubs = lpSubgroepen.filter((s) => s.groep === g.id);
|
||||
return (
|
||||
<div className="structuur-groep" key={g.id}>
|
||||
<div className="structuur-head compact">
|
||||
<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>
|
||||
))}
|
||||
{groepSubs.length === 0 && <div className="muted small">Geen subgroepen.</div>}
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue