From 77a5eacfed350095b22566dc961db57cd642285d Mon Sep 17 00:00:00 2001 From: bes-r <27369668+bes-r@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:17:47 +0200 Subject: [PATCH] =?UTF-8?q?Groepsindeling=20als=20=C3=A9=C3=A9n=20sleep-ov?= =?UTF-8?q?erzicht=20met=20kleurstructuur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ROADMAP.md | 7 + frontend/src/pages.jsx | 552 +++++++++++++++++++--------------------- frontend/src/styles.css | 51 ++++ 3 files changed, 316 insertions(+), 294 deletions(-) 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 ( +
{ e.preventDefault(); if (v.trim()) { onAdd(v.trim()); setV(""); } }} + > + setV(e.target.value)} /> + +
+ ); +} + 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 ( -
- {items.map((item) => ( - - ))} - {items.length === 0 && {leeg}} + startDrag(e, { id: p.id, rol, naam: p.volledige_naam })} + title={p.volledige_naam} + > + {p.volledige_naam} + {rol !== "leerling" && SROL[p.rol] && {SROL[p.rol]}} + {container && ( + + )} + + ); + }; + + function subKaart(s) { + const leeg = s.begeleiders_namen.length + s.leden_namen.length === 0; + return ( +
+
+ {s.naam} + + + + +
+
+
+ {s.begeleiders_namen.map((b) => chip(medewerkerById(b.id) || { id: b.id, volledige_naam: b.naam, rol: "leerkracht" }, "medewerker", { kind: "subgroep", id: s.id }))} + {s.leden_namen.map((l) => chip(leerlingById(l.id) || { id: l.id, volledige_naam: l.naam }, "leerling", { kind: "subgroep", id: s.id }))} + {leeg && sleep hier personen heen} +
+
+
+ ); + } + function groepKaart(g) { + const orphan = !groepHeeftLeerplein(g.id); + const mws = groepMedewerkers(g.id); + const lls = groepLeerlingen(g.id); + return ( +
+
+ {g.naam} + + + + +
+
+
+ {mws.map((m) => chip(m, "medewerker", { kind: "groep", id: g.id }))} + {lls.map((l) => chip(l, "leerling", { kind: "groep", id: g.id }))} + {mws.length + lls.length === 0 && sleep hier personen heen} +
+
+ {orphan && ( +
+ +
+ )} +
+ {groepSubs(g.id).map((s) => subKaart(s))} + nieuweSubgroep(g, naam)} /> +
); } - 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 { - 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 opslaanGroep(e) { - e.preventDefault(); - if (!groepForm.leerplein) { setError(`Kies een ${BOVENGROEP_LABEL_LC} 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 ${BOVENGROEP_LABEL_LC} voor leden uit de volledige ${BOVENGROEP_LABEL_LC}.`); 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 deze ${BOVENGROEP_LABEL_LC}.`); return; } - if (!confirm(`Deze ${BOVENGROEP_LABEL_LC} verwijderen?`)) return; - 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), - }); - } + const orphanGroepen = groepen.filter((g) => !groepHeeftLeerplein(g.id)); return ( -
+

Groepsindeling

-

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.

- -
-

{BOVENGROEP_LABEL}

-
- setLeerpleinForm({ ...leerpleinForm, naam: e.target.value })} /> - setLeerpleinForm({ ...leerpleinForm, omschrijving: e.target.value })} /> -
-
-
Losse leerlingen op niveau van de {BOVENGROEP_LABEL_LC}:
- setLeerpleinForm((f) => ({ ...f, leerlingen: toggleArray(f.leerlingen, id) }))} /> -
-
-
Medewerkers:
- setLeerpleinForm((f) => ({ ...f, medewerkers: toggleArray(f.medewerkers, id) }))} leeg="Geen medewerkers gevonden." /> -
-
- - {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…

: ( -
- {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} -
{lpGroepen.length} groep(en){lp.omschrijving ? ` · ${lp.omschrijving}` : ""}
+
+ +
+ + {leerpleinen.map((lp) => { + const mwIds = lp.medewerkers || []; + const llIds = lp.leerlingen || []; + return ( +
+
+ {lp.naam} + + + + +
+
+
Direct in deze bovenliggende groep
+
+ {mwIds.map((id) => chip(medewerkerById(id), "medewerker", { kind: "leerplein", id: lp.id }))} + {llIds.map((id) => chip(leerlingById(id), "leerling", { kind: "leerplein", id: lp.id }))} + {mwIds.length + llIds.length === 0 && sleep hier personen heen}
-
+
+ {lpGroepen(lp).map((g) => groepKaart(g))} + nieuweGroep(lp, naam)} /> +
-
- {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 deze {BOVENGROEP_LABEL_LC}.
} + ); + })} + {leerpleinen.length === 0 &&
Nog geen bovenliggende groepen. Maak er hierboven één aan.
} + {orphanGroepen.length > 0 && ( +
+
Groepen zonder bovenliggende groep
+
+ {orphanGroepen.map((g) => groepKaart(g))}
- ); - })} - {leerpleinen.length === 0 &&
Nog geen bovenliggende groepen.
} - {groepen.filter((g) => !leerpleinVanGroep(g.id)).length > 0 && ( -
Er zijn groepen zonder {BOVENGROEP_LABEL_LC}. Bewerk ze hieronder en kies een {BOVENGROEP_LABEL_LC}: {groepen.filter((g) => !leerpleinVanGroep(g.id)).map((g) => g.naam).join(", ")}
- )} + )} +
)} + {ghost &&
{ghost.label}
}
); } + /* ---------------------------------- Stage --------------------------------- */ // Module-scherm: interne en externe stages. const OMVANGEN = [ diff --git a/frontend/src/styles.css b/frontend/src/styles.css index f81c35e..b5c58b3 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -357,3 +357,54 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px; .subsectie h2 { margin: 0 0 4px; font-size: 18px; } .veld-kol { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 120px; } .veld-kop { font-size: 11px; font-weight: 700; color: var(--muted); } + +/* ---- Groepsindeling: sleep-overzicht met kleurstructuur ---- */ +.indeling-layout { display: grid; grid-template-columns: 240px 1fr; gap: 16px; align-items: start; } +.indeling-pool { position: sticky; top: 12px; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 12px; } +.indeling-pool h3 { margin: 0 0 8px; font-size: 14px; } +.pool-sectie { margin-bottom: 12px; } +.pool-kop { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; color: var(--muted); margin-bottom: 6px; } +.indeling-board { display: flex; flex-direction: column; gap: 14px; min-width: 0; } + +.chip-rij { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; } +.chip-persoon { display: inline-flex; align-items: center; gap: 6px; padding: 3px 8px 3px 10px; border-radius: 999px; font-size: 12px; line-height: 1.6; cursor: grab; touch-action: none; user-select: none; border: 1px solid transparent; } +.chip-persoon:active { cursor: grabbing; } +.chip-persoon.mw { background: #3730a3; color: #fff; } /* personeel: donker */ +.chip-persoon.ll { background: #dbeafe; color: #1e3a8a; border-color: #bfdbfe; } /* leerling: licht */ +.chip-rol { font-size: 10px; font-weight: 700; opacity: .8; } +.chip-x { border: 0; background: rgba(255,255,255,.25); color: inherit; border-radius: 999px; width: 16px; height: 16px; line-height: 14px; font-size: 13px; cursor: pointer; padding: 0; } +.chip-persoon.ll .chip-x { background: rgba(30,58,138,.15); } +.chip-x:hover { background: rgba(0,0,0,.25); } + +.bovengroep-kaart { border: 1px solid var(--line); border-left: 5px solid var(--brand); border-radius: 12px; background: var(--panel); padding: 10px 12px 12px; } +.bovengroep-kaart.los { border-left-color: #f59e0b; } +.groep-lijst { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; padding-left: 12px; border-left: 2px dashed #c7d2fe; } +.groep-kaart { border: 1px solid var(--line); border-left: 4px solid #0ea5e9; border-radius: 10px; background: #f8fbff; padding: 8px 10px 10px; } +.sub-lijst { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; padding-left: 12px; border-left: 2px dashed #bae6fd; } +.subgroep-kaart { border: 1px solid var(--line); border-left: 3px solid #14b8a6; border-radius: 8px; background: #f0fdfa; padding: 6px 9px 8px; } + +.kaart-kop { display: flex; align-items: center; justify-content: space-between; gap: 8px; } +.bovengroep-kop strong { font-size: 16px; color: var(--brand); } +.groep-kop strong { color: #0369a1; } +.sub-kop strong { color: #0f766e; } +.kaart-acties { display: inline-flex; gap: 6px; white-space: nowrap; } + +.dropzone { margin-top: 6px; border: 1px dashed transparent; border-radius: 8px; padding: 6px; transition: background .1s, border-color .1s; } +.dropzone.over { border-color: var(--brand); background: var(--brand-soft); } +.zone-kop { font-size: 11px; color: var(--muted); margin-bottom: 4px; } +.koppel-rij { margin-top: 6px; } + +.inline-add { display: flex; gap: 6px; margin-top: 4px; } +.inline-add input { flex: 1; min-width: 0; } +.inline-add.klein input { font-size: 13px; padding: 6px 8px; } +.btn.klein { padding: 4px 12px; font-size: 14px; } +.link.small { font-size: 12px; } + +.legenda { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 11px; font-weight: 600; } +.legenda.ll { background: #dbeafe; color: #1e3a8a; } +.legenda.mw { background: #3730a3; color: #fff; } + +@media (max-width: 760px) { + .indeling-layout { grid-template-columns: 1fr; } + .indeling-pool { position: static; } +}