From 8f62734af35ca3e6e8e1f3e45f9b3f08f3a3a856 Mon Sep 17 00:00:00 2001 From: bes-r <27369668+bes-r@users.noreply.github.com> Date: Sun, 14 Jun 2026 15:57:12 +0200 Subject: [PATCH] Verbeter UI van roostermaker --- frontend/src/pages.jsx | 273 ++++++++++++++++++++++++++++++---------- frontend/src/styles.css | 10 ++ 2 files changed, 219 insertions(+), 64 deletions(-) diff --git a/frontend/src/pages.jsx b/frontend/src/pages.jsx index a62b008..5fe9e9d 100644 --- a/frontend/src/pages.jsx +++ b/frontend/src/pages.jsx @@ -1235,10 +1235,22 @@ export function blokInScope(blok, scope, ctx) { return leerlingenVanBlok(blok, ctx).some((id) => set.has(id)); } +function groepVanSubgroep(subgroepen, subgroepId) { + const sub = subgroepen.find((s) => s.id === subgroepId); + return sub ? sub.groep : null; +} + +function bovenliggendeGroep(blok, subgroepen) { + if (blok.groep) return blok.groep; + if (blok.subgroep) return groepVanSubgroep(subgroepen, blok.subgroep); + return null; +} + /* ----------------------------- Blok bewerken/kopiëren --------------------- */ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) { const { activiteiten, locaties, groepen, subgroepen, leerlingen, medewerkers, tijdsloten } = ctx; const [f, setF] = useState({ + tijdslot: blok.tijdslot, activiteit: blok.activiteit, locatie: blok.locatie || "", doel_type: blok.doel_type, @@ -1253,15 +1265,26 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) { const [splitSubs, setSplitSubs] = useState([]); const [splitNieuw, setSplitNieuw] = useState(""); const [verwijderOrig, setVerwijderOrig] = useState(true); - const doelOpties = f.doel_type === "groep" ? groepen : f.doel_type === "subgroep" ? subs : leerlingen; - // Groep waaronder nieuwe split-subgroepen vallen: de groep van het blok zelf. - const splitGroep = blok.groep || null; + const [subgroepGroepFilter, setSubgroepGroepFilter] = useState(() => ( + blok.subgroep ? groepVanSubgroep(subgroepen, blok.subgroep) || "" : "" + )); + const gekozenGroep = f.doel_type === "groep" + ? (f.doel ? Number(f.doel) : null) + : f.doel_type === "subgroep" + ? (subgroepGroepFilter || groepVanSubgroep(subs, Number(f.doel))) + : bovenliggendeGroep(blok, subs); + const subgroepOpties = gekozenGroep ? subs.filter((s) => s.groep === gekozenGroep) : subs; + const doelOpties = f.doel_type === "groep" ? groepen : f.doel_type === "subgroep" ? subgroepOpties : leerlingen; + const splitGroep = gekozenGroep || blok.groep || groepVanSubgroep(subs, blok.subgroep) || null; + const splitOpties = splitGroep ? subs.filter((s) => s.groep === splitGroep) : subs; + const splitGroepNaam = splitGroep ? (groepen.find((g) => g.id === splitGroep)?.naam || "groep") : "groep"; async function maakSubgroep() { if (!nieuweSub.trim()) return; try { - const s = await createResource("subgroepen", { naam: nieuweSub.trim(), groep: f.doel_type === "groep" && f.doel ? f.doel : null }); + const s = await createResource("subgroepen", { naam: nieuweSub.trim(), groep: gekozenGroep || null }); setSubs([...subs, s]); + setSubgroepGroepFilter(s.groep || ""); setF({ ...f, doel_type: "subgroep", doel: s.id }); setNieuweSub(""); } catch (e) { setError(e.message); } @@ -1280,7 +1303,7 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) { if (splitSubs.length === 0) return; try { for (const sid of splitSubs) { - await createResource("roosterblokken", { schooljaar: blok.schooljaar, tijdslot: blok.tijdslot, activiteit: f.activiteit, locatie: f.locatie || null, subgroep: sid, begeleiders: f.begeleiders, opmerkingen: f.opmerkingen }); + await createResource("roosterblokken", { schooljaar: blok.schooljaar, tijdslot: f.tijdslot, activiteit: f.activiteit, locatie: f.locatie || null, subgroep: sid, begeleiders: f.begeleiders, opmerkingen: f.opmerkingen }); } if (verwijderOrig) await deleteResource("roosterblokken", blok.id); await onKlaar(); @@ -1294,7 +1317,7 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) { leerling: f.doel_type === "leerling" ? f.doel : null, }); async function opslaan() { - try { await updateResource("roosterblokken", blok.id, { activiteit: f.activiteit, locatie: f.locatie || null, ...doelPayload(), begeleiders: f.begeleiders, opmerkingen: f.opmerkingen }); await onKlaar(); } + try { await updateResource("roosterblokken", blok.id, { tijdslot: f.tijdslot, activiteit: f.activiteit, locatie: f.locatie || null, ...doelPayload(), begeleiders: f.begeleiders, opmerkingen: f.opmerkingen }); await onKlaar(); } catch (e) { setError(e.message); } } async function kopieer() { @@ -1315,6 +1338,9 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {

Blok bewerken

+ @@ -1324,7 +1350,10 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
- { + setF({ ...f, doel_type: e.target.value, doel: "" }); + if (e.target.value !== "subgroep") setSubgroepGroepFilter(""); + }}> @@ -1336,6 +1365,10 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
{f.doel_type === "subgroep" && (
+ setNieuweSub(e.target.value)} />
@@ -1352,10 +1385,10 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
-
Dit vak opdelen in subgroepen (zelfde tijd):
+
Groep → subgroep: deel dit vak op onder {splitGroepNaam} (zelfde tijd):
- {subs.map((s) => )} - {subs.length === 0 && Nog geen subgroepen.} + {splitOpties.map((s) => )} + {splitOpties.length === 0 && Nog geen subgroepen onder deze groep.}
setSplitNieuw(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); maakSplitSub(); } }} /> @@ -1385,7 +1418,7 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) { function Groepsindeling({ groepen, leerpleinen, subgroepen, medewerkers, blokken, onWijzig }) { const [type, setType] = useState("groep"); const [hoofdId, setHoofdId] = useState(""); - const [nieuw, setNieuw] = useState({ naam: "", begeleiders: [] }); + const [nieuw, setNieuw] = useState({ naam: "", groep: "", begeleiders: [] }); const [error, setError] = useState(null); const opties = type === "groep" ? groepen : leerpleinen; @@ -1395,6 +1428,12 @@ function Groepsindeling({ groepen, leerpleinen, subgroepen, medewerkers, blokken let subs = []; if (hid && type === "groep") subs = subgroepen.filter((s) => s.groep === hid); if (hid && type === "leerplein" && hoofd) subs = subgroepen.filter((s) => (hoofd.subgroepen || []).includes(s.id)); + const leerpleinGroepen = type === "leerplein" && hoofd + ? groepen.filter((g) => (hoofd.groepen || []).includes(g.id)) + : []; + const groepenMetSubgroepen = type === "leerplein" + ? leerpleinGroepen.map((g) => ({ groep: g, subs: subgroepen.filter((s) => s.groep === g.id) })) + : []; const vakkenVan = (subId) => [...new Set(blokken.filter((b) => b.subgroep === subId).map((b) => b.activiteit_naam))]; const toggleBeg = (id) => setNieuw((n) => n.begeleiders.includes(id) ? { ...n, begeleiders: n.begeleiders.filter((x) => x !== id) } : { ...n, begeleiders: [...n.begeleiders, id] }); @@ -1402,8 +1441,8 @@ function Groepsindeling({ groepen, leerpleinen, subgroepen, medewerkers, blokken async function voegToe(e) { e.preventDefault(); try { - await createResource("subgroepen", { naam: nieuw.naam, groep: hid, begeleiders: nieuw.begeleiders }); - setNieuw({ naam: "", begeleiders: [] }); + await createResource("subgroepen", { naam: nieuw.naam, groep: type === "groep" ? hid : nieuw.groep, begeleiders: nieuw.begeleiders }); + setNieuw({ naam: "", groep: "", begeleiders: [] }); await onWijzig(); } catch (err) { setError(err.message); } } @@ -1418,7 +1457,7 @@ function Groepsindeling({ groepen, leerpleinen, subgroepen, medewerkers, blokken
- ) : ( -

Subgroepen toevoegen kan onder een groep (een leerplein bundelt bestaande subgroepen).

- )}
)} @@ -1642,6 +1706,103 @@ export function RoosterScherm({ mode }) { // Op id vergelijken, niet op naam: twee personen kunnen dezelfde naam hebben. const persoonAfwezig = (id, iso) => afwezig.some((a) => a.persoon === id && a.start_datum <= iso && a.eind_datum >= iso); const rolAfk = (id) => { const m = medewerkers.find((x) => x.id === id); return m ? (ROL_AFK[m.rol] || "") : ""; }; + function renderRoosterBlok(b, iso, kal) { + const uit = uitzOp(b.id, iso); + const vervallen = !!kal || (uit && uit.vervalt); + return ( +
setBewerkBlok(b) : undefined} + title={edit ? "Klik om te bewerken" : undefined} + > +
+ {edit && startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot">⠿} + {b.activiteit_naam} + {edit && } +
+
Doel: {b.doel_naam} ({b.doel_type})
+ {b.locatie_naam &&
Locatie: {b.locatie_naam}
} + {b.begeleiders_namen.length > 0 && ( +
Begeleiding: {b.begeleiders_namen.map((x, idx) => ( + {idx > 0 ? ", " : ""}{x.naam}{rolAfk(x.id) ? ` (${rolAfk(x.id)})` : ""} + ))}
+ )} + {kal &&
Vervalt - {kal.type_label}
} + {!kal && uit && uit.vervalt &&
Vervalt {uit.opmerking ? `(${uit.opmerking})` : ""}{edit && <> · }
} + {!kal && uit && !uit.vervalt && uit.vervanger_naam &&
Vervanger: {uit.vervanger_naam}
} + {edit && !kal && !uit && ( + + )} +
+ ); + } + function renderSlotBlokken(slotBlokken, iso, kal) { + const losse = []; + const perGroep = new Map(); + slotBlokken.forEach((b) => { + const gid = bovenliggendeGroep(b, subgroepen); + if (!gid) { losse.push(b); return; } + if (!perGroep.has(gid)) perGroep.set(gid, []); + perGroep.get(gid).push(b); + }); + const out = losse.map((b) => renderRoosterBlok(b, iso, kal)); + if (scopeType === "leerplein" && scopeId && perGroep.size > 0) { + const leerplein = leerpleinen.find((l) => l.id === Number(scopeId)); + out.push( +
+
{leerplein ? leerplein.naam : "Leerplein"}
+
+ {[...perGroep.entries()].map(([gid, groepBlokken]) => { + const groep = groepen.find((g) => g.id === gid); + return ( +
+
{groep ? groep.naam : "Groep"}
+ {groepBlokken.map((b) => renderRoosterBlok(b, iso, kal))} +
+ ); + })} +
+
+ ); + return out; + } + perGroep.forEach((groepBlokken, gid) => { + const heeftSplitsing = groepBlokken.some((b) => b.subgroep); + if (!heeftSplitsing) { + groepBlokken.forEach((b) => out.push(renderRoosterBlok(b, iso, kal))); + return; + } + const groep = groepen.find((g) => g.id === gid); + const heleGroep = groepBlokken.filter((b) => b.groep === gid); + const subIds = [...new Set(groepBlokken.filter((b) => b.subgroep).map((b) => b.subgroep))]; + out.push( +
+
{groep ? groep.naam : "Groep"}
+
+ {heleGroep.length > 0 && ( +
+
Hele groep
+ {heleGroep.map((b) => renderRoosterBlok(b, iso, kal))} +
+ )} + {subIds.map((sid) => { + const sub = subgroepen.find((s) => s.id === sid); + const blocks = groepBlokken.filter((b) => b.subgroep === sid); + return ( +
+
{sub ? sub.naam : "Subgroep"}
+ {blocks.map((b) => renderRoosterBlok(b, iso, kal))} +
+ ); + })} +
+
+ ); + }); + return out; + } return (
@@ -1698,6 +1859,7 @@ export function RoosterScherm({ mode }) { {edit && (
+ @@ -1775,38 +1937,7 @@ export function RoosterScherm({ mode }) { key={t.id} >
{t.start_tijd?.slice(0, 5)} {t.naam}
- {slotBlokken.map((b) => { - const uit = uitzOp(b.id, iso); - const vervallen = !!kal || (uit && uit.vervalt); - return ( -
setBewerkBlok(b) : undefined} - title={edit ? "Klik om te bewerken" : undefined} - > -
- {edit && startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot">⠿} - {b.activiteit_naam} - {edit && } -
-
🎯 {b.doel_naam} ({b.doel_type})
- {b.locatie_naam &&
📍 {b.locatie_naam}
} - {b.begeleiders_namen.length > 0 && ( -
👤 {b.begeleiders_namen.map((x, idx) => ( - {idx > 0 ? ", " : ""}{x.naam}{rolAfk(x.id) ? ` (${rolAfk(x.id)})` : ""} - ))}
- )} - {kal &&
Vervalt — {kal.type_label}
} - {!kal && uit && uit.vervalt &&
Vervalt {uit.opmerking ? `(${uit.opmerking})` : ""}{edit && <> · }
} - {!kal && uit && !uit.vervalt && uit.vervanger_naam &&
↪ Vervanger: {uit.vervanger_naam}
} - {edit && !kal && !uit && ( - - )} -
- ); - })} + {renderSlotBlokken(slotBlokken, iso, kal)}
); })} @@ -1818,6 +1949,20 @@ export function RoosterScherm({ mode }) { {schooljaar && tab === "conflicten" && } + {edit && schooljaar && tab === "indeling" && ( + { + setSubgroepen(await listResource("subgroepen")); + await laadBlokken(); + }} + /> + )} + {tab === "schooljaar" && (

Maak hier snel een schooljaar aan en beheer de kalender (studiedagen, vrije dagen, vakanties).

diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 10abaf0..c74ee07 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -153,11 +153,21 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px; /* --- Groepsindeling --- */ .indeling { margin-top: 12px; } .indeling-hoofd { background: var(--brand); color: #fff; border-radius: 10px; padding: 12px 16px; font-weight: 700; font-size: 16px; text-align: center; } +.indeling-groepen { display: grid; gap: 14px; margin: 12px 0 16px; } +.indeling-groep { border: 1px solid var(--line); border-radius: 8px; background: #fafbff; padding: 10px; } +.indeling-groep-kop { font-weight: 700; margin-bottom: 8px; color: var(--brand); } .indeling-kolommen { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; margin: 12px 0 16px; } .indeling-kolom { border: 1px solid var(--line); border-top: 3px solid var(--brand); border-radius: 10px; padding: 10px 12px; background: var(--panel); display: flex; flex-direction: column; gap: 4px; } .indeling-kop { font-weight: 600; margin-bottom: 2px; } @media (max-width: 700px) { .indeling-kolommen { grid-template-columns: 1fr; } } +.blok-splitsing { border: 1px solid var(--line); border-radius: 8px; background: #fafbff; padding: 6px; margin-bottom: 8px; } +.split-groep { font-size: 12px; font-weight: 700; color: var(--brand); margin: 0 2px 6px; } +.split-kolommen { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 6px; align-items: stretch; } +.split-kolom { min-width: 0; } +.split-kop { font-size: 11px; font-weight: 700; color: var(--muted); margin: 0 0 4px; } +.split-kolom .rooster-blok { margin-bottom: 0; } + /* --- Blok-bewerker (modal) --- */ .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); }