From 6024a46b3538a6105196bb63a0d25163fb35d5f7 Mon Sep 17 00:00:00 2001 From: bes-r <27369668+bes-r@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:52:57 +0200 Subject: [PATCH] perioderooster: hernoemd, boven roostermaker, voltooien met periode + preview (Periode-model, sjabloon) --- ROADMAP.md | 18 +- backend/core/apps.py | 2 +- .../0010_periode_roosterblok_sjabloon.py | 36 ++++ backend/core/models.py | 37 ++++ backend/core/serializers.py | 25 ++- backend/core/urls.py | 1 + backend/core/views.py | 23 ++- backend/modules/autorooster/views.py | 2 +- backend/modules/stage/serializers.py | 4 +- backend/modules/uren/views.py | 2 +- frontend/src/App.jsx | 4 +- frontend/src/pages.jsx | 164 +++++++++++++++--- frontend/src/styles.css | 2 + 13 files changed, 277 insertions(+), 43 deletions(-) create mode 100644 backend/core/migrations/0010_periode_roosterblok_sjabloon.py diff --git a/ROADMAP.md b/ROADMAP.md index 3729bc4..61f88ad 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -25,14 +25,18 @@ volgt. leerling/medewerker enkel toevoegen óf in bulk via plakken/CSV (komma, puntkomma of tab; koprij optioneel) met preview; endpoint /api/personen-import/. -## Fase 6 — Weekroostermaker +## Fase 6 — Perioderooster -- [x] **Weekroostermaker** (`/weekroostermaker`, menu onder Rooster): matrix met - alle klassen als kolommen en de algemene tijdvakken als rijen. Vakken per - klas plaatsen in het **vaste basisweekrooster** (geen periode-koppeling; - `geldig_van/tot` leeg). Live conflictsignalering over alle klassen heen - (begeleider/lokaal dubbel) + "Weekrooster voltooien" als eindcontrole. - Periode-afwijkingen blijven in de Roostermaker. +- [x] **Perioderooster** (`/perioderooster`, menu onder Rooster, **boven** + Roostermaker): matrix met alle klassen als kolommen en de algemene + tijdvakken als rijen. Vakken per klas plaatsen in een **sjabloon**-opzet + (los van de kalender). Live conflictsignalering over alle klassen heen. +- [x] **Voltooien → periode inladen**: bij voltooien kies of maak je een + **Periode** (naam = schooljaar + nummer) met datumbereik; een **preview** + toont welke vakken geplaatst en welke overlappende bestaande blokken + verwijderd worden, daarna bevestigen. De sjabloon-opzet blijft bewaard + voor een volgende periode. Sjabloonblokken tellen niet mee in het echte + rooster/conflicten/uren/auto-rooster (`Roosterblok.sjabloon`). ## Fase 6 — Overzicht & navigatie diff --git a/backend/core/apps.py b/backend/core/apps.py index 704ba6c..83731ec 100644 --- a/backend/core/apps.py +++ b/backend/core/apps.py @@ -29,8 +29,8 @@ class CoreConfig(AppConfig): menu_items=( # Hoofdniveau: Rooster = weergave, Roostermaker = aanpasser. MenuItem("Rooster", "/rooster", icon="calendar", order=10, group="Rooster"), + MenuItem("Perioderooster", "/perioderooster", icon="calendar", order=11, group="Rooster"), MenuItem("Roostermaker", "/roostermaker", icon="calendar", order=12, group="Rooster"), - MenuItem("Weekroostermaker", "/weekroostermaker", icon="calendar", order=13, group="Rooster"), MenuItem("Leerlingrooster", "/leerlingrooster", icon="book", order=14, group="Rooster"), MenuItem("Ouderrooster", "/ouderrooster", icon="badge", order=16, group="Rooster"), # Submenu Organisatie: wie hoort waar. diff --git a/backend/core/migrations/0010_periode_roosterblok_sjabloon.py b/backend/core/migrations/0010_periode_roosterblok_sjabloon.py new file mode 100644 index 0000000..99b9af2 --- /dev/null +++ b/backend/core/migrations/0010_periode_roosterblok_sjabloon.py @@ -0,0 +1,36 @@ +# Generated for Perioderooster: Periode-model + Roosterblok.sjabloon. + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_activiteit_pictogram'), + ] + + operations = [ + migrations.AddField( + model_name='roosterblok', + name='sjabloon', + field=models.BooleanField(default=False, verbose_name='sjabloon'), + ), + migrations.CreateModel( + name='Periode', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nummer', models.PositiveIntegerField(default=1, verbose_name='periodenummer')), + ('start_datum', models.DateField(verbose_name='startdatum')), + ('eind_datum', models.DateField(verbose_name='einddatum')), + ('aangemaakt_op', models.DateTimeField(auto_now_add=True)), + ('schooljaar', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='periodes', to='core.schooljaar')), + ], + options={ + 'verbose_name': 'periode', + 'verbose_name_plural': 'periodes', + 'ordering': ['schooljaar', 'nummer'], + 'unique_together': {('schooljaar', 'nummer')}, + }, + ), + ] diff --git a/backend/core/models.py b/backend/core/models.py index 37745d3..0e843ee 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -414,6 +414,10 @@ class Roosterblok(models.Model): # datumbereik. geldig_van == geldig_tot betekent een eenmalig blok op die dag. geldig_van = models.DateField("geldig vanaf", null=True, blank=True) geldig_tot = models.DateField("geldig tot en met", null=True, blank=True) + # Sjabloonblok: hoort bij de Perioderooster-opzet en telt NIET mee in het + # echte rooster/conflicten. Bij 'voltooien' worden er echte (sjabloon=False) + # blokken van gekopieerd voor de gekozen periode. + sjabloon = models.BooleanField("sjabloon", default=False) aangemaakt_op = models.DateTimeField(auto_now_add=True) class Meta: @@ -445,6 +449,39 @@ class Roosterblok(models.Model): return f"{self.activiteit} @ {self.tijdslot}" +class Periode(models.Model): + """Een periode binnen een schooljaar (Periode 1, 2, ...) met datumbereik. + + Het Perioderooster zet bij 'voltooien' het opgebouwde sjabloon om in echte + roosterblokken met geldig_van/geldig_tot gelijk aan deze periode. + """ + + schooljaar = models.ForeignKey( + Schooljaar, on_delete=models.CASCADE, related_name="periodes" + ) + nummer = models.PositiveIntegerField("periodenummer", default=1) + start_datum = models.DateField("startdatum") + eind_datum = models.DateField("einddatum") + aangemaakt_op = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name = "periode" + verbose_name_plural = "periodes" + ordering = ["schooljaar", "nummer"] + unique_together = ("schooljaar", "nummer") + + @property + def naam(self) -> str: + return f"{self.schooljaar.naam} · Periode {self.nummer}" + + def clean(self): + if self.eind_datum < self.start_datum: + raise ValidationError("'einddatum' ligt vóór 'startdatum'.") + + def __str__(self) -> str: + return self.naam + + # -------------------------------------------------------------------------- # Fase 3: flexibiliteit en uitzonderingen # -------------------------------------------------------------------------- diff --git a/backend/core/serializers.py b/backend/core/serializers.py index 40cbb6f..f510f68 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers from .models import ( Activiteit, Afwezigheid, BlokUitzondering, Functie, Groep, KalenderDag, - Locatie, Lokaal, Lokaalinstelling, Persoon, Roosterblok, Schooljaar, + Locatie, Lokaal, Lokaalinstelling, Periode, Persoon, Roosterblok, Schooljaar, Subgroep, Tijdslot, ) @@ -150,6 +150,22 @@ class SchooljaarSerializer(serializers.ModelSerializer): return data +class PeriodeSerializer(serializers.ModelSerializer): + naam = serializers.CharField(read_only=True) + schooljaar_naam = serializers.CharField(source="schooljaar.naam", read_only=True) + + class Meta: + model = Periode + fields = ["id", "schooljaar", "schooljaar_naam", "nummer", "naam", "start_datum", "eind_datum"] + + def validate(self, data): + start = data.get("start_datum", getattr(self.instance, "start_datum", None)) + eind = data.get("eind_datum", getattr(self.instance, "eind_datum", None)) + if start and eind and eind < start: + raise serializers.ValidationError("De einddatum moet op of na de startdatum liggen.") + return data + + class KalenderDagSerializer(serializers.ModelSerializer): type_label = serializers.CharField(source="get_type_display", read_only=True) @@ -223,7 +239,7 @@ class RoosterblokSerializer(serializers.ModelSerializer): "activiteit", "activiteit_naam", "activiteit_kleur", "activiteit_icoon", "activiteit_pictogram", "locatie", "locatie_naam", "lokaal", "lokaal_naam", "groep", "subgroep", "leerling", "doel_type", "doel_naam", - "geldig_van", "geldig_tot", + "geldig_van", "geldig_tot", "sjabloon", "begeleiders", "begeleiders_namen", "opmerkingen", "aangemaakt_op", ] read_only_fields = ["aangemaakt_op"] @@ -280,9 +296,10 @@ class RoosterblokSerializer(serializers.ModelSerializer): lokaal = data.get("lokaal", getattr(self.instance, "lokaal", None)) tijdslot = data.get("tijdslot", getattr(self.instance, "tijdslot", None)) schooljaar = data.get("schooljaar", getattr(self.instance, "schooljaar", None)) - if lokaal is not None and tijdslot is not None and Lokaalinstelling.actueel().dubbel_blokkeren: + nieuw_sjabloon = data.get("sjabloon", getattr(self.instance, "sjabloon", False)) + if not nieuw_sjabloon and lokaal is not None and tijdslot is not None and Lokaalinstelling.actueel().dubbel_blokkeren: botsing = Roosterblok.objects.filter( - lokaal=lokaal, tijdslot=tijdslot, schooljaar=schooljaar, + lokaal=lokaal, tijdslot=tijdslot, schooljaar=schooljaar, sjabloon=False, ) if self.instance is not None: botsing = botsing.exclude(pk=self.instance.pk) diff --git a/backend/core/urls.py b/backend/core/urls.py index ab7b423..0c80362 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -9,6 +9,7 @@ router.register("groepen", views.GroepViewSet, basename="groep") router.register("subgroepen", views.SubgroepViewSet, basename="subgroep") router.register("functies", views.FunctieViewSet, basename="functie") router.register("schooljaren", views.SchooljaarViewSet, basename="schooljaar") +router.register("periodes", views.PeriodeViewSet, basename="periode") router.register("kalenderdagen", views.KalenderDagViewSet, basename="kalenderdag") router.register("tijdsloten", views.TijdslotViewSet, basename="tijdslot") router.register("activiteiten", views.ActiviteitViewSet, basename="activiteit") diff --git a/backend/core/views.py b/backend/core/views.py index 3a9c477..f741af3 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -7,13 +7,13 @@ from plugins import services from .models import ( Activiteit, Afwezigheid, BlokUitzondering, Functie, Groep, KalenderDag, - Locatie, Lokaal, Lokaalinstelling, Persoon, Roosterblok, Schooljaar, + Locatie, Lokaal, Lokaalinstelling, Periode, Persoon, Roosterblok, Schooljaar, Subgroep, Tijdslot, ) from .serializers import ( ActiviteitSerializer, AfwezigheidSerializer, BlokUitzonderingSerializer, FunctieSerializer, GroepSerializer, KalenderDagSerializer, LocatieSerializer, - LokaalSerializer, LokaalinstellingSerializer, + LokaalSerializer, LokaalinstellingSerializer, PeriodeSerializer, PersoonSerializer, RoosterblokSerializer, SchooljaarSerializer, SubgroepSerializer, TijdslotSerializer, ) @@ -132,6 +132,24 @@ class RoosterblokViewSet(viewsets.ModelViewSet): waarde = self.request.query_params.get(veld) if waarde: qs = qs.filter(**{f"{veld}_id": waarde}) + # Sjabloonblokken (Perioderooster-opzet) standaard uitsluiten uit het + # echte rooster; alleen tonen bij expliciet ?sjabloon=1. + sjabloon = self.request.query_params.get("sjabloon") + if sjabloon in ("1", "true", "True"): + qs = qs.filter(sjabloon=True) + else: + qs = qs.filter(sjabloon=False) + return qs + + +class PeriodeViewSet(viewsets.ModelViewSet): + serializer_class = PeriodeSerializer + + def get_queryset(self): + qs = Periode.objects.select_related("schooljaar").all() + schooljaar = self.request.query_params.get("schooljaar") + if schooljaar: + qs = qs.filter(schooljaar_id=schooljaar) return qs @@ -197,6 +215,7 @@ def conflicten(request): schooljaar = request.query_params.get("schooljaar") qs = ( Roosterblok.objects + .filter(sjabloon=False) .select_related("tijdslot", "locatie", "lokaal", "groep", "subgroep", "leerling") .prefetch_related("begeleiders", "groep__leerlingen", "subgroep__leden") ) diff --git a/backend/modules/autorooster/views.py b/backend/modules/autorooster/views.py index 13d63b4..71ccb17 100644 --- a/backend/modules/autorooster/views.py +++ b/backend/modules/autorooster/views.py @@ -57,7 +57,7 @@ def voorstel(request): beg_bezet = {} # tijdslot -> set(begeleider-id) (globaal) lok_bezet = {} # tijdslot -> set(lokaal-id) (globaal) if respecteer: - for b in Roosterblok.objects.filter(schooljaar=sj).prefetch_related("begeleiders"): + for b in Roosterblok.objects.filter(schooljaar=sj, sjabloon=False).prefetch_related("begeleiders"): for p in b.begeleiders.all(): beg_bezet.setdefault(b.tijdslot_id, set()).add(p.id) if b.lokaal_id: diff --git a/backend/modules/stage/serializers.py b/backend/modules/stage/serializers.py index 3032839..7d8ab6a 100644 --- a/backend/modules/stage/serializers.py +++ b/backend/modules/stage/serializers.py @@ -105,7 +105,7 @@ def _lokaal_bezet_door_ander(lokaal, omvang, dag, dagdeel, tijdslot, exclude_sta return None les = ( Roosterblok.objects - .filter(lokaal=lokaal, tijdslot_id__in=slot_ids) + .filter(lokaal=lokaal, tijdslot_id__in=slot_ids, sjabloon=False) .select_related("activiteit").first() ) if les: @@ -127,7 +127,7 @@ def _theorievak_botsing(begeleider, omvang, dag, dagdeel, tijdslot): qs = ( Roosterblok.objects - .filter(begeleiders=begeleider, activiteit__theorievak=True) + .filter(begeleiders=begeleider, activiteit__theorievak=True, sjabloon=False) .select_related("tijdslot", "activiteit") ) for blok in qs: diff --git a/backend/modules/uren/views.py b/backend/modules/uren/views.py index 42a3a27..b9ed700 100644 --- a/backend/modules/uren/views.py +++ b/backend/modules/uren/views.py @@ -32,7 +32,7 @@ def bereken_uren(sj): d += timedelta(days=1) blokken = list( - Roosterblok.objects.filter(schooljaar=sj) + Roosterblok.objects.filter(schooljaar=sj, sjabloon=False) .select_related("tijdslot", "activiteit", "groep", "subgroep", "leerling", "leerling__groep") .prefetch_related("uitzonderingen") ) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 17370b5..190e873 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,7 +4,7 @@ import Logo from "./Logo.jsx"; import { PersonenPage, FunctiesPage, ActiviteitenPage, LocatiesPage, LokalenPage, TijdslotenPage, SchooljaarPage, - RoosterWeergavePage, RoostermakerPage, WeekRoostermakerPage, + RoosterWeergavePage, RoostermakerPage, PeriodeRoosterPage, AfwezigheidPage, GroepsindelingPage, StagePage, GebruikersPage, UrenPage, LeerlingRoosterPage, OuderRoosterPage, VakWizardPage, PersonenWizardPage, AutoRoosterPage, WachtwoordResetPage, @@ -27,8 +27,8 @@ const normaliseerPad = (path) => (GROEPSINDELING_PADEN.has(path) ? "/groepsindel // placeholder (worden in een latere fase ingevuld). const PAGES = { "/rooster": RoosterWeergavePage, + "/perioderooster": PeriodeRoosterPage, "/roostermaker": RoostermakerPage, - "/weekroostermaker": WeekRoostermakerPage, "/groepsindeling": GroepsindelingPage, "/personen": PersonenPage, "/functies": FunctiesPage, diff --git a/frontend/src/pages.jsx b/frontend/src/pages.jsx index da5fc55..d3904f2 100644 --- a/frontend/src/pages.jsx +++ b/frontend/src/pages.jsx @@ -2764,11 +2764,11 @@ export function RoosterScherm({ mode }) { export function RoosterWeergavePage() { return ; } export function RoostermakerPage() { return ; } -/* --------------- Weekroostermaker: alle klassen in één matrix ------------- */ -// Bouwt het VASTE basisweekrooster (geen periode/kalender-koppeling): per klas -// vakken in elk tijdvak, met directe conflictsignalering over alle klassen heen. -// Afwijken per periode gebeurt daarna in de Roostermaker. -export function WeekRoostermakerPage() { +/* --------------- Perioderooster: alle klassen in één matrix --------------- */ +// Bouwt een weekopzet voor alle klassen (nog zonder periode/kalender-koppeling): +// per klas vakken in elk tijdvak, met directe conflictsignalering over alle +// klassen heen. Bij voltooien wordt een periode gekozen en ingeladen. +export function PeriodeRoosterPage() { const [schooljaren, setSchooljaren] = useState([]); const [schooljaar, setSchooljaar] = useState(""); const [tijdsloten, setTijdsloten] = useState([]); @@ -2784,7 +2784,14 @@ export function WeekRoostermakerPage() { const [cel, setCel] = useState(null); // {tijdslot, groep} const [form, setForm] = useState({ activiteit: "", begeleiders: [], lokaal: "" }); const [bezig, setBezig] = useState(false); - const [voltooid, setVoltooid] = useState(false); + // Voltooien → periode kiezen/aanmaken + preview. + const [periodes, setPeriodes] = useState([]); + const [voltooiOpen, setVoltooiOpen] = useState(false); + const [periodeKeuze, setPeriodeKeuze] = useState("_new"); // id of "_new" + const [nieuwP, setNieuwP] = useState({ start_datum: "", eind_datum: "" }); + const [preview, setPreview] = useState(null); // {toevoegen:[], verwijderen:[]} + const [applyBezig, setApplyBezig] = useState(false); + const [klaarMelding, setKlaarMelding] = useState(null); useEffect(() => { listResource("schooljaren").then((j) => { setSchooljaren(j); if (j.length) setSchooljaar(String(j[0].id)); }).catch((e) => setError(e.message)); @@ -2798,15 +2805,18 @@ export function WeekRoostermakerPage() { listResource("leerpleinen").then(setLeerpleinen).catch(() => {}); }, []); - useEffect(() => { laadBlokken(); }, [schooljaar]); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { laadBlokken(); laadPeriodes(); }, [schooljaar]); // eslint-disable-line react-hooks/exhaustive-deps async function laadBlokken() { if (!schooljaar) { setBlokken([]); return; } try { - const j = await listResource("roosterblokken", `?schooljaar=${schooljaar}`); - setBlokken(j.filter((b) => !b.geldig_van && !b.geldig_tot)); + setBlokken(await listResource("roosterblokken", `?schooljaar=${schooljaar}&sjabloon=1`)); setError(null); } catch (e) { setError(e.message); } } + async function laadPeriodes() { + if (!schooljaar) { setPeriodes([]); return; } + try { setPeriodes(await listResource("periodes", `?schooljaar=${schooljaar}`)); } catch { setPeriodes([]); } + } const ctx = { leerlingen, subgroepen, leerpleinen }; const conflictInfo = berekenRoosterConflicten(blokken, ctx, tijdsloten); @@ -2822,41 +2832,96 @@ export function WeekRoostermakerPage() { if (!cel || !form.activiteit) return; setBezig(true); try { - await createResource("roosterblokken", { schooljaar, tijdslot: cel.tijdslot, activiteit: form.activiteit, groep: cel.groep, subgroep: null, leerling: null, begeleiders: form.begeleiders, lokaal: form.lokaal || null, locatie: null, opmerkingen: "", geldig_van: null, geldig_tot: null }); + await createResource("roosterblokken", { schooljaar, tijdslot: cel.tijdslot, activiteit: form.activiteit, groep: cel.groep, subgroep: null, leerling: null, begeleiders: form.begeleiders, lokaal: form.lokaal || null, locatie: null, opmerkingen: "", geldig_van: null, geldig_tot: null, sjabloon: true }); setCel(null); - setVoltooid(false); + setKlaarMelding(null); await laadBlokken(); } catch (err) { setError(err.message); } setBezig(false); } async function verwijder(id) { - try { await deleteResource("roosterblokken", id); setVoltooid(false); await laadBlokken(); } catch (e) { setError(e.message); } + try { await deleteResource("roosterblokken", id); setKlaarMelding(null); await laadBlokken(); } catch (e) { setError(e.message); } + } + + // ---- Voltooien: periode kiezen/aanmaken, preview en toepassen ---- + const doelKey = (b) => `${b.tijdslot}|${b.groep || ""}|${b.subgroep || ""}|${b.leerling || ""}`; + const overlaptPeriode = (b, van, tot) => { + const bv = b.geldig_van || null, bt = b.geldig_tot || null; + if (bt && bt < van) return false; + if (bv && bv > tot) return false; + return true; + }; + function gekozenPeriodeDatums() { + if (periodeKeuze === "_new") return { van: nieuwP.start_datum, tot: nieuwP.eind_datum }; + const p = periodes.find((x) => String(x.id) === String(periodeKeuze)); + return p ? { van: p.start_datum, tot: p.eind_datum } : { van: "", tot: "" }; + } + function openVoltooien() { + const volgend = (periodes.reduce((m, p) => Math.max(m, p.nummer), 0) || 0) + 1; + setPeriodeKeuze(periodes.length ? String(periodes[0].id) : "_new"); + setNieuwP({ start_datum: "", eind_datum: "", nummer: volgend }); + setPreview(null); + setKlaarMelding(null); + setVoltooiOpen(true); + } + async function maakPreview() { + const { van, tot } = gekozenPeriodeDatums(); + if (!van || !tot) { setError("Kies of vul een periode met begin- en einddatum."); return; } + try { + const live = await listResource("roosterblokken", `?schooljaar=${schooljaar}`); // sjabloon=False + const keys = new Set(blokken.map(doelKey)); + const verwijderen = live.filter((b) => keys.has(doelKey(b)) && overlaptPeriode(b, van, tot)); + setPreview({ toevoegen: blokken, verwijderen, van, tot }); + setError(null); + } catch (e) { setError(e.message); } + } + async function bevestigVoltooien() { + if (!preview) return; + setApplyBezig(true); + try { + let periodeId = periodeKeuze; + if (periodeKeuze === "_new") { + const p = await createResource("periodes", { schooljaar, nummer: nieuwP.nummer, start_datum: preview.van, eind_datum: preview.tot }); + periodeId = p.id; + } + for (const b of preview.verwijderen) { + await deleteResource("roosterblokken", b.id); + } + for (const b of preview.toevoegen) { + await createResource("roosterblokken", { + schooljaar, tijdslot: b.tijdslot, activiteit: b.activiteit, + groep: b.groep, subgroep: b.subgroep, leerling: b.leerling, + begeleiders: (b.begeleiders_namen || []).map((x) => x.id), + lokaal: b.lokaal || null, locatie: b.locatie || null, opmerkingen: b.opmerkingen || "", + geldig_van: preview.van, geldig_tot: preview.tot, sjabloon: false, + }); + } + await laadPeriodes(); + setVoltooiOpen(false); + setPreview(null); + setKlaarMelding(`Perioderooster ingeladen (${preview.toevoegen.length} vak(ken) geplaatst, ${preview.verwijderen.length} overlappend verwijderd). De weekopzet blijft als sjabloon bewaard.`); + } catch (e) { setError(e.message); } + setApplyBezig(false); } return (
-

Weekroostermaker

-

Geef alle klassen in één overzicht hun vaste weekvakken. Dit is het basisweekrooster — niet aan een periode gekoppeld; per periode afwijken (laten vervallen, vervanger) doe je daarna in de Roostermaker. Dezelfde begeleider of hetzelfde lokaal op hetzelfde moment wordt meteen als conflict gemarkeerd.

+

Perioderooster

+

Geef alle klassen in één overzicht hun weekvakken. Je bouwt de weekopzet hier eerst los van de kalender; bij voltooien kies je de periode waarin dit rooster gaat gelden. Dezelfde begeleider of hetzelfde lokaal op hetzelfde moment wordt meteen als conflict gemarkeerd.

{conflictInfo.meldingen.length ? `⚠ ${conflictInfo.meldingen.length} conflict(en)` : "✓ geen conflicten"} - +
- {voltooid && ( -
- {conflictInfo.meldingen.length - ? <>Er zijn nog {conflictInfo.meldingen.length} conflict(en). Los de rood omlijnde vakken op voor je het weekrooster als definitief beschouwt. - : <>Het weekrooster is compleet en conflictvrij; het geldt nu als vast weekrooster voor elke week. Wil je in een bepaalde periode afwijken, ga dan naar de Roostermaker.} -
- )} + {klaarMelding &&
{klaarMelding}
} {conflictInfo.meldingen.length > 0 && (
    @@ -2913,6 +2978,59 @@ export function WeekRoostermakerPage() { )} + {voltooiOpen && ( +
    setVoltooiOpen(false)}> +
    e.stopPropagation()}> +
    +

    Voltooien & periode inladen

    De weekopzet wordt als rooster geplaatst in de gekozen periode. De opzet blijft als sjabloon bewaard.
    + +
    + {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 && ( + <> +
    +

    {preview.toevoegen.length} vak(ken) worden geplaatst voor periode {preview.van} – {preview.tot}.

    + {preview.verwijderen.length > 0 ? ( + <> +

    {preview.verwijderen.length} bestaande blok(ken) overlappen en worden verwijderd:

    +
      + {preview.verwijderen.slice(0, 30).map((b) =>
    • {b.doel_naam} · {b.activiteit_naam} ({b.tijdslot_label})
    • )} + {preview.verwijderen.length > 30 &&
    • …en nog {preview.verwijderen.length - 30}
    • } +
    + + ) :

    Geen overlappende bestaande blokken.

    } +
    +
    + + +
    + + )} +
    +
    + )} + {cel && (
    setCel(null)}>
    e.stopPropagation()}> diff --git a/frontend/src/styles.css b/frontend/src/styles.css index fa76fce..ac3a5ac 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -552,6 +552,8 @@ 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); } +.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; } .voorstel-klas { margin-bottom: 18px; } .voorstel-klas h3 { margin: 8px 0 6px; }