perioderooster: hele periode wissen + weergaveoptie vak volledig in vakkleur

This commit is contained in:
bes-r 2026-06-19 23:31:00 +02:00
parent e435ddd961
commit bfcb8b0d42
3 changed files with 99 additions and 9 deletions

View file

@ -38,6 +38,17 @@ volgt.
voor een volgende periode. Sjabloonblokken tellen niet mee in het echte voor een volgende periode. Sjabloonblokken tellen niet mee in het echte
rooster/conflicten/uren/auto-rooster (`Roosterblok.sjabloon`). rooster/conflicten/uren/auto-rooster (`Roosterblok.sjabloon`).
## Fase 6 — Weergave & beheer
- [x] **Vakkleur als vlak** (toggle 🎨, onthouden per browser): vak volledig in de
activiteitskleur i.p.v. een streepje, met automatisch leesbare tekstkleur
(zwart/wit op basis van helderheid). Werkt in Rooster/Roostermaker en
Perioderooster.
- [x] **Hele periode wissen** in het Perioderooster: in de voltooien-dialog een
bestaande periode kiezen → "Deze periode wissen" verwijdert (na preview +
bevestiging) alle periode-gebonden blokken binnen die periode; vaste
(altijd geldige) blokken blijven staan.
## Fase 6 — Overzicht & navigatie ## Fase 6 — Overzicht & navigatie
- [~] **Alle klassen verwerkbaar in het rooster**, overzichtelijk gemaakt — een - [~] **Alle klassen verwerkbaar in het rooster**, overzichtelijk gemaakt — een

View file

@ -1607,6 +1607,19 @@ export function blokInScope(blok, scope, ctx) {
// Bereken conflicten direct uit de geladen blokken (zelfde tijdslot): begeleider // Bereken conflicten direct uit de geladen blokken (zelfde tijdslot): begeleider
// dubbel, lokaal dubbel of een leerling dubbel ingeroosterd. Houdt rekening met // dubbel, lokaal dubbel of een leerling dubbel ingeroosterd. Houdt rekening met
// geldigheidsperiodes (blokken die elkaar niet in de tijd overlappen botsen niet). // geldigheidsperiodes (blokken die elkaar niet in de tijd overlappen botsen niet).
// Leesbare tekstkleur (zwart of wit) op een gegeven achtergrondkleur, op basis
// van de waargenomen helderheid zo blijft de tekst altijd genoeg contrast houden.
export function tekstOpKleur(hex) {
if (!hex || typeof hex !== "string") return "";
const m = hex.replace("#", "").trim();
const v = m.length === 3 ? m.split("").map((c) => c + c).join("") : m;
if (v.length < 6) return "";
const r = parseInt(v.slice(0, 2), 16), g = parseInt(v.slice(2, 4), 16), b = parseInt(v.slice(4, 6), 16);
if ([r, g, b].some(Number.isNaN)) return "";
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return lum > 0.6 ? "#16181d" : "#ffffff";
}
export function berekenRoosterConflicten(blokken, ctx, tijdsloten) { export function berekenRoosterConflicten(blokken, ctx, tijdsloten) {
const blokIds = new Set(); const blokIds = new Set();
const teksten = new Map(); // tekst -> ernst (dedupe) const teksten = new Map(); // tekst -> ernst (dedupe)
@ -2057,6 +2070,8 @@ export function RoosterScherm({ mode }) {
const [genOpen, setGenOpen] = useState(false); const [genOpen, setGenOpen] = useState(false);
const [weergave, setWeergaveState] = useState(() => { try { return localStorage.getItem("rw_weergave") || "kolom"; } catch { return "kolom"; } }); const [weergave, setWeergaveState] = useState(() => { try { return localStorage.getItem("rw_weergave") || "kolom"; } catch { return "kolom"; } });
const setWeergave = (v) => { setWeergaveState(v); try { localStorage.setItem("rw_weergave", v); } catch { /* niet beschikbaar */ } }; const setWeergave = (v) => { setWeergaveState(v); try { localStorage.setItem("rw_weergave", v); } catch { /* niet beschikbaar */ } };
const [vakkleurVlak, setVakkleurState] = useState(() => { try { return localStorage.getItem("rw_vakkleur") === "vlak"; } catch { return false; } });
const setVakkleur = (v) => { setVakkleurState(v); try { localStorage.setItem("rw_vakkleur", v ? "vlak" : "streep"); } catch { /* niet beschikbaar */ } };
useEffect(() => { useEffect(() => {
listResource("schooljaren").then((j) => { setSchooljaren(j); if (j.length) setSchooljaar((s) => s || String(j[0].id)); }).catch((e) => setError(e.message)); listResource("schooljaren").then((j) => { setSchooljaren(j); if (j.length) setSchooljaar((s) => s || String(j[0].id)); }).catch((e) => setError(e.message));
@ -2321,9 +2336,9 @@ export function RoosterScherm({ mode }) {
const vervallen = !!kal || (uit && uit.vervalt); const vervallen = !!kal || (uit && uit.vervalt);
return ( return (
<div <div
className={"vak-kaart" + (vervallen ? " vervallen" : "") + (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" : "") + (vakkleurVlak ? " kleur-vlak" : "")}
key={b.id} key={b.id}
style={{ borderLeftColor: b.activiteit_kleur }} style={vakkleurVlak ? { borderLeftColor: b.activiteit_kleur, background: b.activiteit_kleur, color: tekstOpKleur(b.activiteit_kleur) } : { 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}
> >
@ -2485,6 +2500,9 @@ export function RoosterScherm({ mode }) {
{schooljaar && (edit ? tab === "week" : true) && ( {schooljaar && (edit ? tab === "week" : true) && (
<button className="btn icon-label weergave-knop" title={weergave === "kolom" ? "Toon dagen onder elkaar" : "Toon dagen als kolommen"} onClick={() => setWeergave(weergave === "kolom" ? "lijst" : "kolom")}>{weergave === "kolom" ? "☰" : "▦"} <span>{weergave === "kolom" ? "Dagen onder elkaar" : "Kolommen"}</span></button> <button className="btn icon-label weergave-knop" title={weergave === "kolom" ? "Toon dagen onder elkaar" : "Toon dagen als kolommen"} onClick={() => setWeergave(weergave === "kolom" ? "lijst" : "kolom")}>{weergave === "kolom" ? "☰" : "▦"} <span>{weergave === "kolom" ? "Dagen onder elkaar" : "Kolommen"}</span></button>
)} )}
{schooljaar && (edit ? tab === "week" : true) && (
<button className="btn icon-label weergave-knop" title={vakkleurVlak ? "Toon kleur als streepje" : "Toon vak volledig in vakkleur"} onClick={() => setVakkleur(!vakkleurVlak)}>🎨 <span>{vakkleurVlak ? "Kleurstreep" : "Vakkleur"}</span></button>
)}
{printAan && schooljaar && (edit ? tab === "week" : true) && ( {printAan && schooljaar && (edit ? tab === "week" : true) && (
<button className="btn print-knop icon-label" title="Rooster afdrukken" onClick={() => window.print()}>🖨 <span>Print</span></button> <button className="btn print-knop icon-label" title="Rooster afdrukken" onClick={() => window.print()}>🖨 <span>Print</span></button>
)} )}
@ -2792,6 +2810,9 @@ export function PeriodeRoosterPage() {
const [preview, setPreview] = useState(null); // {toevoegen:[], verwijderen:[]} const [preview, setPreview] = useState(null); // {toevoegen:[], verwijderen:[]}
const [applyBezig, setApplyBezig] = useState(false); const [applyBezig, setApplyBezig] = useState(false);
const [klaarMelding, setKlaarMelding] = useState(null); const [klaarMelding, setKlaarMelding] = useState(null);
const [wisInfo, setWisInfo] = useState(null); // {van, tot, blokken}
const [vakkleurVlak, setVakkleurState] = useState(() => { try { return localStorage.getItem("rw_vakkleur") === "vlak"; } catch { return false; } });
const setVakkleur = (v) => { setVakkleurState(v); try { localStorage.setItem("rw_vakkleur", v ? "vlak" : "streep"); } catch { /* niet beschikbaar */ } };
useEffect(() => { useEffect(() => {
listResource("schooljaren").then((j) => { setSchooljaren(j); if (j.length) setSchooljaar(String(j[0].id)); }).catch((e) => setError(e.message)); listResource("schooljaren").then((j) => { setSchooljaren(j); if (j.length) setSchooljaar(String(j[0].id)); }).catch((e) => setError(e.message));
@ -2861,6 +2882,7 @@ export function PeriodeRoosterPage() {
setPeriodeKeuze(periodes.length ? String(periodes[0].id) : "_new"); setPeriodeKeuze(periodes.length ? String(periodes[0].id) : "_new");
setNieuwP({ start_datum: "", eind_datum: "", nummer: volgend }); setNieuwP({ start_datum: "", eind_datum: "", nummer: volgend });
setPreview(null); setPreview(null);
setWisInfo(null);
setKlaarMelding(null); setKlaarMelding(null);
setVoltooiOpen(true); setVoltooiOpen(true);
} }
@ -2911,6 +2933,35 @@ export function PeriodeRoosterPage() {
setApplyBezig(false); setApplyBezig(false);
} }
// ---- Hele periode uit het echte rooster wissen ----
async function wisPeriodeControle() {
const { van, tot } = gekozenPeriodeDatums();
if (!van || !tot) { setError("Kies een periode om te wissen."); return; }
try {
const live = await listResource("roosterblokken", `?schooljaar=${schooljaar}`); // sjabloon=False
// Alleen periode-gebonden blokken die binnen deze periode vallen; vaste
// (altijd geldige) blokken zonder datum blijven staan.
const teWissen = live.filter((b) => b.geldig_van && b.geldig_tot && b.geldig_van >= van && b.geldig_tot <= tot);
setPreview(null);
setWisInfo({ van, tot, blokken: teWissen });
setError(null);
} catch (e) { setError(e.message); }
}
async function bevestigWis() {
if (!wisInfo) return;
setApplyBezig(true);
try {
for (const b of wisInfo.blokken) {
await deleteResource("roosterblokken", b.id);
}
const n = wisInfo.blokken.length;
setWisInfo(null);
setVoltooiOpen(false);
setKlaarMelding(`Periode geleegd: ${n} blok(ken) uit het rooster verwijderd. Het sjabloon blijft bewaard.`);
} catch (e) { setError(e.message); }
setApplyBezig(false);
}
return ( return (
<section className="weekmaker"> <section className="weekmaker">
<h1>Perioderooster</h1> <h1>Perioderooster</h1>
@ -2925,6 +2976,7 @@ export function PeriodeRoosterPage() {
</select> </select>
</label> </label>
<span className={conflictInfo.meldingen.length ? "wm-status fout" : "wm-status ok"}>{conflictInfo.meldingen.length ? `${conflictInfo.meldingen.length} conflict(en)` : "✓ geen conflicten"}</span> <span className={conflictInfo.meldingen.length ? "wm-status fout" : "wm-status ok"}>{conflictInfo.meldingen.length ? `${conflictInfo.meldingen.length} conflict(en)` : "✓ geen conflicten"}</span>
<button className="btn icon-label" title={vakkleurVlak ? "Toon kleur als streepje" : "Toon vak volledig in vakkleur"} onClick={() => setVakkleur(!vakkleurVlak)}>🎨 <span>{vakkleurVlak ? "Kleurstreep" : "Vakkleur"}</span></button>
<button className="btn primary" disabled={!schooljaar || blokken.length === 0} onClick={openVoltooien}>Voltooien &amp; periode inladen</button> <button className="btn primary" disabled={!schooljaar || blokken.length === 0} onClick={openVoltooien}>Voltooien &amp; periode inladen</button>
</div> </div>
@ -2961,7 +3013,7 @@ export function PeriodeRoosterPage() {
return ( return (
<td key={g.id} className="wm-cel"> <td key={g.id} className="wm-cel">
{cellblk.map((b) => ( {cellblk.map((b) => (
<div key={b.id} className={"wm-vak" + (conflictInfo.blokIds.has(b.id) ? " in-conflict" : "")} style={{ borderLeftColor: b.activiteit_kleur }}> <div key={b.id} className={"wm-vak" + (conflictInfo.blokIds.has(b.id) ? " in-conflict" : "") + (vakkleurVlak ? " kleur-vlak" : "")} style={vakkleurVlak ? { borderLeftColor: b.activiteit_kleur, background: b.activiteit_kleur, color: tekstOpKleur(b.activiteit_kleur) } : { borderLeftColor: b.activiteit_kleur }}>
<div className="wm-vak-kop"> <div className="wm-vak-kop">
<span className="wm-vak-naam">{b.activiteit_icoon || vakIcoon(b.activiteit_naam)} {b.activiteit_naam}</span> <span className="wm-vak-naam">{b.activiteit_icoon || vakIcoon(b.activiteit_naam)} {b.activiteit_naam}</span>
<button type="button" className="icon-btn danger" title="Verwijderen" onClick={() => verwijder(b.id)}>×</button> <button type="button" className="icon-btn danger" title="Verwijderen" onClick={() => verwijder(b.id)}>×</button>
@ -2995,25 +3047,43 @@ export function PeriodeRoosterPage() {
{conflictInfo.meldingen.length > 0 && <div className="banner waarschuwing">Let op: er zijn nog {conflictInfo.meldingen.length} conflict(en) in de opzet. Je kunt toch inladen, maar los ze bij voorkeur eerst op.</div>} {conflictInfo.meldingen.length > 0 && <div className="banner waarschuwing">Let op: er zijn nog {conflictInfo.meldingen.length} conflict(en) in de opzet. Je kunt toch inladen, maar los ze bij voorkeur eerst op.</div>}
<div className="editor-grid"> <div className="editor-grid">
<label className="veld-kol"><span className="veld-kop">Periode</span> <label className="veld-kol"><span className="veld-kop">Periode</span>
<select value={periodeKeuze} onChange={(e) => { setPeriodeKeuze(e.target.value); setPreview(null); }}> <select value={periodeKeuze} onChange={(e) => { setPeriodeKeuze(e.target.value); setPreview(null); setWisInfo(null); }}>
{periodes.map((p) => <option key={p.id} value={p.id}>{p.naam} ({p.start_datum} {p.eind_datum})</option>)} {periodes.map((p) => <option key={p.id} value={p.id}>{p.naam} ({p.start_datum} {p.eind_datum})</option>)}
<option value="_new">+ nieuwe periode aanmaken</option> <option value="_new">+ nieuwe periode aanmaken</option>
</select> </select>
</label> </label>
{periodeKeuze === "_new" && ( {periodeKeuze === "_new" && (
<> <>
<label className="veld-kol"><span className="veld-kop">Periodenummer</span><input type="number" min="1" value={nieuwP.nummer || ""} onChange={(e) => { setNieuwP({ ...nieuwP, nummer: Number(e.target.value) }); setPreview(null); }} /></label> <label className="veld-kol"><span className="veld-kop">Periodenummer</span><input type="number" min="1" value={nieuwP.nummer || ""} onChange={(e) => { setNieuwP({ ...nieuwP, nummer: Number(e.target.value) }); setPreview(null); setWisInfo(null); }} /></label>
<label className="veld-kol"><span className="veld-kop">Van</span><input type="date" value={nieuwP.start_datum} onChange={(e) => { setNieuwP({ ...nieuwP, start_datum: e.target.value }); setPreview(null); }} /></label> <label className="veld-kol"><span className="veld-kop">Van</span><input type="date" value={nieuwP.start_datum} onChange={(e) => { setNieuwP({ ...nieuwP, start_datum: e.target.value }); setPreview(null); setWisInfo(null); }} /></label>
<label className="veld-kol"><span className="veld-kop">T/m</span><input type="date" value={nieuwP.eind_datum} onChange={(e) => { setNieuwP({ ...nieuwP, eind_datum: e.target.value }); setPreview(null); }} /></label> <label className="veld-kol"><span className="veld-kop">T/m</span><input type="date" value={nieuwP.eind_datum} onChange={(e) => { setNieuwP({ ...nieuwP, eind_datum: e.target.value }); setPreview(null); setWisInfo(null); }} /></label>
</> </>
)} )}
</div> </div>
{!preview && ( {!preview && !wisInfo && (
<div className="editor-actions"> <div className="editor-actions">
<button type="button" className="btn primary" onClick={maakPreview}>Controleren</button> <button type="button" className="btn primary" onClick={maakPreview}>Controleren &amp; inladen</button>
{periodeKeuze !== "_new" && <button type="button" className="btn danger" onClick={wisPeriodeControle}>Deze periode wissen</button>}
<button type="button" className="btn" onClick={() => setVoltooiOpen(false)}>Annuleren</button> <button type="button" className="btn" onClick={() => setVoltooiOpen(false)}>Annuleren</button>
</div> </div>
)} )}
{wisInfo && (
<>
<div className="wm-preview">
<p className="fout"><b>{wisInfo.blokken.length}</b> periode-blok(ken) ({wisInfo.van} {wisInfo.tot}) worden uit het rooster verwijderd. Vaste (altijd geldige) blokken blijven staan.</p>
{wisInfo.blokken.length > 0 && (
<ul className="conflict-lijst">
{wisInfo.blokken.slice(0, 30).map((b) => <li key={b.id} className="waarschuwing">{b.doel_naam} · {b.activiteit_naam} ({b.tijdslot_label})</li>)}
{wisInfo.blokken.length > 30 && <li className="muted">en nog {wisInfo.blokken.length - 30}</li>}
</ul>
)}
</div>
<div className="editor-actions">
<button type="button" className="btn danger" disabled={applyBezig || wisInfo.blokken.length === 0} onClick={bevestigWis}>{applyBezig ? "Bezig…" : `Wissen bevestigen (${wisInfo.blokken.length})`}</button>
<button type="button" className="btn" onClick={() => setWisInfo(null)} disabled={applyBezig}>Terug</button>
</div>
</>
)}
{preview && ( {preview && (
<> <>
<div className="wm-preview"> <div className="wm-preview">

View file

@ -100,6 +100,8 @@ input:focus, select:focus { outline: 2px solid var(--brand-soft); border-color:
.btn { font: inherit; padding: 9px 14px; border-radius: 8px; border: 1px solid var(--line); background: #fff; cursor: pointer; } .btn { font: inherit; padding: 9px 14px; border-radius: 8px; border: 1px solid var(--line); background: #fff; cursor: pointer; }
.btn:hover { background: var(--bg); } .btn:hover { background: var(--bg); }
.btn.primary { background: var(--brand); color: #fff; border-color: var(--brand); } .btn.primary { background: var(--brand); color: #fff; border-color: var(--brand); }
.btn.danger { background: #fee2e2; color: #b91c1c; border-color: #fca5a5; }
.btn.danger:hover { background: #fecaca; }
.btn.primary:hover { filter: brightness(1.05); } .btn.primary:hover { filter: brightness(1.05); }
.link { background: none; border: 0; color: var(--brand); cursor: pointer; padding: 2px 4px; font: inherit; } .link { background: none; border: 0; color: var(--brand); cursor: pointer; padding: 2px 4px; font: inherit; }
@ -552,6 +554,13 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
.wm-add { width: 100%; border: 1px dashed var(--line); background: transparent; color: var(--muted); border-radius: 7px; padding: 4px; cursor: pointer; font: inherit; font-size: 13px; opacity: .6; } .wm-add { width: 100%; border: 1px dashed var(--line); background: transparent; color: var(--muted); border-radius: 7px; padding: 4px; cursor: pointer; font: inherit; font-size: 13px; opacity: .6; }
.wm-cel:hover .wm-add { opacity: 1; } .wm-cel:hover .wm-add { opacity: 1; }
.wm-add:hover { border-color: var(--brand); color: var(--brand); background: var(--brand-soft); } .wm-add:hover { border-color: var(--brand); color: var(--brand); background: var(--brand-soft); }
/* Vak volledig in de vakkleur (tekstkleur wordt inline op contrast gezet) */
.vak-kaart.kleur-vlak .vk-naam { color: inherit; }
.vak-kaart.kleur-vlak .rb-greep, .vak-kaart.kleur-vlak .icon-btn { color: inherit; opacity: .85; }
.vak-kaart.kleur-vlak .rb-status, .vak-kaart.kleur-vlak .rb-verv { color: inherit; }
.wm-vak.kleur-vlak .wm-vak-naam { color: inherit; }
.wm-vak.kleur-vlak .wm-vak-meta, .wm-vak.kleur-vlak .muted { color: inherit; opacity: .85; }
.wm-vak.kleur-vlak .icon-btn { color: inherit; opacity: .85; }
.wm-preview { margin: 6px 0 10px; } .wm-preview { margin: 6px 0 10px; }
.wm-preview .conflict-lijst { background: #fff7ed; border: 1px solid #fed7aa; border-radius: 8px; padding: 8px 12px; margin-top: 6px; } .wm-preview .conflict-lijst { background: #fff7ed; border: 1px solid #fed7aa; border-radius: 8px; padding: 8px 12px; margin-top: 6px; }