From 19862ae8aa931f7d4c8713a9256f0ae27b7ba3a7 Mon Sep 17 00:00:00 2001 From: bes-r <27369668+bes-r@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:51:58 +0200 Subject: [PATCH] conflicten in rooster tonen + tabbalk logischer --- ROADMAP.md | 8 ++++ frontend/src/pages.jsx | 103 +++++++++++++++++++++++++++++++++++++--- frontend/src/styles.css | 17 +++++++ 3 files changed, 121 insertions(+), 7 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 8eb993a..fc8ee68 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -37,6 +37,14 @@ volgt. en **Modulebeheer** verdwijnen. - [x] De hint **"Menu's verschijnen alleen voor ingeschakelde modules."** uit de zijbalk verwijderd. +- [x] **Roostermaker-tabbalk logischer**: de losse tab "Conflicten" is vervallen + (conflicten staan nu in het weekrooster zelf) en Vak-wizard + Auto-rooster + zitten onder één **"Genereren ▾"**-knop. Overgebleven: Week · Jaar · + Genereren · Instellingen. +- [x] **Conflicten direct in het rooster**: een melding boven het weekrooster + (uitklapbaar met details) én een rode omlijning rond de betrokken blokken. + Client-side berekend uit de geladen blokken (begeleider/lokaal/leerling + dubbel op hetzelfde tijdslot), met respect voor geldigheidsperiodes. ## Vakken diff --git a/frontend/src/pages.jsx b/frontend/src/pages.jsx index 1c44431..7ee59c3 100644 --- a/frontend/src/pages.jsx +++ b/frontend/src/pages.jsx @@ -1604,6 +1604,67 @@ export function blokInScope(blok, scope, ctx) { return leerlingenVanBlok(blok, ctx).some((id) => set.has(id)); } +// Bereken conflicten direct uit de geladen blokken (zelfde tijdslot): begeleider +// dubbel, lokaal dubbel of een leerling dubbel ingeroosterd. Houdt rekening met +// geldigheidsperiodes (blokken die elkaar niet in de tijd overlappen botsen niet). +export function berekenRoosterConflicten(blokken, ctx, tijdsloten) { + const blokIds = new Set(); + const teksten = new Map(); // tekst -> ernst (dedupe) + const slotLabel = (id) => { + const t = (tijdsloten || []).find((x) => x.id === id); + if (!t) return ""; + const dag = (WEEKDAGEN.find((d) => d.value === t.dag) || {}).label || ""; + return `${dag} ${(t.start_tijd || "").slice(0, 5)}`.trim(); + }; + const leerlingNaam = (id) => { + const l = (ctx.leerlingen || []).find((x) => x.id === id); + return l ? l.volledige_naam : "Leerling"; + }; + const overlapt = (a, b) => { + const at = a.geldig_tot || null, av = a.geldig_van || null; + const bt = b.geldig_tot || null, bv = b.geldig_van || null; + if (at && bv && at < bv) return false; + if (bt && av && bt < av) return false; + return true; + }; + const perSlot = new Map(); + blokken.forEach((b) => { + if (!perSlot.has(b.tijdslot)) perSlot.set(b.tijdslot, []); + perSlot.get(b.tijdslot).push(b); + }); + perSlot.forEach((lijst, slotId) => { + if (lijst.length < 2) return; + const lab = slotLabel(slotId); + const flag = (a, b, tekst, ernst) => { + if (!overlapt(a, b)) return; + blokIds.add(a.id); + blokIds.add(b.id); + if (!teksten.has(tekst)) teksten.set(tekst, ernst); + }; + for (let i = 0; i < lijst.length; i++) { + for (let j = i + 1; j < lijst.length; j++) { + const a = lijst[i], b = lijst[j]; + (a.begeleiders_namen || []).forEach((p) => { + if ((b.begeleiders_namen || []).some((q) => q.id === p.id)) { + flag(a, b, `${p.naam} staat dubbel ingepland — ${lab}`, "fout"); + } + }); + if (a.lokaal && b.lokaal && a.lokaal === b.lokaal) { + flag(a, b, `Lokaal ${a.lokaal_naam} dubbel geboekt — ${lab}`, "waarschuwing"); + } + const lb = new Set(leerlingenVanBlok(b, ctx)); + leerlingenVanBlok(a, ctx) + .filter((id) => lb.has(id)) + .forEach((id) => flag(a, b, `${leerlingNaam(id)} is dubbel ingeroosterd — ${lab}`, "fout")); + } + } + }); + const meldingen = [...teksten.entries()] + .map(([tekst, ernst]) => ({ tekst, ernst })) + .sort((x, y) => (x.ernst === y.ernst ? 0 : x.ernst === "fout" ? -1 : 1)); + return { blokIds, meldingen }; +} + function groepVanSubgroep(subgroepen, subgroepId) { const sub = subgroepen.find((s) => s.id === subgroepId); return sub ? sub.groep : null; @@ -1992,6 +2053,8 @@ export function RoosterScherm({ mode }) { const laadRef = useRef(() => {}); const [ghost, setGhost] = useState(null); const [hoverSlot, setHoverSlot] = useState(null); + const [conflictOpen, setConflictOpen] = useState(false); + const [genOpen, setGenOpen] = useState(false); useEffect(() => { listResource("schooljaren").then((j) => { setSchooljaren(j); if (j.length) setSchooljaar((s) => s || String(j[0].id)); }).catch((e) => setError(e.message)); @@ -2017,6 +2080,9 @@ export function RoosterScherm({ mode }) { const scopeCtx = { leerlingen, subgroepen, leerpleinen }; const scope = { type: scopeType, id: scopeId ? Number(scopeId) : "" }; const zichtbareBlokken = blokken.filter((b) => blokInScope(b, scope, scopeCtx)); + // Conflicten over álle blokken (niet alleen de zichtbare scope), zodat de + // melding compleet is en de betrokken blokken in het rooster omlijnd worden. + const conflictInfo = berekenRoosterConflicten(blokken, scopeCtx, tijdsloten); const scopeOpties = scopeType === "klas" ? groepen : scopeType === "leerplein" ? leerpleinen : scopeType === "leerling" ? leerlingen @@ -2240,7 +2306,7 @@ export function RoosterScherm({ mode }) { const vervallen = !!kal || (uit && uit.vervalt); return (
toggleOpenBlok(b) : undefined} @@ -2275,7 +2341,7 @@ export function RoosterScherm({ mode }) { const vervallen = !!kal || (uit && uit.vervalt); return (
toggleOpenBlok(b) : undefined} @@ -2445,10 +2511,18 @@ export function RoosterScherm({ mode }) { {edit && (
- - {blokwizardAan && } - {autoroosterAan && } + {(blokwizardAan || autoroosterAan) && ( +
+ + {genOpen && ( +
setGenOpen(false)}> + {blokwizardAan && } + {autoroosterAan && } +
+ )} +
+ )}
)} @@ -2555,6 +2629,23 @@ export function RoosterScherm({ mode }) {
)} + {schooljaar && tab === "week" && conflictInfo.meldingen.length > 0 && ( +
+ + {conflictOpen && ( + + )} +
+ )} + {schooljaar && tab === "week" && (
{WEEKDAGEN.map((d, i) => { @@ -2597,8 +2688,6 @@ export function RoosterScherm({ mode }) {
)} - {schooljaar && tab === "conflicten" && } - {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 0348aef..8ca111e 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -508,3 +508,20 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px; .tabbar .tab .tab-tekst { display: none; } .tabbar .tab { padding-left: 12px; padding-right: 12px; } } + +/* "Genereren"-uitklap in de tabbalk (wizard + auto-rooster gebundeld) */ +.tab-dropdown { position: relative; } +.tab-menu { position: absolute; left: 0; top: 100%; margin-top: 4px; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,.15); min-width: 180px; padding: 6px; z-index: 60; display: flex; flex-direction: column; } +.tab-menu-item { background: transparent; border: 0; text-align: left; padding: 8px 10px; border-radius: 8px; font: inherit; color: var(--ink); cursor: pointer; } +.tab-menu-item:hover { background: var(--line); } + +/* Conflict-melding direct boven het weekrooster + omlijning van betrokken blokken */ +.conflict-banner { border: 1px solid #fca5a5; background: #fef2f2; border-radius: 10px; margin: 0 0 14px; overflow: hidden; } +.conflict-kop { display: flex; align-items: center; gap: 10px; width: 100%; background: transparent; border: 0; padding: 10px 14px; font: inherit; color: #991b1b; cursor: pointer; text-align: left; } +.conflict-kop .ico { font-size: 16px; } +.conflict-kop .conflict-aantal { font-weight: 600; flex: 1; } +.conflict-lijst { list-style: none; margin: 0; padding: 0 14px 12px; } +.conflict-lijst li { padding: 4px 0 4px 16px; position: relative; color: #7f1d1d; font-size: 14px; } +.conflict-lijst li::before { content: "•"; position: absolute; left: 2px; } +.conflict-lijst li.waarschuwing { color: #92400e; } +.rooster-blok.in-conflict, .split-vak.in-conflict { outline: 2px solid #ef4444; outline-offset: 1px; box-shadow: 0 0 0 4px rgba(239,68,68,.15); }