diff --git a/README.md b/README.md index 9c26c92..8f59da4 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,13 @@ Modulair, flexibel roostersysteem voor het speciaal onderwijs (uil-logo: een uil staat voor wijsheid → *Roosterwijs*). Zie `PLAN.md` voor de visie en de fasering, en `DEPLOY.md` voor draaien op een test-/productieserver. -Status: **Fase 0** (fundament + plugin-framework), **Fase 1** (kerndomein: -personen, groepen, subgroepen, functies) en **Fase 2** (schooljaarkalender + -roosterblokken) zijn klaar. Medewerkers kunnen meerdere functies hebben uit een -beheerbare functie-catalogus. +Status: **Fase 0–3** klaar. Fase 0 (plugin-framework), Fase 1 (personen, +groepen, subgroepen, functies), Fase 2 (schooljaarkalender + roosterblokken) en +Fase 3 (afwezigheid, uitzonderingen, effectief weekrooster). Medewerkers kunnen +meerdere functies hebben uit een beheerbare functie-catalogus. Het Rooster-scherm +is een **weekweergave met drag-and-drop** (muis én touch) en een +**touch-vriendelijk** ontwerp; vrije dagen, afwezigheid en uitzonderingen worden +automatisch in het rooster verwerkt. ## Wat er nu staat @@ -57,7 +60,9 @@ API-eindpunten: `/api/subgroepen/`, `/api/functies/`, `/api/schooljaren/`, `/api/kalenderdagen/` (filter `?schooljaar=`), `/api/tijdsloten/`, `/api/activiteiten/`, `/api/locaties/`, `/api/roosterblokken/` - (filters `?schooljaar=`, `?groep=`, `?subgroep=`, `?leerling=`). + (filters `?schooljaar=`, `?groep=`, `?subgroep=`, `?leerling=`), + `/api/afwezigheden/` (filters `?persoon=`, `?van=`, `?tot=`), + `/api/blok-uitzonderingen/` (filters `?roosterblok=`, `?schooljaar=`, `?van=`, `?tot=`). - `/admin/` — Django-admin (personen, groepen, subgroepen en modulestatus). > Draait SQLite niet op je projectmap (bv. op een netwerkschijf)? Zet dan @@ -75,8 +80,7 @@ De dev-server stuurt `/api` automatisch door naar de backend op poort 8000. Open daarna http://localhost:5173 en ga naar **Modulebeheer** om modules aan/uit te zetten. -## Volgende stap (Fase 3 & 4) +## Volgende stap (Fase 4) -Flexibiliteit & uitzonderingen (individuele afwijkingen, afwezigheid/vervanging, -vrije dagen die blokken automatisch uitschakelen) en conflictdetectie -(dubbele inzet van personen/leerlingen, locatiebotsingen). +Conflictdetectie: automatisch signaleren van dubbele inzet van leerkrachten, +ondersteuners of leerlingen op hetzelfde moment, en locatiebotsingen. diff --git a/backend/core/admin.py b/backend/core/admin.py index 86b00b1..ab02fbc 100644 --- a/backend/core/admin.py +++ b/backend/core/admin.py @@ -1,8 +1,8 @@ from django.contrib import admin from .models import ( - Activiteit, Functie, Groep, KalenderDag, Locatie, Persoon, - Roosterblok, Schooljaar, Subgroep, Tijdslot, + Activiteit, Afwezigheid, BlokUitzondering, Functie, Groep, KalenderDag, + Locatie, Persoon, Roosterblok, Schooljaar, Subgroep, Tijdslot, ) @@ -76,3 +76,18 @@ class RoosterblokAdmin(admin.ModelAdmin): list_display = ("activiteit", "tijdslot", "doel_type", "locatie", "schooljaar") list_filter = ("schooljaar", "tijdslot__dag", "activiteit") filter_horizontal = ("begeleiders",) + + +@admin.register(Afwezigheid) +class AfwezigheidAdmin(admin.ModelAdmin): + list_display = ("persoon", "start_datum", "eind_datum", "reden") + list_filter = ("start_datum",) + search_fields = ("persoon__voornaam", "persoon__achternaam") + date_hierarchy = "start_datum" + + +@admin.register(BlokUitzondering) +class BlokUitzonderingAdmin(admin.ModelAdmin): + list_display = ("roosterblok", "datum", "vervalt", "vervanger") + list_filter = ("vervalt", "datum") + date_hierarchy = "datum" diff --git a/backend/core/apps.py b/backend/core/apps.py index c88987e..dd499ba 100644 --- a/backend/core/apps.py +++ b/backend/core/apps.py @@ -36,6 +36,7 @@ class CoreConfig(AppConfig): MenuItem("Locaties", "/locaties", icon="pin", order=55), MenuItem("Tijdsloten", "/tijdsloten", icon="clock", order=60), MenuItem("Schooljaar", "/schooljaar", icon="calendar", order=70), + MenuItem("Afwezigheid", "/afwezigheid", icon="away", order=80), ), ) ) diff --git a/backend/core/migrations/0003_afwezigheid_blokuitzondering.py b/backend/core/migrations/0003_afwezigheid_blokuitzondering.py new file mode 100644 index 0000000..4524a31 --- /dev/null +++ b/backend/core/migrations/0003_afwezigheid_blokuitzondering.py @@ -0,0 +1,47 @@ +# Generated by Django 5.2.15 on 2026-06-10 13:10 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_activiteit_functie_locatie_schooljaar_tijdslot_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Afwezigheid', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start_datum', models.DateField(verbose_name='van')), + ('eind_datum', models.DateField(verbose_name='tot en met')), + ('reden', models.CharField(blank=True, max_length=160, verbose_name='reden')), + ('aangemaakt_op', models.DateTimeField(auto_now_add=True)), + ('persoon', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='afwezigheden', to='core.persoon')), + ], + options={ + 'verbose_name': 'afwezigheid', + 'verbose_name_plural': 'afwezigheden', + 'ordering': ['-start_datum'], + }, + ), + migrations.CreateModel( + name='BlokUitzondering', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('datum', models.DateField(verbose_name='datum')), + ('vervalt', models.BooleanField(default=True, verbose_name='vervalt')), + ('opmerking', models.CharField(blank=True, max_length=200, verbose_name='opmerking')), + ('roosterblok', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='uitzonderingen', to='core.roosterblok')), + ('vervanger', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vervangingen', to='core.persoon')), + ], + options={ + 'verbose_name': 'blok-uitzondering', + 'verbose_name_plural': 'blok-uitzonderingen', + 'ordering': ['datum'], + 'constraints': [models.UniqueConstraint(fields=('roosterblok', 'datum'), name='uniek_blok_datum')], + }, + ), + ] diff --git a/backend/core/models.py b/backend/core/models.py index 47f4e89..d306665 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -334,3 +334,65 @@ class Roosterblok(models.Model): def __str__(self) -> str: return f"{self.activiteit} @ {self.tijdslot}" + + +# -------------------------------------------------------------------------- +# Fase 3: flexibiliteit en uitzonderingen +# -------------------------------------------------------------------------- + +class Afwezigheid(models.Model): + """Een persoon (leerling of medewerker) is afwezig in een periode.""" + + persoon = models.ForeignKey( + Persoon, on_delete=models.CASCADE, related_name="afwezigheden" + ) + start_datum = models.DateField("van") + eind_datum = models.DateField("tot en met") + reden = models.CharField("reden", max_length=160, blank=True) + aangemaakt_op = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name = "afwezigheid" + verbose_name_plural = "afwezigheden" + ordering = ["-start_datum"] + + def clean(self): + if self.eind_datum and self.start_datum and self.eind_datum < self.start_datum: + raise ValidationError("De einddatum ligt vóór de startdatum.") + + def __str__(self) -> str: + return f"{self.persoon} afwezig {self.start_datum}–{self.eind_datum}" + + +class BlokUitzondering(models.Model): + """Een eenmalige afwijking op een roosterblok voor één specifieke datum. + + - vervalt=True -> het blok gaat die dag niet door; + - vervanger -> een vervangende begeleider voor die dag; + - opmerking -> toelichting (bv. 'in ander lokaal'). + """ + + roosterblok = models.ForeignKey( + Roosterblok, on_delete=models.CASCADE, related_name="uitzonderingen" + ) + datum = models.DateField("datum") + vervalt = models.BooleanField("vervalt", default=True) + vervanger = models.ForeignKey( + Persoon, on_delete=models.SET_NULL, null=True, blank=True, + related_name="vervangingen", + ) + opmerking = models.CharField("opmerking", max_length=200, blank=True) + + class Meta: + verbose_name = "blok-uitzondering" + verbose_name_plural = "blok-uitzonderingen" + ordering = ["datum"] + constraints = [ + models.UniqueConstraint( + fields=["roosterblok", "datum"], name="uniek_blok_datum" + ) + ] + + def __str__(self) -> str: + wat = "vervalt" if self.vervalt else "wijziging" + return f"{self.roosterblok} op {self.datum} ({wat})" diff --git a/backend/core/serializers.py b/backend/core/serializers.py index 78d00eb..ea61588 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -2,8 +2,8 @@ from rest_framework import serializers from .models import ( - Activiteit, Functie, Groep, KalenderDag, Locatie, Persoon, - Roosterblok, Schooljaar, Subgroep, Tijdslot, + Activiteit, Afwezigheid, BlokUitzondering, Functie, Groep, KalenderDag, + Locatie, Persoon, Roosterblok, Schooljaar, Subgroep, Tijdslot, ) @@ -157,3 +157,36 @@ class RoosterblokSerializer(serializers.ModelSerializer): "Kies precies één doelgroep: groep, subgroep of leerling." ) return data + + +class AfwezigheidSerializer(serializers.ModelSerializer): + persoon_naam = serializers.CharField(source="persoon.volledige_naam", read_only=True) + persoon_rol = serializers.CharField(source="persoon.rol", read_only=True) + + class Meta: + model = Afwezigheid + fields = [ + "id", "persoon", "persoon_naam", "persoon_rol", + "start_datum", "eind_datum", "reden", "aangemaakt_op", + ] + read_only_fields = ["aangemaakt_op"] + + 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 ligt vóór de startdatum.") + return data + + +class BlokUitzonderingSerializer(serializers.ModelSerializer): + vervanger_naam = serializers.CharField( + source="vervanger.volledige_naam", read_only=True, default=None + ) + + class Meta: + model = BlokUitzondering + fields = [ + "id", "roosterblok", "datum", "vervalt", + "vervanger", "vervanger_naam", "opmerking", + ] diff --git a/backend/core/urls.py b/backend/core/urls.py index 9775d1e..027ca6d 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -14,6 +14,8 @@ router.register("tijdsloten", views.TijdslotViewSet, basename="tijdslot") router.register("activiteiten", views.ActiviteitViewSet, basename="activiteit") router.register("locaties", views.LocatieViewSet, basename="locatie") router.register("roosterblokken", views.RoosterblokViewSet, basename="roosterblok") +router.register("afwezigheden", views.AfwezigheidViewSet, basename="afwezigheid") +router.register("blok-uitzonderingen", views.BlokUitzonderingViewSet, basename="blok-uitzondering") urlpatterns = [ path("health/", views.health, name="health"), diff --git a/backend/core/views.py b/backend/core/views.py index 4a57da9..79aff06 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -10,14 +10,14 @@ from rest_framework.response import Response from plugins import services from .models import ( - Activiteit, Functie, Groep, KalenderDag, Locatie, Persoon, - Roosterblok, Schooljaar, Subgroep, Tijdslot, + Activiteit, Afwezigheid, BlokUitzondering, Functie, Groep, KalenderDag, + Locatie, Persoon, Roosterblok, Schooljaar, Subgroep, Tijdslot, ) from .serializers import ( - ActiviteitSerializer, FunctieSerializer, GroepSerializer, - KalenderDagSerializer, LocatieSerializer, PersoonSerializer, - RoosterblokSerializer, SchooljaarSerializer, SubgroepSerializer, - TijdslotSerializer, + ActiviteitSerializer, AfwezigheidSerializer, BlokUitzonderingSerializer, + FunctieSerializer, GroepSerializer, KalenderDagSerializer, LocatieSerializer, + PersoonSerializer, RoosterblokSerializer, SchooljaarSerializer, + SubgroepSerializer, TijdslotSerializer, ) logger = logging.getLogger("roosterwijs.auth") @@ -170,3 +170,41 @@ class RoosterblokViewSet(viewsets.ModelViewSet): if waarde: qs = qs.filter(**{f"{veld}_id": waarde}) return qs + + +class AfwezigheidViewSet(viewsets.ModelViewSet): + serializer_class = AfwezigheidSerializer + + def get_queryset(self): + qs = Afwezigheid.objects.select_related("persoon").all() + persoon = self.request.query_params.get("persoon") + if persoon: + qs = qs.filter(persoon_id=persoon) + # Overlap met een periode [van, tot]: start <= tot EN eind >= van. + van = self.request.query_params.get("van") + tot = self.request.query_params.get("tot") + if van: + qs = qs.filter(eind_datum__gte=van) + if tot: + qs = qs.filter(start_datum__lte=tot) + return qs + + +class BlokUitzonderingViewSet(viewsets.ModelViewSet): + serializer_class = BlokUitzonderingSerializer + + def get_queryset(self): + qs = BlokUitzondering.objects.select_related("vervanger", "roosterblok").all() + blok = self.request.query_params.get("roosterblok") + if blok: + qs = qs.filter(roosterblok_id=blok) + van = self.request.query_params.get("van") + tot = self.request.query_params.get("tot") + if van: + qs = qs.filter(datum__gte=van) + if tot: + qs = qs.filter(datum__lte=tot) + schooljaar = self.request.query_params.get("schooljaar") + if schooljaar: + qs = qs.filter(roosterblok__schooljaar_id=schooljaar) + return qs diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 771173d..22b0112 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,11 +4,13 @@ import Logo from "./Logo.jsx"; import { GroepenPage, PersonenPage, SubgroepenPage, FunctiesPage, ActiviteitenPage, LocatiesPage, TijdslotenPage, SchooljaarPage, RoosterPage, + AfwezigheidPage, } from "./pages.jsx"; const ICONS = { calendar: "📅", users: "👥", layers: "🗂️", printer: "🖨️", key: "🔑", puzzle: "🧩", badge: "🪪", book: "📚", pin: "📍", clock: "🕘", + away: "🌴", }; // Koppel een menu-pad aan een echt scherm. Onbekende paden tonen een @@ -23,6 +25,7 @@ const PAGES = { "/locaties": LocatiesPage, "/tijdsloten": TijdslotenPage, "/schooljaar": SchooljaarPage, + "/afwezigheid": AfwezigheidPage, }; export default function App() { diff --git a/frontend/src/pages.jsx b/frontend/src/pages.jsx index b71f0c2..2255fe3 100644 --- a/frontend/src/pages.jsx +++ b/frontend/src/pages.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { listResource, createResource, @@ -687,7 +687,73 @@ function Kalender({ schooljaarId }) { ); } +/* ------------------------------- Afwezigheid ------------------------------ */ +export function AfwezigheidPage() { + const { items, error, loading, reload, setError } = useList("afwezigheden"); + const [personen, setPersonen] = useState([]); + const leeg = { persoon: "", start_datum: "", eind_datum: "", reden: "" }; + const [form, setForm] = useState(leeg); + + useEffect(() => { listResource("personen").then(setPersonen).catch(() => {}); }, []); + + async function opslaan(e) { + e.preventDefault(); + try { + await createResource("afwezigheden", form); + setForm(leeg); + await reload(); + } catch (err) { setError(err.message); } + } + async function verwijder(id) { + if (!confirm("Deze afwezigheid verwijderen?")) return; + try { await deleteResource("afwezigheden", id); await reload(); } catch (err) { setError(err.message); } + } + + return ( +
+

Afwezigheid

+

Leg vast wie wanneer afwezig is. In het rooster zie je dat terug bij de betrokken blokken.

+ +
+ + + + setForm({ ...form, reden: e.target.value })} /> + +
+ {loading ?

Laden…

: ( + + + + {items.map((a) => ( + + + + + + + ))} + {items.length === 0 && } + +
PersoonPeriodeReden
{a.persoon_naam}{a.start_datum} t/m {a.eind_datum}{a.reden || "—"}
Geen afwezigheden.
+ )} +
+ ); +} + +/* ------------------------------ Datumhulpjes ------------------------------ */ +function pad2(n) { return String(n).padStart(2, "0"); } +function fmtISO(d) { return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; } +function fmtDM(d) { return `${pad2(d.getDate())}-${pad2(d.getMonth() + 1)}`; } +function maandagVan(d) { const x = new Date(d); x.setHours(0, 0, 0, 0); const wd = (x.getDay() + 6) % 7; x.setDate(x.getDate() - wd); return x; } +function plusDagen(d, n) { const x = new Date(d); x.setDate(x.getDate() + n); return x; } + /* ---------------------------------- Rooster ------------------------------- */ +// Weekweergave met effectief rooster (vrije dagen, afwezigheid, uitzonderingen) +// en drag-and-drop (muis + touch) om een blok naar een ander tijdslot te slepen. export function RoosterPage() { const [schooljaren, setSchooljaren] = useState([]); const [schooljaar, setSchooljaar] = useState(""); @@ -699,19 +765,24 @@ export function RoosterPage() { const [leerlingen, setLeerlingen] = useState([]); const [medewerkers, setMedewerkers] = useState([]); const [blokken, setBlokken] = useState([]); + const [kalender, setKalender] = useState([]); + const [afwezig, setAfwezig] = useState([]); + const [uitzonderingen, setUitzonderingen] = useState([]); + const [maandag, setMaandag] = useState(() => maandagVan(new Date())); + const [toonForm, setToonForm] = useState(false); const [error, setError] = useState(null); - const leeg = { - tijdslot: "", activiteit: "", locatie: "", - doel_type: "groep", doel: "", begeleiders: [], opmerkingen: "", - }; + const leeg = { tijdslot: "", activiteit: "", locatie: "", doel_type: "groep", doel: "", begeleiders: [], opmerkingen: "" }; const [form, setForm] = useState(leeg); + // Drag-and-drop via pointer events (werkt op muis én touch). + const dragRef = useRef(null); + const laadRef = useRef(() => {}); + const [ghost, setGhost] = useState(null); + const [hoverSlot, setHoverSlot] = useState(null); + useEffect(() => { - listResource("schooljaren").then((j) => { - setSchooljaren(j); - if (j.length && !schooljaar) setSchooljaar(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)); listResource("tijdsloten").then(setTijdsloten).catch(() => {}); listResource("activiteiten").then(setActiviteiten).catch(() => {}); listResource("locaties").then(setLocaties).catch(() => {}); @@ -721,96 +792,120 @@ export function RoosterPage() { listResource("personen").then((ps) => setMedewerkers(ps.filter((p) => p.rol !== "leerling"))).catch(() => {}); }, []); + const weekDatums = [0, 1, 2, 3, 4].map((i) => plusDagen(maandag, i)); + const vanISO = fmtISO(maandag); + const totISO = fmtISO(plusDagen(maandag, 6)); + async function laadBlokken() { - if (!schooljaar) return; + if (!schooljaar) { setBlokken([]); return; } try { setBlokken(await listResource("roosterblokken", `?schooljaar=${schooljaar}`)); + setKalender(await listResource("kalenderdagen", `?schooljaar=${schooljaar}`)); setError(null); - } catch (e) { - setError(e.message); - } + } catch (e) { setError(e.message); } } + async function laadWeek() { + if (!schooljaar) return; + try { + setAfwezig(await listResource("afwezigheden", `?van=${vanISO}&tot=${totISO}`)); + setUitzonderingen(await listResource("blok-uitzonderingen", `?schooljaar=${schooljaar}&van=${vanISO}&tot=${totISO}`)); + } catch (e) { setError(e.message); } + } + laadRef.current = laadBlokken; useEffect(() => { laadBlokken(); }, [schooljaar]); + useEffect(() => { laadWeek(); }, [schooljaar, vanISO]); - const doelOpties = form.doel_type === "groep" ? groepen - : form.doel_type === "subgroep" ? subgroepen - : leerlingen; + // Listeners voor drag-and-drop, één keer opgezet. + useEffect(() => { + function move(e) { + if (!dragRef.current) return; + setGhost({ x: e.clientX, y: e.clientY, label: dragRef.current.activiteit_naam }); + const el = document.elementFromPoint(e.clientX, e.clientY); + const cell = el && el.closest("[data-tijdslot]"); + setHoverSlot(cell ? Number(cell.dataset.tijdslot) : null); + } + async function up(e) { + if (!dragRef.current) return; + const el = document.elementFromPoint(e.clientX, e.clientY); + const cell = el && el.closest("[data-tijdslot]"); + const doel = cell ? Number(cell.dataset.tijdslot) : null; + const blok = dragRef.current; + dragRef.current = null; + setGhost(null); + setHoverSlot(null); + if (doel && doel !== blok.tijdslot) { + try { await updateResource("roosterblokken", blok.id, { tijdslot: doel }); await laadRef.current(); } + catch (err) { setError(err.message); } + } + } + window.addEventListener("pointermove", move); + window.addEventListener("pointerup", up); + return () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); }; + }, []); - function toggleBegeleider(id) { - setForm((f) => - f.begeleiders.includes(id) - ? { ...f, begeleiders: f.begeleiders.filter((x) => x !== id) } - : { ...f, begeleiders: [...f.begeleiders, id] } - ); + function startDrag(e, blok) { + e.preventDefault(); + dragRef.current = blok; + setGhost({ x: e.clientX, y: e.clientY, label: blok.activiteit_naam }); + setHoverSlot(blok.tijdslot); } + const doelOpties = form.doel_type === "groep" ? groepen : form.doel_type === "subgroep" ? subgroepen : leerlingen; + function toggleBegeleider(id) { + setForm((f) => f.begeleiders.includes(id) ? { ...f, begeleiders: f.begeleiders.filter((x) => x !== id) } : { ...f, begeleiders: [...f.begeleiders, id] }); + } async function opslaan(e) { e.preventDefault(); - const payload = { - schooljaar, - tijdslot: form.tijdslot, - activiteit: form.activiteit, - locatie: form.locatie || null, - groep: form.doel_type === "groep" ? form.doel : null, - subgroep: form.doel_type === "subgroep" ? form.doel : null, - leerling: form.doel_type === "leerling" ? form.doel : null, - begeleiders: form.begeleiders, - opmerkingen: form.opmerkingen, - }; - try { - await createResource("roosterblokken", payload); - setForm({ ...leeg, doel_type: form.doel_type }); - await laadBlokken(); - } catch (err) { - setError(err.message); - } + const payload = { schooljaar, tijdslot: form.tijdslot, activiteit: form.activiteit, locatie: form.locatie || null, groep: form.doel_type === "groep" ? form.doel : null, subgroep: form.doel_type === "subgroep" ? form.doel : null, leerling: form.doel_type === "leerling" ? form.doel : null, begeleiders: form.begeleiders, opmerkingen: form.opmerkingen }; + try { await createResource("roosterblokken", payload); setForm({ ...leeg, doel_type: form.doel_type }); setToonForm(false); await laadBlokken(); } catch (err) { setError(err.message); } } async function verwijder(id) { - if (!confirm("Dit roosterblok verwijderen?")) return; - try { - await deleteResource("roosterblokken", id); - await laadBlokken(); - } catch (err) { - setError(err.message); - } + if (!confirm("Dit roosterblok (alle weken) verwijderen?")) return; + try { await deleteResource("roosterblokken", id); await laadBlokken(); } catch (err) { setError(err.message); } + } + async function laatVervallen(blok, iso) { + try { await createResource("blok-uitzonderingen", { roosterblok: blok.id, datum: iso, vervalt: true }); await laadWeek(); } + catch (err) { setError(err.message); } + } + async function herstel(uitId) { + try { await deleteResource("blok-uitzonderingen", uitId); await laadWeek(); } catch (err) { setError(err.message); } } - // Groepeer blokken per weekdag voor een overzichtelijk rooster. - const perDag = WEEKDAGEN.map((d) => ({ - dag: d, - blokken: blokken - .filter((b) => { - const t = tijdsloten.find((x) => x.id === b.tijdslot); - return t && t.dag === d.value; - }) - .sort((a, b) => { - const ta = tijdsloten.find((x) => x.id === a.tijdslot); - const tb = tijdsloten.find((x) => x.id === b.tijdslot); - return (ta?.volgorde ?? 0) - (tb?.volgorde ?? 0); - }), - })); + // Hulpfuncties voor het effectieve rooster. + const kalenderOp = (iso) => kalender.find((k) => k.datum === iso); + const uitzOp = (blokId, iso) => uitzonderingen.find((u) => u.roosterblok === blokId && u.datum === iso); + const persoonAfwezig = (naam, iso) => afwezig.some((a) => a.persoon_naam === naam && a.start_datum <= iso && a.eind_datum >= iso); return (

Rooster

-

Roosterblokken: wie doet wat, wanneer, waar en met wie. Koppel aan een groep, subgroep of individuele leerling.

+

Sleep een blok naar een ander tijdslot om het te verplaatsen. Vrije dagen, afwezigheid en uitzonderingen worden meegenomen.

-
+
+ {schooljaar && ( +
+ + + + week van {fmtDM(maandag)} +
+ )} + {schooljaar && }
- {schooljaar && ( + {schooljaar && toonForm && (