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 sluiten ✕
+ setF({ ...f, tijdslot: e.target.value })}>
+ {tijdsloten.map((t) => {t.dag_label} {t.start_tijd?.slice(0, 5)} {t.naam} )}
+
setF({ ...f, activiteit: e.target.value })}>
{activiteiten.map((a) => {a.naam} )}
@@ -1324,7 +1350,10 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
- setF({ ...f, doel_type: e.target.value, doel: "" })}>
+ {
+ setF({ ...f, doel_type: e.target.value, doel: "" });
+ if (e.target.value !== "subgroep") setSubgroepGroepFilter("");
+ }}>
Groep
Subgroep
Individuele leerling
@@ -1336,6 +1365,10 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
{f.doel_type === "subgroep" && (
+ { setSubgroepGroepFilter(e.target.value ? Number(e.target.value) : ""); setF({ ...f, doel: "", doel_type: "subgroep" }); }}>
+ — alle groepen —
+ {groepen.map((g) => {g.naam} )}
+
setNieuweSub(e.target.value)} />
+ Subgroep aanmaken
@@ -1352,10 +1385,10 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
Verwijderen
- Dit vak opdelen in subgroepen (zelfde tijd):
+ Groep → subgroep: deel dit vak op onder {splitGroepNaam} (zelfde tijd):
- {subs.map((s) => toggleSplit(s.id)}>{s.naam} )}
- {subs.length === 0 && Nog geen subgroepen. }
+ {splitOpties.map((s) => toggleSplit(s.id)}>{s.naam} )}
+ {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
Type
- { setType(e.target.value); setHoofdId(""); }}>
+ { setType(e.target.value); setHoofdId(""); setNieuw({ naam: "", groep: "", begeleiders: [] }); }}>
Groep
Leerplein
@@ -1432,23 +1471,51 @@ function Groepsindeling({ groepen, leerpleinen, subgroepen, medewerkers, blokken
{hid && hoofd && (
{hoofd.naam} — {type}
-
- {subs.map((s) => (
-
-
{s.naam}
-
👤 {s.begeleiders_namen && s.begeleiders_namen.length ? s.begeleiders_namen.map((x) => x.naam).join(", ") : "geen verantwoordelijke"}
-
📚 {vakkenVan(s.id).join(", ") || "nog geen vakken"}
-
{s.aantal_leden} leerling(en)
-
verwijderSub(s.id)}>verwijderen
-
- ))}
- {subs.length === 0 &&
Nog geen subgroepen onder {hoofd.naam}.
}
-
+ {type === "leerplein" ? (
+
+ {groepenMetSubgroepen.map(({ groep, subs: groepSubs }) => (
+
+
{groep.naam}
+
+ {groepSubs.map((s) => (
+
+
{s.naam}
+
Verantwoordelijke: {s.begeleiders_namen && s.begeleiders_namen.length ? s.begeleiders_namen.map((x) => x.naam).join(", ") : "geen"}
+
Vakken: {vakkenVan(s.id).join(", ") || "nog geen vakken"}
+
{s.aantal_leden} leerling(en)
+
verwijderSub(s.id)}>verwijderen
+
+ ))}
+ {groepSubs.length === 0 &&
Nog geen subgroepen onder {groep.naam}.
}
+
+
+ ))}
+ {leerpleinGroepen.length === 0 &&
Nog geen groepen onder {hoofd.naam}.
}
+
+ ) : (
+
+ {subs.map((s) => (
+
+
{s.naam}
+
Verantwoordelijke: {s.begeleiders_namen && s.begeleiders_namen.length ? s.begeleiders_namen.map((x) => x.naam).join(", ") : "geen"}
+
Vakken: {vakkenVan(s.id).join(", ") || "nog geen vakken"}
+
{s.aantal_leden} leerling(en)
+
verwijderSub(s.id)}>verwijderen
+
+ ))}
+ {subs.length === 0 &&
Nog geen subgroepen onder {hoofd.naam}.
}
+
+ )}
- {type === "groep" ? (
-
- ) : (
-
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 && { e.stopPropagation(); verwijder(b.id); }} title="Blok verwijderen">× }
+
+
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 && <> · { e.stopPropagation(); herstel(uit.id); }}>herstel >}
}
+ {!kal && uit && !uit.vervalt && uit.vervanger_naam &&
Vervanger: {uit.vervanger_naam}
}
+ {edit && !kal && !uit && (
+
{ e.stopPropagation(); laatVervallen(b, iso); }}>dit vak laten vervallen
+ )}
+
+ );
+ }
+ 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 && (
setTab("week")}>Weekrooster
+
setTab("indeling")}>Indeling
setTab("conflicten")}>Conflicten
setTab("schooljaar")}>Schooljaar
setTab("instellingen")}>Instellingen
@@ -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 && { e.stopPropagation(); verwijder(b.id); }} title="Blok verwijderen">× }
-
-
🎯 {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 && <> · { e.stopPropagation(); herstel(uit.id); }}>herstel >}
}
- {!kal && uit && !uit.vervalt && uit.vervanger_naam &&
↪ Vervanger: {uit.vervanger_naam}
}
- {edit && !kal && !uit && (
-
{ e.stopPropagation(); laatVervallen(b, iso); }}>dit vak laten vervallen
- )}
-
- );
- })}
+ {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); }