uniforme vak-/klasweergave: klas-kop met vak eronder, één kaartontwerp

This commit is contained in:
bes-r 2026-06-18 23:32:30 +02:00
parent 2600db7f69
commit 066fe23250
2 changed files with 77 additions and 63 deletions

View file

@ -2303,33 +2303,39 @@ export function RoosterScherm({ mode }) {
const editorCtx = { activiteiten, locaties, lokalen, groepen, subgroepen, leerlingen, medewerkers, tijdsloten }; const editorCtx = { activiteiten, locaties, lokalen, groepen, subgroepen, leerlingen, medewerkers, tijdsloten };
const isOpenBlok = (b) => edit && bewerkBlok && bewerkBlok.id === b.id; const isOpenBlok = (b) => edit && bewerkBlok && bewerkBlok.id === b.id;
const toggleOpenBlok = (b) => setBewerkBlok((huidig) => (huidig && huidig.id === b.id ? null : b)); const toggleOpenBlok = (b) => setBewerkBlok((huidig) => (huidig && huidig.id === b.id ? null : b));
function renderRoosterBlok(b, iso, kal) { // Eén uniform vak-kaartje, gebruikt in álle situaties (los vak of vak per
// subgroep) zodat de weergave overal hetzelfde oogt.
function renderVakKaart(b, iso, kal) {
const uit = uitzOp(b.id, iso); const uit = uitzOp(b.id, iso);
const vervallen = !!kal || (uit && uit.vervalt); const vervallen = !!kal || (uit && uit.vervalt);
return ( return (
<div <div
className={(vervallen ? "rooster-blok vervallen" : "rooster-blok") + (edit ? " klikbaar" : "") + (isOpenBlok(b) ? " open" : "") + (conflictInfo.blokIds.has(b.id) ? " in-conflict" : "")} className={"vak-kaart" + (vervallen ? " vervallen" : "") + (edit ? " klikbaar" : "") + (isOpenBlok(b) ? " open" : "") + (conflictInfo.blokIds.has(b.id) ? " in-conflict" : "")}
key={b.id} key={b.id}
style={{ borderLeftColor: b.activiteit_kleur }} style={{ borderLeftColor: b.activiteit_kleur }}
onClick={edit ? () => toggleOpenBlok(b) : undefined} onClick={edit ? () => toggleOpenBlok(b) : undefined}
title={edit ? "Klik om te bewerken" : undefined} title={edit ? "Klik om te bewerken" : undefined}
> >
<div className="rb-top"> <div className="vk-kop">
{edit && <span className="rb-greep" onPointerDown={(e) => startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot"></span>} {edit && <span className="rb-greep" onPointerDown={(e) => startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot"></span>}
{b.activiteit_pictogram && <img src={b.activiteit_pictogram} alt="" className="rb-picto" />}<strong>{b.activiteit_icoon || vakIcoon(b.activiteit_naam)} {b.activiteit_naam}</strong> {b.activiteit_pictogram && <img src={b.activiteit_pictogram} alt="" className="rb-picto" />}
<strong className="vk-naam">{b.activiteit_icoon || vakIcoon(b.activiteit_naam)} {b.activiteit_naam}</strong>
{edit && <button type="button" className="icon-btn" onClick={(e) => { e.stopPropagation(); toggleOpenBlok(b); }} title="Bewerken"></button>} {edit && <button type="button" className="icon-btn" onClick={(e) => { e.stopPropagation(); toggleOpenBlok(b); }} title="Bewerken"></button>}
{edit && <button type="button" className="icon-btn danger rb-x" onClick={(e) => { e.stopPropagation(); verwijder(b.id); }} title="Blok verwijderen">×</button>} {edit && <button type="button" className="icon-btn danger rb-x" onClick={(e) => { e.stopPropagation(); verwijder(b.id); }} title="Blok verwijderen">×</button>}
</div> </div>
<div className="small">Doel: {b.doel_naam} <span className="muted">({b.doel_type})</span></div> {(b.locatie_naam || b.lokaal_naam || b.begeleiders_namen.length > 0) && (
{b.locatie_naam && <div className="small">Locatie: {b.locatie_naam}</div>} <div className="vk-meta">
{b.lokaal_naam && <div className="small">Lokaal: {b.lokaal_naam}</div>} {b.locatie_naam && <span className="vk-tag">📍 {b.locatie_naam}</span>}
{b.lokaal_naam && <span className="vk-tag">🚪 {b.lokaal_naam}</span>}
{b.begeleiders_namen.length > 0 && ( {b.begeleiders_namen.length > 0 && (
<div className="small muted">Begeleiding: {b.begeleiders_namen.map((x, idx) => ( <span className="vk-tag">👤 {b.begeleiders_namen.map((x, idx) => (
<span key={x.id} className={persoonAfwezig(x.id, iso) ? "afwezig" : ""}>{idx > 0 ? ", " : ""}{x.naam}{rolAfk(x.id) ? ` (${rolAfk(x.id)})` : ""}</span> <span key={x.id} className={persoonAfwezig(x.id, iso) ? "afwezig" : ""}>{idx > 0 ? ", " : ""}{x.naam}{rolAfk(x.id) ? ` (${rolAfk(x.id)})` : ""}</span>
))}</div> ))}</span>
)}
</div>
)} )}
{stageNoot(b)} {stageNoot(b)}
{kal && <div className="small rb-status">Vervalt - {kal.type_label}</div>} {kal && <div className="small rb-status">Vervalt {kal.type_label}</div>}
{!kal && uit && uit.vervalt && <div className="small rb-status">Vervalt {uit.opmerking ? `(${uit.opmerking})` : ""}{edit && <button type="button" className="icon-btn" title="Herstellen" onClick={(e) => { e.stopPropagation(); herstel(uit.id); }}></button>}</div>} {!kal && uit && uit.vervalt && <div className="small rb-status">Vervalt {uit.opmerking ? `(${uit.opmerking})` : ""}{edit && <button type="button" className="icon-btn" title="Herstellen" onClick={(e) => { e.stopPropagation(); herstel(uit.id); }}></button>}</div>}
{!kal && uit && !uit.vervalt && uit.vervanger_naam && <div className="small rb-verv">Vervanger: {uit.vervanger_naam}</div>} {!kal && uit && !uit.vervalt && uit.vervanger_naam && <div className="small rb-verv">Vervanger: {uit.vervanger_naam}</div>}
{edit && !kal && !uit && ( {edit && !kal && !uit && (
@ -2338,33 +2344,15 @@ export function RoosterScherm({ mode }) {
</div> </div>
); );
} }
function renderCompactBlok(b, iso, kal) { // Klas-/doelgroepkop boven de vakken: altijd duidelijk in beeld.
const uit = uitzOp(b.id, iso); const doelIcoon = (t) => (t === "leerling" ? "🧒" : t === "leerplein" ? "🏫" : "👥");
const vervallen = !!kal || (uit && uit.vervalt); const klasKop = (naam, sub, count, icoon) => (
return ( <div className="split-header">
<div <span className="split-icon">{icoon}</span>
className={(vervallen ? "split-vak vervallen" : "split-vak") + (edit ? " klikbaar" : "") + (isOpenBlok(b) ? " open" : "") + (conflictInfo.blokIds.has(b.id) ? " in-conflict" : "")} <div className="split-title"><strong>{naam}</strong><span>{sub}</span></div>
key={b.id} {count != null && <span className="split-count">{count}</span>}
style={{ borderLeftColor: b.activiteit_kleur }}
onClick={edit ? () => toggleOpenBlok(b) : undefined}
title={edit ? "Klik om te bewerken" : undefined}
>
<div className="split-vak-kop">
{edit && <span className="rb-greep" onPointerDown={(e) => startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot"></span>}
{b.activiteit_pictogram && <img src={b.activiteit_pictogram} alt="" className="rb-picto" />}<strong>{b.activiteit_icoon || vakIcoon(b.activiteit_naam)} {b.activiteit_naam}</strong>
{edit && <button type="button" className="icon-btn" onClick={(e) => { e.stopPropagation(); toggleOpenBlok(b); }} title="Bewerken"></button>}
{edit && <button type="button" className="icon-btn danger" onClick={(e) => { e.stopPropagation(); verwijder(b.id); }} title="Blok verwijderen">×</button>}
</div>
<div className="split-vak-meta">
{b.locatie_naam && <span>{b.locatie_naam}</span>}
{b.lokaal_naam && <span>{b.lokaal_naam}</span>}
{b.begeleiders_namen.length > 0 && <span>{b.begeleiders_namen.map((x) => x.naam).join(", ")}</span>}
{vervallen && <span className="rb-status">Vervalt</span>}
</div>
{stageNoot(b)}
</div> </div>
); );
}
function renderSlotBlokken(slotBlokken, iso, kal) { function renderSlotBlokken(slotBlokken, iso, kal) {
const losse = []; const losse = [];
const perGroep = new Map(); const perGroep = new Map();
@ -2374,27 +2362,38 @@ export function RoosterScherm({ mode }) {
if (!perGroep.has(gid)) perGroep.set(gid, []); if (!perGroep.has(gid)) perGroep.set(gid, []);
perGroep.get(gid).push(b); perGroep.get(gid).push(b);
}); });
const out = losse.map((b) => renderRoosterBlok(b, iso, kal)); const out = [];
// Losse doelen (individuele leerling e.d.): per doel een klas-kop + vakken.
const losPerDoel = new Map();
losse.forEach((b) => {
const k = `${b.doel_type}:${b.doel_naam}`;
if (!losPerDoel.has(k)) losPerDoel.set(k, []);
losPerDoel.get(k).push(b);
});
losPerDoel.forEach((blks, k) => {
const eerste = blks[0];
out.push(
<div className="blok-splitsing enkel" key={`los-${k}`}>
{klasKop(eerste.doel_naam, eerste.doel_type, blks.length > 1 ? blks.length : null, doelIcoon(eerste.doel_type))}
<div className="split-vakken">{blks.map((b) => renderVakKaart(b, iso, kal))}</div>
</div>
);
});
if (scopeType === "leerplein" && scopeId && perGroep.size > 0) { if (scopeType === "leerplein" && scopeId && perGroep.size > 0) {
const leerplein = leerpleinen.find((l) => l.id === Number(scopeId)); const leerplein = leerpleinen.find((l) => l.id === Number(scopeId));
const kolommen = [...perGroep.entries()]; const kolommen = [...perGroep.entries()];
out.push( out.push(
<div className="blok-splitsing leerplein-splitsing" key={`leerplein-${scopeId}`}> <div className="blok-splitsing leerplein-splitsing" key={`leerplein-${scopeId}`}>
<div className="split-header"> {klasKop(leerplein ? leerplein.naam : BOVENGROEP_LABEL, `${BOVENGROEP_LABEL} → groepen`, kolommen.length, "🏫")}
<span className="split-icon"></span>
<div className="split-title">
<strong>{leerplein ? leerplein.naam : BOVENGROEP_LABEL}</strong>
<span>{BOVENGROEP_LABEL} groepen</span>
</div>
<span className="split-count">{kolommen.length}</span>
</div>
<div className="split-kolommen split-vertakking"> <div className="split-kolommen split-vertakking">
{kolommen.map(([gid, groepBlokken]) => { {kolommen.map(([gid, groepBlokken]) => {
const groep = groepen.find((g) => g.id === gid); const groep = groepen.find((g) => g.id === gid);
return ( return (
<div className="split-kolom" key={gid}> <div className="split-kolom" key={gid}>
<div className="split-kop split-node-kop">{groep ? groep.naam : "Groep"}</div> <div className="split-kop split-node-kop">{groep ? groep.naam : "Groep"}</div>
<div className="split-vakken">{groepBlokken.map((b) => renderCompactBlok(b, iso, kal))}</div> <div className="split-vakken">{groepBlokken.map((b) => renderVakKaart(b, iso, kal))}</div>
</div> </div>
); );
})} })}
@ -2403,31 +2402,31 @@ export function RoosterScherm({ mode }) {
); );
return out; return out;
} }
perGroep.forEach((groepBlokken, gid) => { perGroep.forEach((groepBlokken, gid) => {
const groep = groepen.find((g) => g.id === gid);
const groepNaam = groep ? groep.naam : "Groep";
const heeftSplitsing = groepBlokken.some((b) => b.subgroep); const heeftSplitsing = groepBlokken.some((b) => b.subgroep);
if (!heeftSplitsing) { if (!heeftSplitsing) {
groepBlokken.forEach((b) => out.push(renderRoosterBlok(b, iso, kal))); out.push(
<div className="blok-splitsing enkel" key={`groep-${gid}`}>
{klasKop(groepNaam, "groep", groepBlokken.length > 1 ? groepBlokken.length : null, "👥")}
<div className="split-vakken">{groepBlokken.map((b) => renderVakKaart(b, iso, kal))}</div>
</div>
);
return; return;
} }
const groep = groepen.find((g) => g.id === gid);
const heleGroep = groepBlokken.filter((b) => b.groep === gid); const heleGroep = groepBlokken.filter((b) => b.groep === gid);
const subIds = [...new Set(groepBlokken.filter((b) => b.subgroep).map((b) => b.subgroep))]; const subIds = [...new Set(groepBlokken.filter((b) => b.subgroep).map((b) => b.subgroep))];
const kolomAantal = subIds.length + (heleGroep.length > 0 ? 1 : 0); const kolomAantal = subIds.length + (heleGroep.length > 0 ? 1 : 0);
out.push( out.push(
<div className="blok-splitsing" key={`split-${gid}`}> <div className="blok-splitsing" key={`split-${gid}`}>
<div className="split-header"> {klasKop(groepNaam, "groep → subgroepen", kolomAantal, "👥")}
<span className="split-icon"></span>
<div className="split-title">
<strong>{groep ? groep.naam : "Groep"}</strong>
<span>Groep subgroepen</span>
</div>
<span className="split-count">{kolomAantal}</span>
</div>
<div className="split-kolommen split-vertakking"> <div className="split-kolommen split-vertakking">
{heleGroep.length > 0 && ( {heleGroep.length > 0 && (
<div className="split-kolom"> <div className="split-kolom">
<div className="split-kop split-node-kop">Hele groep</div> <div className="split-kop split-node-kop">Hele groep</div>
<div className="split-vakken">{heleGroep.map((b) => renderCompactBlok(b, iso, kal))}</div> <div className="split-vakken">{heleGroep.map((b) => renderVakKaart(b, iso, kal))}</div>
</div> </div>
)} )}
{subIds.map((sid) => { {subIds.map((sid) => {
@ -2436,7 +2435,7 @@ export function RoosterScherm({ mode }) {
return ( return (
<div className="split-kolom" key={sid}> <div className="split-kolom" key={sid}>
<div className="split-kop split-node-kop">{sub ? sub.naam : "Subgroep"}</div> <div className="split-kop split-node-kop">{sub ? sub.naam : "Subgroep"}</div>
<div className="split-vakken">{blocks.map((b) => renderCompactBlok(b, iso, kal))}</div> <div className="split-vakken">{blocks.map((b) => renderVakKaart(b, iso, kal))}</div>
</div> </div>
); );
})} })}

View file

@ -219,6 +219,20 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
.split-vak-kop { display: flex; align-items: center; gap: 5px; } .split-vak-kop { display: flex; align-items: center; gap: 5px; }
.split-vak-kop strong { min-width: 0; flex: 1; font-size: 12px; } .split-vak-kop strong { min-width: 0; flex: 1; font-size: 12px; }
.split-vak-meta { display: flex; flex-direction: column; gap: 1px; color: var(--muted); font-size: 11px; margin-top: 2px; } .split-vak-meta { display: flex; flex-direction: column; gap: 1px; color: var(--muted); font-size: 11px; margin-top: 2px; }
/* Eén uniform vak-kaartje (overal gelijk) + klas-kop-container */
.blok-splitsing.enkel { background: linear-gradient(180deg, #eff6ff 0, #f8fbff 40px, #fff 100%); }
.blok-splitsing.enkel .split-header { margin-bottom: 8px; }
.vak-kaart { position: relative; border: 1px solid var(--line); border-left: 4px solid var(--brand); border-radius: 7px; background: #fff; padding: 7px 8px; cursor: default; box-shadow: 0 1px 0 rgba(31,41,55,.03); touch-action: none; }
.vak-kaart.klikbaar { cursor: pointer; }
.vak-kaart.klikbaar:hover, .vak-kaart.open { box-shadow: 0 0 0 2px var(--brand-soft); }
.vak-kaart.vervallen { opacity: .55; }
.vak-kaart.vervallen .vk-naam { text-decoration: line-through; }
.vk-kop { display: flex; align-items: center; gap: 5px; }
.vk-naam { min-width: 0; flex: 1; font-size: 13px; overflow: hidden; text-overflow: ellipsis; }
.vk-meta { display: flex; flex-wrap: wrap; gap: 4px 6px; margin-top: 5px; }
.vk-tag { display: inline-flex; align-items: center; gap: 3px; background: var(--bg); color: var(--muted); border-radius: 999px; padding: 2px 8px; font-size: 11px; line-height: 1.5; max-width: 100%; }
@media print { .blok-splitsing.enkel { background: #fff; } }
.klas-strip { display: flex; flex-wrap: wrap; gap: 6px; flex: 1 1 auto; min-width: 0; } .klas-strip { display: flex; flex-wrap: wrap; gap: 6px; flex: 1 1 auto; min-width: 0; }
/* --- Blok-bewerker --- */ /* --- Blok-bewerker --- */
@ -365,7 +379,8 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
/* Stage in het rooster + stagelocatie-beheer */ /* Stage in het rooster + stagelocatie-beheer */
.rb-stage { color: #b45309; font-weight: 600; } .rb-stage { color: #b45309; font-weight: 600; }
.rooster-blok.vervallen .rb-stage, .rooster-blok.vervallen .rb-stage,
.split-vak.vervallen .rb-stage { opacity: .7; } .split-vak.vervallen .rb-stage,
.vak-kaart.vervallen .rb-stage { opacity: .7; }
.subsectie { margin-top: 28px; padding-top: 18px; border-top: 1px solid var(--line); } .subsectie { margin-top: 28px; padding-top: 18px; border-top: 1px solid var(--line); }
.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; }
@ -535,4 +550,4 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
.conflict-lijst li { padding: 4px 0 4px 16px; position: relative; color: #7f1d1d; font-size: 14px; } .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::before { content: "•"; position: absolute; left: 2px; }
.conflict-lijst li.waarschuwing { color: #92400e; } .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); } .rooster-blok.in-conflict, .split-vak.in-conflict, .vak-kaart.in-conflict { outline: 2px solid #ef4444; outline-offset: 1px; box-shadow: 0 0 0 4px rgba(239,68,68,.15); }