diff --git a/ROADMAP.md b/ROADMAP.md index 61f88ad..45e9f81 100644 --- a/ROADMAP.md +++ b/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 diff --git a/frontend/src/pages.jsx b/frontend/src/pages.jsx index 80e26e6..05b284b 100644 --- a/frontend/src/pages.jsx +++ b/frontend/src/pages.jsx @@ -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 (
toggleOpenBlok(b) : 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) && ( + + )} {printAan && schooljaar && (edit ? tab === "week" : true) && ( )} @@ -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 (

Perioderooster

@@ -2925,6 +2976,7 @@ export function PeriodeRoosterPage() { {conflictInfo.meldingen.length ? `⚠ ${conflictInfo.meldingen.length} conflict(en)` : "✓ geen conflicten"} +
@@ -2961,7 +3013,7 @@ export function PeriodeRoosterPage() { return ( {cellblk.map((b) => ( -
+
{b.activiteit_icoon || vakIcoon(b.activiteit_naam)} {b.activiteit_naam} @@ -2995,25 +3047,43 @@ export function PeriodeRoosterPage() { {conflictInfo.meldingen.length > 0 &&
Let op: er zijn nog {conflictInfo.meldingen.length} conflict(en) in de opzet. Je kunt toch inladen, maar los ze bij voorkeur eerst op.
}
{periodeKeuze === "_new" && ( <> - - - + + + )}
- {!preview && ( + {!preview && !wisInfo && (
- + + {periodeKeuze !== "_new" && }
)} + {wisInfo && ( + <> +
+

{wisInfo.blokken.length} periode-blok(ken) ({wisInfo.van} – {wisInfo.tot}) worden uit het rooster verwijderd. Vaste (altijd geldige) blokken blijven staan.

+ {wisInfo.blokken.length > 0 && ( +
    + {wisInfo.blokken.slice(0, 30).map((b) =>
  • {b.doel_naam} · {b.activiteit_naam} ({b.tijdslot_label})
  • )} + {wisInfo.blokken.length > 30 &&
  • …en nog {wisInfo.blokken.length - 30}
  • } +
+ )} +
+
+ + +
+ + )} {preview && ( <>
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index ac3a5ac..96fb6db 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -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; }