diff --git a/ROADMAP.md b/ROADMAP.md index 48b046b..67cfc0b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -62,6 +62,13 @@ volgt. - [x] Interne stages gebruiken nu een **lokaal** (de aparte stagelocatie-lijst is vervallen) en tellen mee in de dubbele-boekingcheck. +## Groepsindeling + +- [x] **Eén sleep-overzicht**: bovenliggende groep → groep → subgroep genest, met + een zijbalk "nog niet ingedeeld". Personen naar elk niveau slepen; × haalt + ze weer weg. Leerlingen licht gekleurd, personeel donker; duidelijke + gekleurde structuur per niveau. Inline aanmaken/hernoemen. + ## Koppelingen & inzetregels - [x] **Personeel koppelen aan leerplein en subgroep** (personeelsformulier: diff --git a/frontend/src/pages.jsx b/frontend/src/pages.jsx index 458cf06..af9a0fc 100644 --- a/frontend/src/pages.jsx +++ b/frontend/src/pages.jsx @@ -804,6 +804,21 @@ export function AfwezigheidPage() { /* ----------------------------- Groepsindeling ----------------------------- */ // Gezamenlijk beheer: bovenliggende groep -> groep -> subgroep. +const SROL = { leerkracht: "LK", praktijkondersteuner: "PO", klassenondersteuner: "KO" }; + +function InlineAdd({ placeholder, onAdd, klein }) { + const [v, setV] = useState(""); + return ( +
+ ); +} + export function GroepsindelingPage() { const [leerpleinen, setLeerpleinen] = useState([]); const [groepen, setGroepen] = useState([]); @@ -813,15 +828,9 @@ export function GroepsindelingPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - 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); + const dragRef = useRef(null); + const [ghost, setGhost] = useState(null); + const [hoverZone, setHoverZone] = useState(null); async function laadAlles() { setLoading(true); @@ -839,316 +848,271 @@ export function GroepsindelingPage() { setLeerlingen(ll); setMedewerkers(ps.filter((p) => p.rol !== "leerling")); setError(null); - } catch (err) { - setError(err.message); - } finally { - setLoading(false); - } + } catch (err) { setError(err.message); } finally { setLoading(false); } } 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 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); + const leerlingById = (id) => leerlingen.find((l) => l.id === id); + const medewerkerById = (id) => medewerkers.find((m) => m.id === id); + const groepLeerlingen = (gid) => leerlingen.filter((l) => l.groep === gid); + const groepMedewerkers = (gid) => medewerkers.filter((m) => m.stamgroep === gid); + const groepSubs = (gid) => subgroepen.filter((s) => s.groep === gid); + const lpGroepen = (lp) => groepen.filter((g) => (lp.groepen || []).includes(g.id)); + const groepHeeftLeerplein = (gid) => leerpleinen.some((lp) => (lp.groepen || []).includes(gid)); - function Vinkjes({ items, geselecteerd, toggle, leeg = "Geen leerlingen gevonden." }) { + // "Nog niet ingedeeld": personen die nergens voorkomen. + const ingedeeldeLeerlingen = (() => { + const s = new Set(); + leerlingen.forEach((l) => { if (l.groep) s.add(l.id); }); + leerpleinen.forEach((lp) => (lp.leerlingen || []).forEach((id) => s.add(id))); + subgroepen.forEach((sg) => (sg.leden_namen || []).forEach((x) => s.add(x.id))); + return s; + })(); + const ingedeeldeMedewerkers = (() => { + const s = new Set(); + medewerkers.forEach((m) => { if (m.stamgroep) s.add(m.id); }); + leerpleinen.forEach((lp) => (lp.medewerkers || []).forEach((id) => s.add(id))); + subgroepen.forEach((sg) => (sg.begeleiders_namen || []).forEach((x) => s.add(x.id))); + return s; + })(); + const losseLeerlingen = leerlingen.filter((l) => !ingedeeldeLeerlingen.has(l.id)); + const losseMedewerkers = medewerkers.filter((m) => !ingedeeldeMedewerkers.has(m.id)); + + function startDrag(e, payload) { + e.preventDefault(); + dragRef.current = payload; + setGhost({ x: e.clientX, y: e.clientY, label: payload.naam }); + } + useEffect(() => { + function move(e) { + if (!dragRef.current) return; + setGhost({ x: e.clientX, y: e.clientY, label: dragRef.current.naam }); + const el = document.elementFromPoint(e.clientX, e.clientY); + const z = el && el.closest("[data-drop-kind]"); + setHoverZone(z ? `${z.dataset.dropKind}:${z.dataset.dropId}` : null); + } + async function up(e) { + if (!dragRef.current) return; + const el = document.elementFromPoint(e.clientX, e.clientY); + const z = el && el.closest("[data-drop-kind]"); + const payload = dragRef.current; + dragRef.current = null; + setGhost(null); + setHoverZone(null); + if (z) await drop({ kind: z.dataset.dropKind, id: Number(z.dataset.dropId) }, payload); + } + window.addEventListener("pointermove", move); + window.addEventListener("pointerup", up); + return () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); }; + }, [leerpleinen, groepen, subgroepen, leerlingen, medewerkers]); + + async function drop(zone, p) { + const { kind, id } = zone; + try { + if (kind === "groep") { + await updateResource("personen", p.id, p.rol === "leerling" ? { groep: id } : { stamgroep: id }); + } else if (kind === "leerplein") { + const lp = leerpleinen.find((x) => x.id === id); if (!lp) return; + if (p.rol === "leerling") { + if (!(lp.leerlingen || []).includes(p.id)) await updateResource("leerpleinen", id, { leerlingen: [...(lp.leerlingen || []), p.id] }); + } else if (!(lp.medewerkers || []).includes(p.id)) { + await updateResource("leerpleinen", id, { medewerkers: [...(lp.medewerkers || []), p.id] }); + } + } else if (kind === "subgroep") { + const s = subgroepen.find((x) => x.id === id); if (!s) return; + if (p.rol === "leerling") { + const ids = s.leden_namen.map((x) => x.id); + if (!ids.includes(p.id)) await updateResource("subgroepen", id, { leden: [...ids, p.id] }); + } else { + const ids = s.begeleiders_namen.map((x) => x.id); + if (!ids.includes(p.id)) await updateResource("subgroepen", id, { begeleiders: [...ids, p.id] }); + } + } + await laadAlles(); + } catch (e) { setError(e.message); } + } + async function haalWeg(container, p) { + const { kind, id } = container; + try { + if (kind === "groep") { + await updateResource("personen", p.id, p.rol === "leerling" ? { groep: null } : { stamgroep: null }); + } else if (kind === "leerplein") { + const lp = leerpleinen.find((x) => x.id === id); if (!lp) return; + await updateResource("leerpleinen", id, p.rol === "leerling" + ? { leerlingen: (lp.leerlingen || []).filter((x) => x !== p.id) } + : { medewerkers: (lp.medewerkers || []).filter((x) => x !== p.id) }); + } else if (kind === "subgroep") { + const s = subgroepen.find((x) => x.id === id); if (!s) return; + await updateResource("subgroepen", id, p.rol === "leerling" + ? { leden: s.leden_namen.map((x) => x.id).filter((x) => x !== p.id) } + : { begeleiders: s.begeleiders_namen.map((x) => x.id).filter((x) => x !== p.id) }); + } + await laadAlles(); + } catch (e) { setError(e.message); } + } + + async function nieuweBovengroep(naam) { try { await createResource("leerpleinen", { naam }); await laadAlles(); } catch (e) { setError(e.message); } } + async function nieuweGroep(lp, naam) { try { const g = await createResource("groepen", { naam }); await updateResource("leerpleinen", lp.id, { groepen: [...(lp.groepen || []), g.id] }); await laadAlles(); } catch (e) { setError(e.message); } } + async function nieuweSubgroep(g, naam) { try { await createResource("subgroepen", { naam, groep: g.id }); await laadAlles(); } catch (e) { setError(e.message); } } + async function koppelGroep(g, lpId) { const lp = leerpleinen.find((x) => x.id === lpId); if (!lp) return; try { await updateResource("leerpleinen", lpId, { groepen: [...(lp.groepen || []), g.id] }); await laadAlles(); } catch (e) { setError(e.message); } } + async function hernoem(resource, id, huidig) { const naam = prompt("Nieuwe naam:", huidig); if (naam && naam.trim()) { try { await updateResource(resource, id, { naam: naam.trim() }); await laadAlles(); } catch (e) { setError(e.message); } } } + async function verwijder(resource, id, vraag) { if (!confirm(vraag)) return; try { await deleteResource(resource, id); await laadAlles(); } catch (e) { setError(e.message); } } + + const zoneProps = (kind, id) => ({ + "data-drop-kind": kind, "data-drop-id": id, + className: "dropzone" + (hoverZone === `${kind}:${id}` ? " over" : ""), + }); + const chip = (p, rol, container) => { + if (!p) return null; return ( -Beheer bovenliggende groepen, groepen en subgroepen op één plek. Een groep hoort altijd onder een bovenliggende groep; een subgroep hoort altijd onder een groep.
+Sleep personen naar een bovenliggende groep, groep of subgroep. leerling en personeel hebben elk een eigen kleur. Klik × om iemand uit een groep te halen.
Laden…
: ( -