perioderooster: hele periode wissen + weergaveoptie vak volledig in vakkleur
This commit is contained in:
parent
e435ddd961
commit
bfcb8b0d42
3 changed files with 99 additions and 9 deletions
11
ROADMAP.md
11
ROADMAP.md
|
|
@ -38,6 +38,17 @@ volgt.
|
|||
voor een volgende periode. Sjabloonblokken tellen niet mee in het echte
|
||||
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
|
||||
|
||||
- [~] **Alle klassen verwerkbaar in het rooster**, overzichtelijk gemaakt — een
|
||||
|
|
|
|||
|
|
@ -1607,6 +1607,19 @@ export function blokInScope(blok, scope, ctx) {
|
|||
// 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).
|
||||
// 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) {
|
||||
const blokIds = new Set();
|
||||
const teksten = new Map(); // tekst -> ernst (dedupe)
|
||||
|
|
@ -2057,6 +2070,8 @@ export function RoosterScherm({ mode }) {
|
|||
const [genOpen, setGenOpen] = useState(false);
|
||||
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 [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(() => {
|
||||
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);
|
||||
return (
|
||||
<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}
|
||||
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}
|
||||
title={edit ? "Klik om te bewerken" : undefined}
|
||||
>
|
||||
|
|
@ -2485,6 +2500,9 @@ export function RoosterScherm({ mode }) {
|
|||
{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>
|
||||
)}
|
||||
{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) && (
|
||||
<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 [applyBezig, setApplyBezig] = useState(false);
|
||||
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(() => {
|
||||
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");
|
||||
setNieuwP({ start_datum: "", eind_datum: "", nummer: volgend });
|
||||
setPreview(null);
|
||||
setWisInfo(null);
|
||||
setKlaarMelding(null);
|
||||
setVoltooiOpen(true);
|
||||
}
|
||||
|
|
@ -2911,6 +2933,35 @@ export function PeriodeRoosterPage() {
|
|||
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 (
|
||||
<section className="weekmaker">
|
||||
<h1>Perioderooster</h1>
|
||||
|
|
@ -2925,6 +2976,7 @@ export function PeriodeRoosterPage() {
|
|||
</select>
|
||||
</label>
|
||||
<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 & periode inladen</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -2961,7 +3013,7 @@ export function PeriodeRoosterPage() {
|
|||
return (
|
||||
<td key={g.id} className="wm-cel">
|
||||
{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">
|
||||
<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>
|
||||
|
|
@ -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>}
|
||||
<div className="editor-grid">
|
||||
<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>)}
|
||||
<option value="_new">+ nieuwe periode aanmaken</option>
|
||||
</select>
|
||||
</label>
|
||||
{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">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">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">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); 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); setWisInfo(null); }} /></label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!preview && (
|
||||
{!preview && !wisInfo && (
|
||||
<div className="editor-actions">
|
||||
<button type="button" className="btn primary" onClick={maakPreview}>Controleren</button>
|
||||
<button type="button" className="btn primary" onClick={maakPreview}>Controleren & 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>
|
||||
</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 && (
|
||||
<>
|
||||
<div className="wm-preview">
|
||||
|
|
|
|||
|
|
@ -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:hover { background: var(--bg); }
|
||||
.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); }
|
||||
|
||||
.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-cel:hover .wm-add { opacity: 1; }
|
||||
.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 .conflict-lijst { background: #fff7ed; border: 1px solid #fed7aa; border-radius: 8px; padding: 8px 12px; margin-top: 6px; }
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue