From bfcb8b0d42a6be59e4ecd4d42b725869af317558 Mon Sep 17 00:00:00 2001
From: bes-r <27369668+bes-r@users.noreply.github.com>
Date: Fri, 19 Jun 2026 23:31:00 +0200
Subject: [PATCH] perioderooster: hele periode wissen + weergaveoptie vak
volledig in vakkleur
---
ROADMAP.md | 11 ++++++
frontend/src/pages.jsx | 88 ++++++++++++++++++++++++++++++++++++-----
frontend/src/styles.css | 9 +++++
3 files changed, 99 insertions(+), 9 deletions(-)
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; }
|