uniforme vak-/klasweergave: klas-kop met vak eronder, één kaartontwerp
This commit is contained in:
parent
2600db7f69
commit
066fe23250
2 changed files with 77 additions and 63 deletions
|
|
@ -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.begeleiders_namen.length > 0 && (
|
{b.lokaal_naam && <span className="vk-tag">🚪 {b.lokaal_naam}</span>}
|
||||||
<div className="small muted">Begeleiding: {b.begeleiders_namen.map((x, idx) => (
|
{b.begeleiders_namen.length > 0 && (
|
||||||
<span key={x.id} className={persoonAfwezig(x.id, iso) ? "afwezig" : ""}>{idx > 0 ? ", " : ""}{x.naam}{rolAfk(x.id) ? ` (${rolAfk(x.id)})` : ""}</span>
|
<span className="vk-tag">👤 {b.begeleiders_namen.map((x, idx) => (
|
||||||
))}</div>
|
<span key={x.id} className={persoonAfwezig(x.id, iso) ? "afwezig" : ""}>{idx > 0 ? ", " : ""}{x.naam}{rolAfk(x.id) ? ` (${rolAfk(x.id)})` : ""}</span>
|
||||||
|
))}</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 }}
|
</div>
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -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); }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue