Groepsindeling als één sleep-overzicht met kleurstructuur
This commit is contained in:
parent
3d29359d61
commit
77a5eacfed
3 changed files with 316 additions and 294 deletions
|
|
@ -62,6 +62,13 @@ volgt.
|
||||||
- [x] Interne stages gebruiken nu een **lokaal** (de aparte stagelocatie-lijst is
|
- [x] Interne stages gebruiken nu een **lokaal** (de aparte stagelocatie-lijst is
|
||||||
vervallen) en tellen mee in de dubbele-boekingcheck.
|
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
|
## Koppelingen & inzetregels
|
||||||
|
|
||||||
- [x] **Personeel koppelen aan leerplein en subgroep** (personeelsformulier:
|
- [x] **Personeel koppelen aan leerplein en subgroep** (personeelsformulier:
|
||||||
|
|
|
||||||
|
|
@ -804,6 +804,21 @@ export function AfwezigheidPage() {
|
||||||
|
|
||||||
/* ----------------------------- Groepsindeling ----------------------------- */
|
/* ----------------------------- Groepsindeling ----------------------------- */
|
||||||
// Gezamenlijk beheer: bovenliggende groep -> groep -> subgroep.
|
// Gezamenlijk beheer: bovenliggende groep -> groep -> subgroep.
|
||||||
|
const SROL = { leerkracht: "LK", praktijkondersteuner: "PO", klassenondersteuner: "KO" };
|
||||||
|
|
||||||
|
function InlineAdd({ placeholder, onAdd, klein }) {
|
||||||
|
const [v, setV] = useState("");
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className={klein ? "inline-add klein" : "inline-add"}
|
||||||
|
onSubmit={(e) => { e.preventDefault(); if (v.trim()) { onAdd(v.trim()); setV(""); } }}
|
||||||
|
>
|
||||||
|
<input placeholder={placeholder} value={v} onChange={(e) => setV(e.target.value)} />
|
||||||
|
<button className="btn klein" type="submit">+</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function GroepsindelingPage() {
|
export function GroepsindelingPage() {
|
||||||
const [leerpleinen, setLeerpleinen] = useState([]);
|
const [leerpleinen, setLeerpleinen] = useState([]);
|
||||||
const [groepen, setGroepen] = useState([]);
|
const [groepen, setGroepen] = useState([]);
|
||||||
|
|
@ -813,15 +828,9 @@ export function GroepsindelingPage() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
const leegLeerplein = { naam: "", omschrijving: "", leerlingen: [], medewerkers: [] };
|
const dragRef = useRef(null);
|
||||||
const leegGroep = { naam: "", omschrijving: "", leerplein: "", leerlingen: [] };
|
const [ghost, setGhost] = useState(null);
|
||||||
const leegSubgroep = { naam: "", omschrijving: "", groep: "", leerplein: "", ledenBron: "groep", leden: [], begeleiders: [] };
|
const [hoverZone, setHoverZone] = useState(null);
|
||||||
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);
|
|
||||||
|
|
||||||
async function laadAlles() {
|
async function laadAlles() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -839,316 +848,271 @@ export function GroepsindelingPage() {
|
||||||
setLeerlingen(ll);
|
setLeerlingen(ll);
|
||||||
setMedewerkers(ps.filter((p) => p.rol !== "leerling"));
|
setMedewerkers(ps.filter((p) => p.rol !== "leerling"));
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) { setError(err.message); } finally { setLoading(false); }
|
||||||
setError(err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
useEffect(() => { laadAlles(); }, []);
|
useEffect(() => { laadAlles(); }, []);
|
||||||
|
|
||||||
const toggleArray = (lijst, id) => lijst.includes(id) ? lijst.filter((x) => x !== id) : [...lijst, id];
|
const leerlingById = (id) => leerlingen.find((l) => l.id === id);
|
||||||
const groepNaam = (id) => groepen.find((g) => g.id === id)?.naam || "—";
|
const medewerkerById = (id) => medewerkers.find((m) => m.id === id);
|
||||||
const leerpleinVanGroep = (groepId) => leerpleinen.find((lp) => (lp.groepen || []).includes(Number(groepId)));
|
const groepLeerlingen = (gid) => leerlingen.filter((l) => l.groep === gid);
|
||||||
const leerpleinVanSubgroep = (subgroepId) => leerpleinen.find((lp) => (lp.subgroepen || []).includes(Number(subgroepId)));
|
const groepMedewerkers = (gid) => medewerkers.filter((m) => m.stamgroep === gid);
|
||||||
const leerlingenVanGroep = (groepId) => leerlingen.filter((l) => l.groep === Number(groepId));
|
const groepSubs = (gid) => subgroepen.filter((s) => s.groep === gid);
|
||||||
function leerlingenVanLeerplein(leerpleinId) {
|
const lpGroepen = (lp) => groepen.filter((g) => (lp.groepen || []).includes(g.id));
|
||||||
const lp = leerpleinen.find((x) => x.id === Number(leerpleinId));
|
const groepHeeftLeerplein = (gid) => leerpleinen.some((lp) => (lp.groepen || []).includes(gid));
|
||||||
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." }) {
|
// "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 (
|
return (
|
||||||
<div className="check-grid">
|
<span
|
||||||
{items.map((item) => (
|
key={(container ? container.kind + container.id : "pool") + "-" + p.id}
|
||||||
<label className="check-item" key={item.id}>
|
className={`chip-persoon ${rol === "leerling" ? "ll" : "mw"}`}
|
||||||
<input type="checkbox" checked={geselecteerd.includes(item.id)} onChange={() => toggle(item.id)} />
|
onPointerDown={(e) => startDrag(e, { id: p.id, rol, naam: p.volledige_naam })}
|
||||||
<span>{item.volledige_naam || item.naam}</span>
|
title={p.volledige_naam}
|
||||||
</label>
|
>
|
||||||
))}
|
<span className="chip-naam">{p.volledige_naam}</span>
|
||||||
{items.length === 0 && <span className="muted small">{leeg}</span>}
|
{rol !== "leerling" && SROL[p.rol] && <span className="chip-rol">{SROL[p.rol]}</span>}
|
||||||
|
{container && (
|
||||||
|
<button type="button" className="chip-x" title="Uit deze groep halen"
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
onClick={(e) => { e.stopPropagation(); haalWeg(container, { id: p.id, rol }); }}
|
||||||
|
>×</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function subKaart(s) {
|
||||||
|
const leeg = s.begeleiders_namen.length + s.leden_namen.length === 0;
|
||||||
|
return (
|
||||||
|
<div className="subgroep-kaart" key={s.id}>
|
||||||
|
<div className="kaart-kop sub-kop">
|
||||||
|
<strong>{s.naam}</strong>
|
||||||
|
<span className="kaart-acties">
|
||||||
|
<button className="link small" onClick={() => hernoem("subgroepen", s.id, s.naam)}>naam</button>
|
||||||
|
<button className="link small danger" onClick={() => verwijder("subgroepen", s.id, `Subgroep '${s.naam}' verwijderen?`)}>×</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div {...zoneProps("subgroep", s.id)}>
|
||||||
|
<div className="chip-rij">
|
||||||
|
{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 && <span className="muted small">sleep hier personen heen</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function groepKaart(g) {
|
||||||
|
const orphan = !groepHeeftLeerplein(g.id);
|
||||||
|
const mws = groepMedewerkers(g.id);
|
||||||
|
const lls = groepLeerlingen(g.id);
|
||||||
|
return (
|
||||||
|
<div className="groep-kaart" key={g.id}>
|
||||||
|
<div className="kaart-kop groep-kop">
|
||||||
|
<strong>{g.naam}</strong>
|
||||||
|
<span className="kaart-acties">
|
||||||
|
<button className="link small" onClick={() => hernoem("groepen", g.id, g.naam)}>naam</button>
|
||||||
|
<button className="link small danger" onClick={() => verwijder("groepen", g.id, `Groep '${g.naam}' verwijderen? (roosterblokken van deze groep vervallen)`)}>×</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div {...zoneProps("groep", g.id)}>
|
||||||
|
<div className="chip-rij">
|
||||||
|
{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 && <span className="muted small">sleep hier personen heen</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{orphan && (
|
||||||
|
<div className="koppel-rij">
|
||||||
|
<select value="" onChange={(e) => e.target.value && koppelGroep(g, Number(e.target.value))}>
|
||||||
|
<option value="">koppel aan bovenliggende groep…</option>
|
||||||
|
{leerpleinen.map((lp) => <option key={lp.id} value={lp.id}>{lp.naam}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="sub-lijst">
|
||||||
|
{groepSubs(g.id).map((s) => subKaart(s))}
|
||||||
|
<InlineAdd klein placeholder="+ subgroep" onAdd={(naam) => nieuweSubgroep(g, naam)} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncGroepLeerplein(groepId, leerpleinId) {
|
const orphanGroepen = groepen.filter((g) => !groepHeeftLeerplein(g.id));
|
||||||
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),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section className="indeling">
|
||||||
<h1>Groepsindeling</h1>
|
<h1>Groepsindeling</h1>
|
||||||
<p className="muted">Beheer bovenliggende groepen, groepen en subgroepen op één plek. Een groep hoort altijd onder een bovenliggende groep; een subgroep hoort altijd onder een groep.</p>
|
<p className="muted">Sleep personen naar een bovenliggende groep, groep of subgroep. <span className="legenda ll">leerling</span> en <span className="legenda mw">personeel</span> hebben elk een eigen kleur. Klik × om iemand uit een groep te halen.</p>
|
||||||
<Foutmelding msg={error} />
|
<Foutmelding msg={error} />
|
||||||
|
|
||||||
<form className="form-card indeling-form" onSubmit={opslaanLeerplein}>
|
|
||||||
<h2>{BOVENGROEP_LABEL}</h2>
|
|
||||||
<div className="form-row">
|
|
||||||
<input placeholder={`Naam ${BOVENGROEP_LABEL_LC}`} 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">Losse leerlingen op niveau van de {BOVENGROEP_LABEL_LC}:</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>
|
|
||||||
<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">{editLeerpleinId ? `${BOVENGROEP_LABEL} opslaan` : `${BOVENGROEP_LABEL} toevoegen`}</button>
|
|
||||||
{editLeerpleinId && <button type="button" className="btn" onClick={() => { setEditLeerpleinId(null); setLeerpleinForm(leegLeerplein); }}>Annuleren</button>}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form className="form-card indeling-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="">— {BOVENGROEP_LABEL_LC} —</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 indeling-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 ${BOVENGROEP_LABEL_LC}`})</option>)}
|
|
||||||
</select>
|
|
||||||
<select value={subgroepForm.leerplein} onChange={(e) => setSubgroepForm({ ...subgroepForm, leerplein: e.target.value })}>
|
|
||||||
<option value="">— koppel ook aan {BOVENGROEP_LABEL_LC} —</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 volledige {BOVENGROEP_LABEL_LC}</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> : (
|
||||||
<div className="indeling-tree">
|
<div className="indeling-layout">
|
||||||
{leerpleinen.map((lp) => {
|
<aside className="indeling-pool">
|
||||||
const lpGroepen = groepen.filter((g) => (lp.groepen || []).includes(g.id));
|
<h3>Nog niet ingedeeld</h3>
|
||||||
const lpSubgroepen = subgroepen.filter((s) => (lp.subgroepen || []).includes(s.id) || lpGroepen.some((g) => g.id === s.groep));
|
<div className="pool-sectie">
|
||||||
return (
|
<div className="pool-kop">Leerlingen</div>
|
||||||
<div className="tree-root" key={lp.id}>
|
<div className="chip-rij">
|
||||||
<div className="tree-node tree-parent">
|
{losseLeerlingen.map((l) => chip(l, "leerling", null))}
|
||||||
<div className="tree-node-main">
|
{losseLeerlingen.length === 0 && <span className="muted small">— allemaal ingedeeld —</span>}
|
||||||
<span className="tree-dot parent" />
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<strong>{lp.naam}</strong>
|
<div className="pool-sectie">
|
||||||
<div className="muted small">{lpGroepen.length} groep(en){lp.omschrijving ? ` · ${lp.omschrijving}` : ""}</div>
|
<div className="pool-kop">Personeel</div>
|
||||||
|
<div className="chip-rij">
|
||||||
|
{losseMedewerkers.map((m) => chip(m, "medewerker", null))}
|
||||||
|
{losseMedewerkers.length === 0 && <span className="muted small">— allemaal ingedeeld —</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<div className="indeling-board">
|
||||||
|
<InlineAdd placeholder="Nieuwe bovenliggende groep…" onAdd={nieuweBovengroep} />
|
||||||
|
{leerpleinen.map((lp) => {
|
||||||
|
const mwIds = lp.medewerkers || [];
|
||||||
|
const llIds = lp.leerlingen || [];
|
||||||
|
return (
|
||||||
|
<div className="bovengroep-kaart" key={lp.id}>
|
||||||
|
<div className="kaart-kop bovengroep-kop">
|
||||||
|
<strong>{lp.naam}</strong>
|
||||||
|
<span className="kaart-acties">
|
||||||
|
<button className="link small" onClick={() => hernoem("leerpleinen", lp.id, lp.naam)}>naam</button>
|
||||||
|
<button className="link small danger" onClick={() => verwijder("leerpleinen", lp.id, `Bovenliggende groep '${lp.naam}' verwijderen?`)}>×</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div {...zoneProps("leerplein", lp.id)}>
|
||||||
|
<div className="zone-kop">Direct in deze bovenliggende groep</div>
|
||||||
|
<div className="chip-rij">
|
||||||
|
{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 && <span className="muted small">sleep hier personen heen</span>}
|
||||||
</div>
|
</div>
|
||||||
</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 className="groep-lijst">
|
||||||
|
{lpGroepen(lp).map((g) => groepKaart(g))}
|
||||||
|
<InlineAdd klein placeholder="+ groep" onAdd={(naam) => nieuweGroep(lp, naam)} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="tree-children">
|
);
|
||||||
{lpGroepen.map((g) => {
|
})}
|
||||||
const groepLeerlingen = leerlingenVanGroep(g.id);
|
{leerpleinen.length === 0 && <div className="muted">Nog geen bovenliggende groepen. Maak er hierboven één aan.</div>}
|
||||||
const groepSubs = lpSubgroepen.filter((s) => s.groep === g.id);
|
{orphanGroepen.length > 0 && (
|
||||||
return (
|
<div className="bovengroep-kaart los">
|
||||||
<div className="tree-branch" key={g.id}>
|
<div className="kaart-kop"><strong>Groepen zonder bovenliggende groep</strong></div>
|
||||||
<div className="tree-node tree-group">
|
<div className="groep-lijst">
|
||||||
<div className="tree-node-main">
|
{orphanGroepen.map((g) => groepKaart(g))}
|
||||||
<span className="tree-dot group" />
|
|
||||||
<div>
|
|
||||||
<strong>{g.naam}</strong>
|
|
||||||
<div className="muted small">{groepLeerlingen.length} leerling(en)</div>
|
|
||||||
</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="tree-meta muted small">{groepLeerlingen.map((l) => l.volledige_naam).join(", ") || "Geen leerlingen"}</div>
|
|
||||||
<div className="tree-children tree-subchildren">
|
|
||||||
{groepSubs.map((s) => (
|
|
||||||
<div className="tree-node tree-subgroup" key={s.id}>
|
|
||||||
<div className="tree-node-main">
|
|
||||||
<span className="tree-dot sub" />
|
|
||||||
<div>
|
|
||||||
<strong>{s.naam}</strong>
|
|
||||||
<div className="muted small">{s.aantal_leden} leerling(en): {s.leden_namen.map((x) => x.naam).join(", ") || "geen"}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<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>
|
|
||||||
))}
|
|
||||||
{groepSubs.length === 0 && <div className="tree-empty muted small">Geen subgroepen.</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{lpGroepen.length === 0 && <div className="tree-empty muted">Nog geen groepen onder deze {BOVENGROEP_LABEL_LC}.</div>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
})}
|
</div>
|
||||||
{leerpleinen.length === 0 && <div className="muted">Nog geen bovenliggende groepen.</div>}
|
|
||||||
{groepen.filter((g) => !leerpleinVanGroep(g.id)).length > 0 && (
|
|
||||||
<div className="banner error">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(", ")}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{ghost && <div className="drag-ghost" style={{ left: ghost.x + 10, top: ghost.y + 10 }}>{ghost.label}</div>}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ---------------------------------- Stage --------------------------------- */
|
/* ---------------------------------- Stage --------------------------------- */
|
||||||
// Module-scherm: interne en externe stages.
|
// Module-scherm: interne en externe stages.
|
||||||
const OMVANGEN = [
|
const OMVANGEN = [
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
.subsectie h2 { margin: 0 0 4px; font-size: 18px; }
|
||||||
.veld-kol { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 120px; }
|
.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); }
|
.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; }
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue