Lokalen en Locaties als eigen items in het instellingen-menu (bovenbalk)
This commit is contained in:
parent
a9dff26d9a
commit
3d29359d61
18 changed files with 469 additions and 159 deletions
|
|
@ -53,6 +53,15 @@ volgt.
|
||||||
(in het weekrooster gemarkeerd "Op stage" bij de betreffende blokken).
|
(in het weekrooster gemarkeerd "Op stage" bij de betreffende blokken).
|
||||||
- [x] Stage instelbaar **per blok, per dagdeel of per dag**.
|
- [x] Stage instelbaar **per blok, per dagdeel of per dag**.
|
||||||
|
|
||||||
|
## Lokalen & dubbele boekingen
|
||||||
|
|
||||||
|
- [x] **Lokalen** als apart roomsysteem (naast de vrije Locaties), met
|
||||||
|
capaciteit; gedeeld door lessen en interne stages.
|
||||||
|
- [x] **Dubbele-boekingcheck** per tijdslot: gemeld in het conflicten-overzicht;
|
||||||
|
per school instelbaar om opslaan **hard te blokkeren** (Lokalen-scherm).
|
||||||
|
- [x] Interne stages gebruiken nu een **lokaal** (de aparte stagelocatie-lijst is
|
||||||
|
vervallen) en tellen mee in de dubbele-boekingcheck.
|
||||||
|
|
||||||
## Koppelingen & inzetregels
|
## Koppelingen & inzetregels
|
||||||
|
|
||||||
- [x] **Personeel koppelen aan leerplein en subgroep** (personeelsformulier:
|
- [x] **Personeel koppelen aan leerplein en subgroep** (personeelsformulier:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ from django.contrib import admin
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Activiteit, Afwezigheid, BlokUitzondering, Functie, Groep, KalenderDag,
|
Activiteit, Afwezigheid, BlokUitzondering, Functie, Groep, KalenderDag,
|
||||||
Locatie, Persoon, Roosterblok, Schooljaar, Subgroep, Tijdslot,
|
Locatie, Lokaal, Lokaalinstelling, Persoon, Roosterblok, Schooljaar,
|
||||||
|
Subgroep, Tijdslot,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -92,3 +93,15 @@ class BlokUitzonderingAdmin(admin.ModelAdmin):
|
||||||
list_display = ("roosterblok", "datum", "vervalt", "vervanger")
|
list_display = ("roosterblok", "datum", "vervalt", "vervanger")
|
||||||
list_filter = ("vervalt", "datum")
|
list_filter = ("vervalt", "datum")
|
||||||
date_hierarchy = "datum"
|
date_hierarchy = "datum"
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Lokaal)
|
||||||
|
class LokaalAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("naam", "capaciteit", "omschrijving", "actief")
|
||||||
|
list_filter = ("actief",)
|
||||||
|
search_fields = ("naam",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Lokaalinstelling)
|
||||||
|
class LokaalinstellingAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("dubbel_blokkeren",)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ class CoreConfig(AppConfig):
|
||||||
MenuItem("Personen", "/personen", icon="users", order=20, group="Organisatie"),
|
MenuItem("Personen", "/personen", icon="users", order=20, group="Organisatie"),
|
||||||
MenuItem("Groepsindeling", "/groepsindeling", icon="layers", order=24, group="Organisatie"),
|
MenuItem("Groepsindeling", "/groepsindeling", icon="layers", order=24, group="Organisatie"),
|
||||||
MenuItem("Afwezigheid", "/afwezigheid", icon="away", order=28, group="Organisatie"),
|
MenuItem("Afwezigheid", "/afwezigheid", icon="away", order=28, group="Organisatie"),
|
||||||
|
# Submenu Instellingen (cog in de bovenbalk): bouwstenen.
|
||||||
|
MenuItem("Lokalen", "/lokalen", icon="door", order=70, group="Instellingen"),
|
||||||
|
MenuItem("Locaties", "/locaties", icon="pin", order=72, group="Instellingen"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Generated by Django 5.2.15 on 2026-06-16 06:09
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0005_subgroep_begeleiders_subgroep_groep'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Lokaal',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('naam', models.CharField(max_length=80, unique=True, verbose_name='naam')),
|
||||||
|
('omschrijving', models.CharField(blank=True, max_length=255, verbose_name='omschrijving')),
|
||||||
|
('capaciteit', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='capaciteit')),
|
||||||
|
('actief', models.BooleanField(default=True, verbose_name='actief')),
|
||||||
|
('aangemaakt_op', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'lokaal',
|
||||||
|
'verbose_name_plural': 'lokalen',
|
||||||
|
'ordering': ['naam'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Lokaalinstelling',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('dubbel_blokkeren', models.BooleanField(default=False, help_text='Aan = opslaan weigeren bij dubbele lokaalboeking; uit = alleen waarschuwen.', verbose_name='dubbele boeking hard blokkeren')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'lokaalinstelling',
|
||||||
|
'verbose_name_plural': 'lokaalinstelling',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='roosterblok',
|
||||||
|
name='lokaal',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='roosterblokken', to='core.lokaal'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -317,6 +317,50 @@ class Locatie(models.Model):
|
||||||
return self.naam
|
return self.naam
|
||||||
|
|
||||||
|
|
||||||
|
class Lokaal(models.Model):
|
||||||
|
"""Een fysiek lokaal/ruimte met dubbele-boekingbewaking.
|
||||||
|
|
||||||
|
Apart van de vrije 'Locatie'-lijst: lokalen worden gedeeld door lessen
|
||||||
|
(Roosterblok) en interne stages, en er wordt gecontroleerd of een lokaal
|
||||||
|
niet op hetzelfde tijdslot dubbel geboekt wordt.
|
||||||
|
"""
|
||||||
|
|
||||||
|
naam = models.CharField("naam", max_length=80, unique=True)
|
||||||
|
omschrijving = models.CharField("omschrijving", max_length=255, blank=True)
|
||||||
|
capaciteit = models.PositiveSmallIntegerField("capaciteit", null=True, blank=True)
|
||||||
|
actief = models.BooleanField("actief", default=True)
|
||||||
|
aangemaakt_op = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "lokaal"
|
||||||
|
verbose_name_plural = "lokalen"
|
||||||
|
ordering = ["naam"]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.naam
|
||||||
|
|
||||||
|
|
||||||
|
class Lokaalinstelling(models.Model):
|
||||||
|
"""Eén-rij-instelling: blokkeert dubbele lokaalboekingen hard (per school)."""
|
||||||
|
|
||||||
|
dubbel_blokkeren = models.BooleanField(
|
||||||
|
"dubbele boeking hard blokkeren", default=False,
|
||||||
|
help_text="Aan = opslaan weigeren bij dubbele lokaalboeking; uit = alleen waarschuwen.",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "lokaalinstelling"
|
||||||
|
verbose_name_plural = "lokaalinstelling"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def actueel(cls):
|
||||||
|
obj, _ = cls.objects.get_or_create(pk=1)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "Lokaalinstelling"
|
||||||
|
|
||||||
|
|
||||||
class Roosterblok(models.Model):
|
class Roosterblok(models.Model):
|
||||||
"""De spil: wie doet wat, wanneer, waar, met wie.
|
"""De spil: wie doet wat, wanneer, waar, met wie.
|
||||||
|
|
||||||
|
|
@ -337,6 +381,10 @@ class Roosterblok(models.Model):
|
||||||
Locatie, on_delete=models.SET_NULL, null=True, blank=True,
|
Locatie, on_delete=models.SET_NULL, null=True, blank=True,
|
||||||
related_name="roosterblokken",
|
related_name="roosterblokken",
|
||||||
)
|
)
|
||||||
|
lokaal = models.ForeignKey(
|
||||||
|
Lokaal, on_delete=models.SET_NULL, null=True, blank=True,
|
||||||
|
related_name="roosterblokken",
|
||||||
|
)
|
||||||
|
|
||||||
# Doelgroep: precies één van de drie is gevuld.
|
# Doelgroep: precies één van de drie is gevuld.
|
||||||
groep = models.ForeignKey(
|
groep = models.ForeignKey(
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ from rest_framework import serializers
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Activiteit, Afwezigheid, BlokUitzondering, Functie, Groep, KalenderDag,
|
Activiteit, Afwezigheid, BlokUitzondering, Functie, Groep, KalenderDag,
|
||||||
Locatie, Persoon, Roosterblok, Schooljaar, Subgroep, Tijdslot,
|
Locatie, Lokaal, Lokaalinstelling, Persoon, Roosterblok, Schooljaar,
|
||||||
|
Subgroep, Tijdslot,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -191,11 +192,24 @@ class LocatieSerializer(serializers.ModelSerializer):
|
||||||
fields = ["id", "naam", "omschrijving", "actief"]
|
fields = ["id", "naam", "omschrijving", "actief"]
|
||||||
|
|
||||||
|
|
||||||
|
class LokaalSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Lokaal
|
||||||
|
fields = ["id", "naam", "omschrijving", "capaciteit", "actief"]
|
||||||
|
|
||||||
|
|
||||||
|
class LokaalinstellingSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Lokaalinstelling
|
||||||
|
fields = ["dubbel_blokkeren"]
|
||||||
|
|
||||||
|
|
||||||
class RoosterblokSerializer(serializers.ModelSerializer):
|
class RoosterblokSerializer(serializers.ModelSerializer):
|
||||||
activiteit_naam = serializers.CharField(source="activiteit.naam", read_only=True)
|
activiteit_naam = serializers.CharField(source="activiteit.naam", read_only=True)
|
||||||
activiteit_kleur = serializers.CharField(source="activiteit.kleur", read_only=True)
|
activiteit_kleur = serializers.CharField(source="activiteit.kleur", read_only=True)
|
||||||
tijdslot_label = serializers.CharField(source="tijdslot.__str__", read_only=True)
|
tijdslot_label = serializers.CharField(source="tijdslot.__str__", read_only=True)
|
||||||
locatie_naam = serializers.CharField(source="locatie.naam", read_only=True, default=None)
|
locatie_naam = serializers.CharField(source="locatie.naam", read_only=True, default=None)
|
||||||
|
lokaal_naam = serializers.CharField(source="lokaal.naam", read_only=True, default=None)
|
||||||
doel_type = serializers.CharField(read_only=True)
|
doel_type = serializers.CharField(read_only=True)
|
||||||
doel_naam = serializers.SerializerMethodField()
|
doel_naam = serializers.SerializerMethodField()
|
||||||
begeleiders_namen = serializers.SerializerMethodField()
|
begeleiders_namen = serializers.SerializerMethodField()
|
||||||
|
|
@ -205,7 +219,7 @@ class RoosterblokSerializer(serializers.ModelSerializer):
|
||||||
fields = [
|
fields = [
|
||||||
"id", "schooljaar", "tijdslot", "tijdslot_label",
|
"id", "schooljaar", "tijdslot", "tijdslot_label",
|
||||||
"activiteit", "activiteit_naam", "activiteit_kleur",
|
"activiteit", "activiteit_naam", "activiteit_kleur",
|
||||||
"locatie", "locatie_naam",
|
"locatie", "locatie_naam", "lokaal", "lokaal_naam",
|
||||||
"groep", "subgroep", "leerling", "doel_type", "doel_naam",
|
"groep", "subgroep", "leerling", "doel_type", "doel_naam",
|
||||||
"begeleiders", "begeleiders_namen", "opmerkingen", "aangemaakt_op",
|
"begeleiders", "begeleiders_namen", "opmerkingen", "aangemaakt_op",
|
||||||
]
|
]
|
||||||
|
|
@ -258,6 +272,33 @@ class RoosterblokSerializer(serializers.ModelSerializer):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
f"'{p.volledige_naam}' is niet gekoppeld aan het vak '{activiteit.naam}'."
|
f"'{p.volledige_naam}' is niet gekoppeld aan het vak '{activiteit.naam}'."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Dubbele-lokaalboeking: bij harde modus weigeren we opslaan.
|
||||||
|
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:
|
||||||
|
botsing = Roosterblok.objects.filter(
|
||||||
|
lokaal=lokaal, tijdslot=tijdslot, schooljaar=schooljaar,
|
||||||
|
)
|
||||||
|
if self.instance is not None:
|
||||||
|
botsing = botsing.exclude(pk=self.instance.pk)
|
||||||
|
if botsing.exists():
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"Lokaal '{lokaal.naam}' is op dit tijdslot al bezet."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
from modules.stage.models import Stage as _Stage
|
||||||
|
stage_bezet = any(
|
||||||
|
st.dekt_tijdslot(tijdslot)
|
||||||
|
for st in _Stage.objects.filter(actief=True, lokaal=lokaal)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
stage_bezet = False
|
||||||
|
if stage_bezet:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"Lokaal '{lokaal.naam}' is op dit tijdslot al door een stage bezet."
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ router.register("kalenderdagen", views.KalenderDagViewSet, basename="kalenderdag
|
||||||
router.register("tijdsloten", views.TijdslotViewSet, basename="tijdslot")
|
router.register("tijdsloten", views.TijdslotViewSet, basename="tijdslot")
|
||||||
router.register("activiteiten", views.ActiviteitViewSet, basename="activiteit")
|
router.register("activiteiten", views.ActiviteitViewSet, basename="activiteit")
|
||||||
router.register("locaties", views.LocatieViewSet, basename="locatie")
|
router.register("locaties", views.LocatieViewSet, basename="locatie")
|
||||||
|
router.register("lokalen", views.LokaalViewSet, basename="lokaal")
|
||||||
router.register("roosterblokken", views.RoosterblokViewSet, basename="roosterblok")
|
router.register("roosterblokken", views.RoosterblokViewSet, basename="roosterblok")
|
||||||
router.register("afwezigheden", views.AfwezigheidViewSet, basename="afwezigheid")
|
router.register("afwezigheden", views.AfwezigheidViewSet, basename="afwezigheid")
|
||||||
router.register("blok-uitzonderingen", views.BlokUitzonderingViewSet, basename="blok-uitzondering")
|
router.register("blok-uitzonderingen", views.BlokUitzonderingViewSet, basename="blok-uitzondering")
|
||||||
|
|
@ -21,6 +22,7 @@ urlpatterns = [
|
||||||
path("health/", views.health, name="health"),
|
path("health/", views.health, name="health"),
|
||||||
path("info/", views.info, name="info"),
|
path("info/", views.info, name="info"),
|
||||||
path("conflicten/", views.conflicten, name="conflicten"),
|
path("conflicten/", views.conflicten, name="conflicten"),
|
||||||
|
path("lokaalinstelling/", views.lokaalinstelling, name="lokaalinstelling"),
|
||||||
path("auth/me/", views.auth_me, name="auth-me"),
|
path("auth/me/", views.auth_me, name="auth-me"),
|
||||||
path("auth/login/", views.auth_login_view, name="auth-login"),
|
path("auth/login/", views.auth_login_view, name="auth-login"),
|
||||||
path("auth/logout/", views.auth_logout_view, name="auth-logout"),
|
path("auth/logout/", views.auth_logout_view, name="auth-logout"),
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,13 @@ from plugins import services
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Activiteit, Afwezigheid, BlokUitzondering, Functie, Groep, KalenderDag,
|
Activiteit, Afwezigheid, BlokUitzondering, Functie, Groep, KalenderDag,
|
||||||
Locatie, Persoon, Roosterblok, Schooljaar, Subgroep, Tijdslot,
|
Locatie, Lokaal, Lokaalinstelling, Persoon, Roosterblok, Schooljaar,
|
||||||
|
Subgroep, Tijdslot,
|
||||||
)
|
)
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
ActiviteitSerializer, AfwezigheidSerializer, BlokUitzonderingSerializer,
|
ActiviteitSerializer, AfwezigheidSerializer, BlokUitzonderingSerializer,
|
||||||
FunctieSerializer, GroepSerializer, KalenderDagSerializer, LocatieSerializer,
|
FunctieSerializer, GroepSerializer, KalenderDagSerializer, LocatieSerializer,
|
||||||
|
LokaalSerializer, LokaalinstellingSerializer,
|
||||||
PersoonSerializer, RoosterblokSerializer, SchooljaarSerializer,
|
PersoonSerializer, RoosterblokSerializer, SchooljaarSerializer,
|
||||||
SubgroepSerializer, TijdslotSerializer,
|
SubgroepSerializer, TijdslotSerializer,
|
||||||
)
|
)
|
||||||
|
|
@ -155,6 +157,23 @@ class LocatieViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = LocatieSerializer
|
serializer_class = LocatieSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class LokaalViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Lokaal.objects.all()
|
||||||
|
serializer_class = LokaalSerializer
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["GET", "PUT"])
|
||||||
|
def lokaalinstelling(request):
|
||||||
|
"""Lees of wijzig de (per-school) instelling voor dubbele lokaalboekingen."""
|
||||||
|
obj = Lokaalinstelling.actueel()
|
||||||
|
if request.method == "PUT":
|
||||||
|
ser = LokaalinstellingSerializer(obj, data=request.data, partial=True)
|
||||||
|
ser.is_valid(raise_exception=True)
|
||||||
|
ser.save()
|
||||||
|
return Response(ser.data)
|
||||||
|
return Response(LokaalinstellingSerializer(obj).data)
|
||||||
|
|
||||||
|
|
||||||
class RoosterblokViewSet(viewsets.ModelViewSet):
|
class RoosterblokViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = RoosterblokSerializer
|
serializer_class = RoosterblokSerializer
|
||||||
|
|
||||||
|
|
@ -234,7 +253,7 @@ def conflicten(request):
|
||||||
schooljaar = request.query_params.get("schooljaar")
|
schooljaar = request.query_params.get("schooljaar")
|
||||||
qs = (
|
qs = (
|
||||||
Roosterblok.objects
|
Roosterblok.objects
|
||||||
.select_related("tijdslot", "locatie", "groep", "subgroep", "leerling")
|
.select_related("tijdslot", "locatie", "lokaal", "groep", "subgroep", "leerling")
|
||||||
.prefetch_related("begeleiders", "groep__leerlingen", "subgroep__leden")
|
.prefetch_related("begeleiders", "groep__leerlingen", "subgroep__leden")
|
||||||
)
|
)
|
||||||
if schooljaar:
|
if schooljaar:
|
||||||
|
|
@ -303,6 +322,22 @@ def conflicten(request):
|
||||||
"blokken": [b.activiteit.naam for b in info["blokken"]],
|
"blokken": [b.activiteit.naam for b in info["blokken"]],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Lokaalbotsing (dubbele boeking van een lokaal op hetzelfde tijdslot).
|
||||||
|
lokaal_map = {}
|
||||||
|
for b in blokken:
|
||||||
|
if b.lokaal_id:
|
||||||
|
lokaal_map.setdefault(b.lokaal_id, {"naam": b.lokaal.naam, "blokken": []})
|
||||||
|
lokaal_map[b.lokaal_id]["blokken"].append(b)
|
||||||
|
for info in lokaal_map.values():
|
||||||
|
if len(info["blokken"]) > 1:
|
||||||
|
resultaat.append({
|
||||||
|
"type": "lokaal",
|
||||||
|
"ernst": "fout",
|
||||||
|
"tijdslot": slot_label,
|
||||||
|
"detail": f"Lokaal {info['naam']} is dubbel geboekt",
|
||||||
|
"blokken": [b.activiteit.naam for b in info["blokken"]],
|
||||||
|
})
|
||||||
|
|
||||||
# Onderbezetting (waarschuwing).
|
# Onderbezetting (waarschuwing).
|
||||||
for b in blokken:
|
for b in blokken:
|
||||||
if b.begeleiders.count() == 0:
|
if b.begeleiders.count() == 0:
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Stage, StageLocatie
|
from .models import Stage
|
||||||
|
|
||||||
|
|
||||||
@admin.register(StageLocatie)
|
|
||||||
class StageLocatieAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ("naam", "omschrijving", "actief")
|
|
||||||
search_fields = ("naam",)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Stage)
|
@admin.register(Stage)
|
||||||
class StageAdmin(admin.ModelAdmin):
|
class StageAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
"leerling", "type", "begeleider", "stagelocatie", "organisatie",
|
"leerling", "type", "begeleider", "lokaal", "organisatie",
|
||||||
"omvang", "dag", "dagdeel", "actief",
|
"omvang", "dag", "dagdeel", "actief",
|
||||||
)
|
)
|
||||||
list_filter = ("type", "omvang", "actief", "dag")
|
list_filter = ("type", "omvang", "actief", "dag")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 5.2.15 on 2026-06-16 06:11
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0006_lokaal_lokaalinstelling_roosterblok_lokaal'),
|
||||||
|
('module_stage', '0002_stagelocatie_omvang_dagdeel_tijdslot'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='stage',
|
||||||
|
name='stagelocatie',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='stage',
|
||||||
|
name='lokaal',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stages', to='core.lokaal'),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='StageLocatie',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
Stage-module: interne en externe stages van leerlingen.
|
Stage-module: interne en externe stages van leerlingen.
|
||||||
|
|
||||||
- Beide stagevormen krijgen een begeleider (verplicht).
|
- Beide stagevormen krijgen een begeleider (verplicht).
|
||||||
- Een interne stage krijgt daarnaast een stageruimte (eigen lijst, los van de
|
- Een interne stage gebruikt een LOKAAL (gedeelde pool met de lessen, met
|
||||||
gewone lokalen).
|
dubbele-boekingbewaking) — dus geen aparte stagelocatie-lijst meer.
|
||||||
- Een externe stage legt de externe organisatie vast.
|
- Een externe stage legt de externe organisatie vast.
|
||||||
- Een stage geldt voor een hele dag, een dagdeel of één specifiek roosterblok.
|
- Een stage geldt voor een hele dag, een dagdeel of één specifiek roosterblok.
|
||||||
"""
|
"""
|
||||||
|
|
@ -40,26 +40,6 @@ class Weekdag(models.IntegerChoices):
|
||||||
VRIJDAG = 4, "Vrijdag"
|
VRIJDAG = 4, "Vrijdag"
|
||||||
|
|
||||||
|
|
||||||
class StageLocatie(models.Model):
|
|
||||||
"""Een stageruimte/-plek, los van de gewone lokalen (core.Locatie).
|
|
||||||
|
|
||||||
Zo houd je de stagelocaties gescheiden van de lesruimtes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
naam = models.CharField("naam", max_length=120, unique=True)
|
|
||||||
omschrijving = models.CharField("omschrijving", max_length=255, blank=True)
|
|
||||||
actief = models.BooleanField("actief", default=True)
|
|
||||||
aangemaakt_op = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = "stagelocatie"
|
|
||||||
verbose_name_plural = "stagelocaties"
|
|
||||||
ordering = ["naam"]
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.naam
|
|
||||||
|
|
||||||
|
|
||||||
class Stage(models.Model):
|
class Stage(models.Model):
|
||||||
leerling = models.ForeignKey(
|
leerling = models.ForeignKey(
|
||||||
"core.Persoon", on_delete=models.CASCADE, related_name="stages",
|
"core.Persoon", on_delete=models.CASCADE, related_name="stages",
|
||||||
|
|
@ -70,9 +50,9 @@ class Stage(models.Model):
|
||||||
"core.Persoon", on_delete=models.PROTECT, related_name="stagebegeleidingen",
|
"core.Persoon", on_delete=models.PROTECT, related_name="stagebegeleidingen",
|
||||||
help_text="Verplicht voor zowel interne als externe stage.",
|
help_text="Verplicht voor zowel interne als externe stage.",
|
||||||
)
|
)
|
||||||
# Interne stage: een eigen stageruimte.
|
# Interne stage: een lokaal uit de gedeelde lokalen-pool.
|
||||||
stagelocatie = models.ForeignKey(
|
lokaal = models.ForeignKey(
|
||||||
StageLocatie, on_delete=models.SET_NULL, null=True, blank=True,
|
"core.Lokaal", on_delete=models.SET_NULL, null=True, blank=True,
|
||||||
related_name="stages",
|
related_name="stages",
|
||||||
)
|
)
|
||||||
# Externe stage: de externe organisatie.
|
# Externe stage: de externe organisatie.
|
||||||
|
|
@ -103,8 +83,8 @@ class Stage(models.Model):
|
||||||
ordering = ["leerling__achternaam", "leerling__voornaam"]
|
ordering = ["leerling__achternaam", "leerling__voornaam"]
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.type == StageType.INTERN and not self.stagelocatie_id:
|
if self.type == StageType.INTERN and not self.lokaal_id:
|
||||||
raise ValidationError("Een interne stage vereist een stageruimte.")
|
raise ValidationError("Een interne stage vereist een lokaal.")
|
||||||
if self.type == StageType.EXTERN and not self.organisatie:
|
if self.type == StageType.EXTERN and not self.organisatie:
|
||||||
raise ValidationError("Een externe stage vereist een externe organisatie.")
|
raise ValidationError("Een externe stage vereist een externe organisatie.")
|
||||||
if self.omvang == Omvang.BLOK and not self.tijdslot_id:
|
if self.omvang == Omvang.BLOK and not self.tijdslot_id:
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,12 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import Dagdeel, Omvang, Stage, StageLocatie, StageType
|
from .models import Dagdeel, Omvang, Stage, StageType
|
||||||
|
|
||||||
|
|
||||||
class StageLocatieSerializer(serializers.ModelSerializer):
|
|
||||||
aantal_stages = serializers.IntegerField(source="stages.count", read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = StageLocatie
|
|
||||||
fields = ["id", "naam", "omschrijving", "actief", "aantal_stages", "aangemaakt_op"]
|
|
||||||
read_only_fields = ["aangemaakt_op"]
|
|
||||||
|
|
||||||
|
|
||||||
class StageSerializer(serializers.ModelSerializer):
|
class StageSerializer(serializers.ModelSerializer):
|
||||||
leerling_naam = serializers.CharField(source="leerling.volledige_naam", read_only=True)
|
leerling_naam = serializers.CharField(source="leerling.volledige_naam", read_only=True)
|
||||||
begeleider_naam = serializers.CharField(source="begeleider.volledige_naam", read_only=True)
|
begeleider_naam = serializers.CharField(source="begeleider.volledige_naam", read_only=True)
|
||||||
stagelocatie_naam = serializers.CharField(source="stagelocatie.naam", read_only=True, default=None)
|
lokaal_naam = serializers.CharField(source="lokaal.naam", read_only=True, default=None)
|
||||||
type_label = serializers.CharField(source="get_type_display", read_only=True)
|
type_label = serializers.CharField(source="get_type_display", read_only=True)
|
||||||
omvang_label = serializers.CharField(source="get_omvang_display", read_only=True)
|
omvang_label = serializers.CharField(source="get_omvang_display", read_only=True)
|
||||||
dagdeel_label = serializers.CharField(source="get_dagdeel_display", read_only=True, default=None)
|
dagdeel_label = serializers.CharField(source="get_dagdeel_display", read_only=True, default=None)
|
||||||
|
|
@ -28,7 +19,7 @@ class StageSerializer(serializers.ModelSerializer):
|
||||||
fields = [
|
fields = [
|
||||||
"id", "leerling", "leerling_naam", "type", "type_label",
|
"id", "leerling", "leerling_naam", "type", "type_label",
|
||||||
"begeleider", "begeleider_naam",
|
"begeleider", "begeleider_naam",
|
||||||
"stagelocatie", "stagelocatie_naam", "organisatie",
|
"lokaal", "lokaal_naam", "organisatie",
|
||||||
"omvang", "omvang_label", "dag", "dag_label", "effectieve_dag",
|
"omvang", "omvang_label", "dag", "dag_label", "effectieve_dag",
|
||||||
"dagdeel", "dagdeel_label", "tijdslot", "tijdslot_label",
|
"dagdeel", "dagdeel_label", "tijdslot", "tijdslot_label",
|
||||||
"omschrijving", "actief", "aangemaakt_op",
|
"omschrijving", "actief", "aangemaakt_op",
|
||||||
|
|
@ -40,7 +31,7 @@ class StageSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
type_ = self._val(data, "type")
|
type_ = self._val(data, "type")
|
||||||
stagelocatie = self._val(data, "stagelocatie")
|
lokaal = self._val(data, "lokaal")
|
||||||
organisatie = self._val(data, "organisatie", "")
|
organisatie = self._val(data, "organisatie", "")
|
||||||
begeleider = self._val(data, "begeleider")
|
begeleider = self._val(data, "begeleider")
|
||||||
omvang = self._val(data, "omvang", Omvang.HELE_DAG)
|
omvang = self._val(data, "omvang", Omvang.HELE_DAG)
|
||||||
|
|
@ -50,8 +41,8 @@ class StageSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
if begeleider is not None and begeleider.rol == "leerling":
|
if begeleider is not None and begeleider.rol == "leerling":
|
||||||
raise serializers.ValidationError("Een leerling kan geen begeleider zijn.")
|
raise serializers.ValidationError("Een leerling kan geen begeleider zijn.")
|
||||||
if type_ == StageType.INTERN and not stagelocatie:
|
if type_ == StageType.INTERN and not lokaal:
|
||||||
raise serializers.ValidationError("Een interne stage vereist een stageruimte.")
|
raise serializers.ValidationError("Een interne stage vereist een lokaal.")
|
||||||
if type_ == StageType.EXTERN and not organisatie:
|
if type_ == StageType.EXTERN and not organisatie:
|
||||||
raise serializers.ValidationError("Een externe stage vereist een externe organisatie.")
|
raise serializers.ValidationError("Een externe stage vereist een externe organisatie.")
|
||||||
if omvang == Omvang.BLOK and not tijdslot:
|
if omvang == Omvang.BLOK and not tijdslot:
|
||||||
|
|
@ -69,9 +60,66 @@ class StageSerializer(serializers.ModelSerializer):
|
||||||
f"{begeleider.volledige_naam} begeleidt op dat moment al het "
|
f"{begeleider.volledige_naam} begeleidt op dat moment al het "
|
||||||
f"theorievak '{botsing}' en kan niet tegelijk stage begeleiden."
|
f"theorievak '{botsing}' en kan niet tegelijk stage begeleiden."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Dubbele-lokaalboeking (interne stage): bij harde modus weigeren.
|
||||||
|
if lokaal is not None:
|
||||||
|
from core.models import Lokaalinstelling
|
||||||
|
if Lokaalinstelling.actueel().dubbel_blokkeren:
|
||||||
|
bezet = _lokaal_bezet_door_ander(
|
||||||
|
lokaal, omvang, dag, dagdeel, tijdslot,
|
||||||
|
exclude_stage_id=getattr(self.instance, "id", None),
|
||||||
|
)
|
||||||
|
if bezet:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"Lokaal '{lokaal.naam}' is op dat moment al bezet ({bezet})."
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _gedekte_tijdsloten(omvang, dag, dagdeel, tijdslot):
|
||||||
|
"""Geef de core.Tijdslot-objecten die door deze stage-omvang gedekt worden."""
|
||||||
|
from core.models import Tijdslot
|
||||||
|
from .models import MIDDAG_GRENS, Omvang as _Omvang, Dagdeel as _Dagdeel
|
||||||
|
|
||||||
|
if omvang == _Omvang.BLOK:
|
||||||
|
return [tijdslot] if tijdslot is not None else []
|
||||||
|
if dag is None:
|
||||||
|
return []
|
||||||
|
sloten = list(Tijdslot.objects.filter(dag=dag))
|
||||||
|
if omvang == _Omvang.HELE_DAG:
|
||||||
|
return sloten
|
||||||
|
out = []
|
||||||
|
for t in sloten:
|
||||||
|
is_ochtend = t.start_tijd < MIDDAG_GRENS
|
||||||
|
if (dagdeel == _Dagdeel.OCHTEND and is_ochtend) or (dagdeel == _Dagdeel.MIDDAG and not is_ochtend):
|
||||||
|
out.append(t)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _lokaal_bezet_door_ander(lokaal, omvang, dag, dagdeel, tijdslot, exclude_stage_id=None):
|
||||||
|
"""Geef een omschrijving van wie het lokaal al bezet, of None."""
|
||||||
|
from core.models import Roosterblok
|
||||||
|
sloten = _gedekte_tijdsloten(omvang, dag, dagdeel, tijdslot)
|
||||||
|
slot_ids = [t.id for t in sloten]
|
||||||
|
if not slot_ids:
|
||||||
|
return None
|
||||||
|
les = (
|
||||||
|
Roosterblok.objects
|
||||||
|
.filter(lokaal=lokaal, tijdslot_id__in=slot_ids)
|
||||||
|
.select_related("activiteit").first()
|
||||||
|
)
|
||||||
|
if les:
|
||||||
|
return f"les {les.activiteit.naam}"
|
||||||
|
# Andere stages die hetzelfde lokaal op een overlappend tijdslot gebruiken.
|
||||||
|
qs = Stage.objects.filter(actief=True, lokaal=lokaal).exclude(type=StageType.EXTERN)
|
||||||
|
if exclude_stage_id:
|
||||||
|
qs = qs.exclude(pk=exclude_stage_id)
|
||||||
|
for st in qs:
|
||||||
|
if any(st.dekt_tijdslot(t) for t in sloten):
|
||||||
|
return f"stage van {st.leerling.volledige_naam}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _theorievak_botsing(begeleider, omvang, dag, dagdeel, tijdslot):
|
def _theorievak_botsing(begeleider, omvang, dag, dagdeel, tijdslot):
|
||||||
"""Zoek een theorievak-roosterblok dat de begeleider op hetzelfde moment doet."""
|
"""Zoek een theorievak-roosterblok dat de begeleider op hetzelfde moment doet."""
|
||||||
from core.models import Roosterblok
|
from core.models import Roosterblok
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ from . import views
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register("stages", views.StageViewSet, basename="stage")
|
router.register("stages", views.StageViewSet, basename="stage")
|
||||||
router.register("stagelocaties", views.StageLocatieViewSet, basename="stagelocatie")
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("stage-conflicten/", views.stage_conflicten, name="stage-conflicten"),
|
path("stage-conflicten/", views.stage_conflicten, name="stage-conflicten"),
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,8 @@ from rest_framework import viewsets
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from .models import Stage, StageLocatie
|
from .models import Stage
|
||||||
from .serializers import StageLocatieSerializer, StageSerializer, _theorievak_botsing
|
from .serializers import StageSerializer, _theorievak_botsing, _lokaal_bezet_door_ander
|
||||||
|
|
||||||
|
|
||||||
class StageLocatieViewSet(viewsets.ModelViewSet):
|
|
||||||
queryset = StageLocatie.objects.all()
|
|
||||||
serializer_class = StageLocatieSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class StageViewSet(viewsets.ModelViewSet):
|
class StageViewSet(viewsets.ModelViewSet):
|
||||||
|
|
@ -16,7 +11,7 @@ class StageViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = Stage.objects.select_related(
|
qs = Stage.objects.select_related(
|
||||||
"leerling", "begeleider", "stagelocatie", "tijdslot"
|
"leerling", "begeleider", "lokaal", "tijdslot"
|
||||||
).all()
|
).all()
|
||||||
type_ = self.request.query_params.get("type")
|
type_ = self.request.query_params.get("type")
|
||||||
if type_:
|
if type_:
|
||||||
|
|
@ -32,9 +27,9 @@ class StageViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
def stage_conflicten(request):
|
def stage_conflicten(request):
|
||||||
"""Signaleer begeleiders die tegelijk stage én een theorievak doen."""
|
"""Signaleer stage-conflicten: begeleider op theorievak + dubbele lokaalboeking."""
|
||||||
resultaat = []
|
resultaat = []
|
||||||
for stage in Stage.objects.filter(actief=True).select_related("leerling", "begeleider"):
|
for stage in Stage.objects.filter(actief=True).select_related("leerling", "begeleider", "lokaal"):
|
||||||
botsing = _theorievak_botsing(
|
botsing = _theorievak_botsing(
|
||||||
stage.begeleider, stage.omvang, stage.dag, stage.dagdeel, stage.tijdslot
|
stage.begeleider, stage.omvang, stage.dag, stage.dagdeel, stage.tijdslot
|
||||||
)
|
)
|
||||||
|
|
@ -49,6 +44,22 @@ def stage_conflicten(request):
|
||||||
),
|
),
|
||||||
"blokken": [botsing],
|
"blokken": [botsing],
|
||||||
})
|
})
|
||||||
|
if stage.lokaal_id:
|
||||||
|
bezet = _lokaal_bezet_door_ander(
|
||||||
|
stage.lokaal, stage.omvang, stage.dag, stage.dagdeel, stage.tijdslot,
|
||||||
|
exclude_stage_id=stage.id,
|
||||||
|
)
|
||||||
|
if bezet:
|
||||||
|
resultaat.append({
|
||||||
|
"type": "lokaal",
|
||||||
|
"ernst": "fout",
|
||||||
|
"tijdslot": stage.get_omvang_display(),
|
||||||
|
"detail": (
|
||||||
|
f"Lokaal '{stage.lokaal.naam}' van de stage van "
|
||||||
|
f"{stage.leerling.volledige_naam} botst met {bezet}."
|
||||||
|
),
|
||||||
|
"blokken": [stage.lokaal.naam],
|
||||||
|
})
|
||||||
fouten = sum(1 for r in resultaat if r["ernst"] == "fout")
|
fouten = sum(1 for r in resultaat if r["ernst"] == "fout")
|
||||||
return Response({
|
return Response({
|
||||||
"aantal": len(resultaat),
|
"aantal": len(resultaat),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { getMe, login, logout, getModules, setModuleState } from "./api.js";
|
||||||
import Logo from "./Logo.jsx";
|
import Logo from "./Logo.jsx";
|
||||||
import {
|
import {
|
||||||
PersonenPage, FunctiesPage,
|
PersonenPage, FunctiesPage,
|
||||||
ActiviteitenPage, LocatiesPage, TijdslotenPage, SchooljaarPage,
|
ActiviteitenPage, LocatiesPage, LokalenPage, TijdslotenPage, SchooljaarPage,
|
||||||
RoosterWeergavePage, RoostermakerPage,
|
RoosterWeergavePage, RoostermakerPage,
|
||||||
AfwezigheidPage, GroepsindelingPage, StagePage, GebruikersPage,
|
AfwezigheidPage, GroepsindelingPage, StagePage, GebruikersPage,
|
||||||
} from "./pages.jsx";
|
} from "./pages.jsx";
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
calendar: "📅", users: "👥", layers: "🗂️", printer: "🖨️",
|
calendar: "📅", users: "👥", layers: "🗂️", printer: "🖨️",
|
||||||
key: "🔑", puzzle: "🧩", badge: "🪪", book: "📚", pin: "📍", clock: "🕘",
|
key: "🔑", puzzle: "🧩", badge: "🪪", book: "📚", pin: "📍", clock: "🕘",
|
||||||
away: "🌴", gear: "⚙️",
|
away: "🌴", gear: "⚙️", door: "🚪",
|
||||||
};
|
};
|
||||||
|
|
||||||
const GROEPSINDELING_PADEN = new Set(["/groepsindeling", "/structuur", "/leerplein", "/groepen", "/subgroepen"]);
|
const GROEPSINDELING_PADEN = new Set(["/groepsindeling", "/structuur", "/leerplein", "/groepen", "/subgroepen"]);
|
||||||
|
|
@ -28,6 +28,7 @@ const PAGES = {
|
||||||
"/functies": FunctiesPage,
|
"/functies": FunctiesPage,
|
||||||
"/activiteiten": ActiviteitenPage,
|
"/activiteiten": ActiviteitenPage,
|
||||||
"/locaties": LocatiesPage,
|
"/locaties": LocatiesPage,
|
||||||
|
"/lokalen": LokalenPage,
|
||||||
"/tijdsloten": TijdslotenPage,
|
"/tijdsloten": TijdslotenPage,
|
||||||
"/schooljaar": SchooljaarPage,
|
"/schooljaar": SchooljaarPage,
|
||||||
"/afwezigheid": AfwezigheidPage,
|
"/afwezigheid": AfwezigheidPage,
|
||||||
|
|
|
||||||
|
|
@ -57,3 +57,7 @@ export const updateResource = (resource, id, body) =>
|
||||||
request("PATCH", `/${resource}/${id}/`, body);
|
request("PATCH", `/${resource}/${id}/`, body);
|
||||||
export const deleteResource = (resource, id) =>
|
export const deleteResource = (resource, id) =>
|
||||||
request("DELETE", `/${resource}/${id}/`);
|
request("DELETE", `/${resource}/${id}/`);
|
||||||
|
|
||||||
|
// --- Lokaal-instelling (singleton; dubbele-boekingbeleid) ------------------
|
||||||
|
export const getLokaalinstelling = () => request("GET", "/lokaalinstelling/");
|
||||||
|
export const setLokaalinstelling = (data) => request("PUT", "/lokaalinstelling/", data);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import {
|
||||||
updateResource,
|
updateResource,
|
||||||
deleteResource,
|
deleteResource,
|
||||||
getModules,
|
getModules,
|
||||||
|
getLokaalinstelling,
|
||||||
|
setLokaalinstelling,
|
||||||
} from "./api.js";
|
} from "./api.js";
|
||||||
|
|
||||||
const ROLLEN = [
|
const ROLLEN = [
|
||||||
|
|
@ -217,12 +219,85 @@ export function LocatiesPage() {
|
||||||
return (
|
return (
|
||||||
<CatalogusPage
|
<CatalogusPage
|
||||||
titel="Locaties"
|
titel="Locaties"
|
||||||
uitleg="Lokalen en ruimtes. Worden gebruikt voor conflictdetectie."
|
uitleg="Vrije locatie-labels (bijv. externe plekken). Voor lesruimtes met dubbele-boekingcheck: zie Lokalen."
|
||||||
resource="locaties"
|
resource="locaties"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function LokalenPage() {
|
||||||
|
const { items, error, loading, reload, setError } = useList("lokalen");
|
||||||
|
const leeg = { naam: "", omschrijving: "", capaciteit: "" };
|
||||||
|
const [form, setForm] = useState(leeg);
|
||||||
|
const [editId, setEditId] = useState(null);
|
||||||
|
const [blokkeren, setBlokkeren] = useState(false);
|
||||||
|
const [instError, setInstError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getLokaalinstelling().then((d) => setBlokkeren(!!d.dubbel_blokkeren)).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function toggleBlokkeren(val) {
|
||||||
|
setBlokkeren(val);
|
||||||
|
try { await setLokaalinstelling({ dubbel_blokkeren: val }); setInstError(null); }
|
||||||
|
catch (err) { setInstError(err.message); setBlokkeren(!val); }
|
||||||
|
}
|
||||||
|
async function opslaan(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const payload = { naam: form.naam, omschrijving: form.omschrijving, capaciteit: form.capaciteit === "" ? null : Number(form.capaciteit) };
|
||||||
|
try {
|
||||||
|
if (editId) await updateResource("lokalen", editId, payload);
|
||||||
|
else await createResource("lokalen", payload);
|
||||||
|
setForm(leeg); setEditId(null); await reload();
|
||||||
|
} catch (err) { setError(err.message); }
|
||||||
|
}
|
||||||
|
async function verwijder(id) {
|
||||||
|
if (!confirm("Dit lokaal verwijderen?")) return;
|
||||||
|
try { await deleteResource("lokalen", id); await reload(); } catch (err) { setError(err.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h1>Lokalen</h1>
|
||||||
|
<p className="muted">Lesruimtes die door lessen én interne stages gedeeld worden. Het systeem bewaakt dubbele boekingen per tijdslot.</p>
|
||||||
|
<Foutmelding msg={error} />
|
||||||
|
<Foutmelding msg={instError} />
|
||||||
|
<label className="veld" style={{ marginBottom: 12 }}>
|
||||||
|
<input type="checkbox" checked={blokkeren} onChange={(e) => toggleBlokkeren(e.target.checked)} />
|
||||||
|
{" "}Dubbele boeking hard blokkeren (anders alleen waarschuwen in het conflicten-overzicht)
|
||||||
|
</label>
|
||||||
|
<form className="form-card" onSubmit={opslaan}>
|
||||||
|
<div className="form-row">
|
||||||
|
<input placeholder="Naam" value={form.naam} required onChange={(e) => setForm({ ...form, naam: e.target.value })} />
|
||||||
|
<input placeholder="Omschrijving (optioneel)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} />
|
||||||
|
<input type="number" min="1" placeholder="Capaciteit (optioneel)" value={form.capaciteit} onChange={(e) => setForm({ ...form, capaciteit: e.target.value })} />
|
||||||
|
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
|
||||||
|
{editId && <button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{loading ? <p>Laden…</p> : (
|
||||||
|
<table className="grid">
|
||||||
|
<thead><tr><th>Naam</th><th>Capaciteit</th><th>Omschrijving</th><th></th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{items.map((l) => (
|
||||||
|
<tr key={l.id}>
|
||||||
|
<td>{l.naam}</td>
|
||||||
|
<td className="muted">{l.capaciteit ?? "—"}</td>
|
||||||
|
<td className="muted">{l.omschrijving || "—"}</td>
|
||||||
|
<td className="row-actions">
|
||||||
|
<button className="link" onClick={() => { setEditId(l.id); setForm({ naam: l.naam, omschrijving: l.omschrijving || "", capaciteit: l.capaciteit ?? "" }); }}>bewerken</button>
|
||||||
|
<button className="link danger" onClick={() => verwijder(l.id)}>verwijderen</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{items.length === 0 && <tr><td colSpan="4" className="muted">Nog geen lokalen.</td></tr>}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------------------- Groepen -------------------------------- */
|
/* --------------------------------- Groepen -------------------------------- */
|
||||||
export function GroepenPage() {
|
export function GroepenPage() {
|
||||||
return <GroepsindelingPage />;
|
return <GroepsindelingPage />;
|
||||||
|
|
@ -1075,7 +1150,7 @@ export function GroepsindelingPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------- Stage --------------------------------- */
|
/* ---------------------------------- Stage --------------------------------- */
|
||||||
// Module-scherm: interne en externe stages + stagelocaties.
|
// Module-scherm: interne en externe stages.
|
||||||
const OMVANGEN = [
|
const OMVANGEN = [
|
||||||
{ value: "hele_dag", label: "Hele dag" },
|
{ value: "hele_dag", label: "Hele dag" },
|
||||||
{ value: "dagdeel", label: "Dagdeel" },
|
{ value: "dagdeel", label: "Dagdeel" },
|
||||||
|
|
@ -1090,20 +1165,17 @@ export function StagePage() {
|
||||||
const { items, error, loading, reload, setError } = useList("stages");
|
const { items, error, loading, reload, setError } = useList("stages");
|
||||||
const [leerlingen, setLeerlingen] = useState([]);
|
const [leerlingen, setLeerlingen] = useState([]);
|
||||||
const [medewerkers, setMedewerkers] = useState([]);
|
const [medewerkers, setMedewerkers] = useState([]);
|
||||||
const [stagelocaties, setStagelocaties] = useState([]);
|
const [lokalen, setLokalen] = useState([]);
|
||||||
const [tijdsloten, setTijdsloten] = useState([]);
|
const [tijdsloten, setTijdsloten] = useState([]);
|
||||||
const leeg = { leerling: "", type: "intern", begeleider: "", stagelocatie: "", organisatie: "", omvang: "hele_dag", dag: "", dagdeel: "", tijdslot: "", omschrijving: "" };
|
const leeg = { leerling: "", type: "intern", begeleider: "", lokaal: "", organisatie: "", omvang: "hele_dag", dag: "", dagdeel: "", tijdslot: "", omschrijving: "" };
|
||||||
const [form, setForm] = useState(leeg);
|
const [form, setForm] = useState(leeg);
|
||||||
const [editId, setEditId] = useState(null);
|
const [editId, setEditId] = useState(null);
|
||||||
|
|
||||||
async function laadStagelocaties() {
|
|
||||||
try { setStagelocaties(await listResource("stagelocaties")); } catch { setStagelocaties([]); }
|
|
||||||
}
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
|
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
|
||||||
listResource("personen").then((ps) => setMedewerkers(ps.filter((p) => p.rol !== "leerling"))).catch(() => {});
|
listResource("personen").then((ps) => setMedewerkers(ps.filter((p) => p.rol !== "leerling"))).catch(() => {});
|
||||||
listResource("tijdsloten").then(setTijdsloten).catch(() => {});
|
listResource("tijdsloten").then(setTijdsloten).catch(() => {});
|
||||||
laadStagelocaties();
|
listResource("lokalen").then(setLokalen).catch(() => setLokalen([]));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isIntern = form.type === "intern";
|
const isIntern = form.type === "intern";
|
||||||
|
|
@ -1114,7 +1186,7 @@ export function StagePage() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const payload = {
|
const payload = {
|
||||||
leerling: form.leerling, type: form.type, begeleider: form.begeleider,
|
leerling: form.leerling, type: form.type, begeleider: form.begeleider,
|
||||||
stagelocatie: isIntern ? (form.stagelocatie || null) : null,
|
lokaal: isIntern ? (form.lokaal || null) : null,
|
||||||
organisatie: isIntern ? "" : form.organisatie,
|
organisatie: isIntern ? "" : form.organisatie,
|
||||||
omvang: form.omvang,
|
omvang: form.omvang,
|
||||||
dag: isBlok ? null : (form.dag === "" ? null : Number(form.dag)),
|
dag: isBlok ? null : (form.dag === "" ? null : Number(form.dag)),
|
||||||
|
|
@ -1148,7 +1220,7 @@ export function StagePage() {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1>Stage</h1>
|
<h1>Stage</h1>
|
||||||
<p className="muted">Interne en externe stages. Beide krijgen een begeleider; een interne stage een stageruimte. Stel in per hele dag, dagdeel of los blok.</p>
|
<p className="muted">Interne en externe stages. Beide krijgen een begeleider; een interne stage gebruikt een lokaal (met dubbele-boekingcheck). Stel in per hele dag, dagdeel of los blok.</p>
|
||||||
<Foutmelding msg={error} />
|
<Foutmelding msg={error} />
|
||||||
<form className="form-card" onSubmit={opslaan}>
|
<form className="form-card" onSubmit={opslaan}>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
|
|
@ -1167,32 +1239,46 @@ export function StagePage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
{isIntern ? (
|
{isIntern ? (
|
||||||
<select value={form.stagelocatie} required onChange={(e) => setForm({ ...form, stagelocatie: e.target.value })}>
|
<select value={form.lokaal} required onChange={(e) => setForm({ ...form, lokaal: e.target.value })}>
|
||||||
<option value="">— stageruimte —</option>
|
<option value="">— lokaal —</option>
|
||||||
{stagelocaties.map((l) => <option key={l.id} value={l.id}>{l.naam}</option>)}
|
{lokalen.map((l) => <option key={l.id} value={l.id}>{l.naam}</option>)}
|
||||||
</select>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
<input placeholder="Externe organisatie" value={form.organisatie} required onChange={(e) => setForm({ ...form, organisatie: e.target.value })} />
|
<input placeholder="Externe organisatie" value={form.organisatie} required onChange={(e) => setForm({ ...form, organisatie: e.target.value })} />
|
||||||
)}
|
)}
|
||||||
<select value={form.omvang} onChange={(e) => setForm({ ...form, omvang: e.target.value })}>
|
</div>
|
||||||
{OMVANGEN.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
<div className="form-row">
|
||||||
</select>
|
<label className="veld-kol">
|
||||||
|
<span className="veld-kop">Omvang</span>
|
||||||
|
<select value={form.omvang} onChange={(e) => setForm({ ...form, omvang: e.target.value })}>
|
||||||
|
{OMVANGEN.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
{isBlok ? (
|
{isBlok ? (
|
||||||
<select value={form.tijdslot} required onChange={(e) => setForm({ ...form, tijdslot: e.target.value })}>
|
<label className="veld-kol">
|
||||||
<option value="">— roosterblok —</option>
|
<span className="veld-kop">Roosterblok</span>
|
||||||
{tijdsloten.map((t) => <option key={t.id} value={t.id}>{tijdslotLabel(t.id)}</option>)}
|
<select value={form.tijdslot} required onChange={(e) => setForm({ ...form, tijdslot: e.target.value })}>
|
||||||
</select>
|
<option value="">— roosterblok —</option>
|
||||||
|
{tijdsloten.map((t) => <option key={t.id} value={t.id}>{tijdslotLabel(t.id)}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
) : (
|
) : (
|
||||||
<select value={form.dag} required onChange={(e) => setForm({ ...form, dag: e.target.value })}>
|
<label className="veld-kol">
|
||||||
<option value="">— dag —</option>
|
<span className="veld-kop">Dag</span>
|
||||||
{WEEKDAGEN.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
|
<select value={form.dag} required onChange={(e) => setForm({ ...form, dag: e.target.value })}>
|
||||||
</select>
|
<option value="">— dag —</option>
|
||||||
|
{WEEKDAGEN.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
)}
|
)}
|
||||||
{isDagdeel && (
|
{isDagdeel && (
|
||||||
<select value={form.dagdeel} required onChange={(e) => setForm({ ...form, dagdeel: e.target.value })}>
|
<label className="veld-kol">
|
||||||
<option value="">— dagdeel —</option>
|
<span className="veld-kop">Dagdeel</span>
|
||||||
{DAGDELEN.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
|
<select value={form.dagdeel} required onChange={(e) => setForm({ ...form, dagdeel: e.target.value })}>
|
||||||
</select>
|
<option value="">— kies dagdeel —</option>
|
||||||
|
{DAGDELEN.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
|
|
@ -1212,10 +1298,10 @@ export function StagePage() {
|
||||||
<td>{s.leerling_naam}</td>
|
<td>{s.leerling_naam}</td>
|
||||||
<td>{s.type_label}</td>
|
<td>{s.type_label}</td>
|
||||||
<td>{s.begeleider_naam}</td>
|
<td>{s.begeleider_naam}</td>
|
||||||
<td className="muted">{s.type === "intern" ? (s.stagelocatie_naam || "—") : (s.organisatie || "—")}</td>
|
<td className="muted">{s.type === "intern" ? (s.lokaal_naam || "—") : (s.organisatie || "—")}</td>
|
||||||
<td className="muted">{wanneerTekst(s)}</td>
|
<td className="muted">{wanneerTekst(s)}</td>
|
||||||
<td className="row-actions">
|
<td className="row-actions">
|
||||||
<button className="link" onClick={() => { setEditId(s.id); setForm({ leerling: s.leerling, type: s.type, begeleider: s.begeleider, stagelocatie: s.stagelocatie || "", organisatie: s.organisatie || "", omvang: s.omvang || "hele_dag", dag: s.dag ?? "", dagdeel: s.dagdeel || "", tijdslot: s.tijdslot || "", omschrijving: s.omschrijving || "" }); }}>bewerken</button>
|
<button className="link" onClick={() => { setEditId(s.id); setForm({ leerling: s.leerling, type: s.type, begeleider: s.begeleider, lokaal: s.lokaal || "", organisatie: s.organisatie || "", omvang: s.omvang || "hele_dag", dag: s.dag ?? "", dagdeel: s.dagdeel || "", tijdslot: s.tijdslot || "", omschrijving: s.omschrijving || "" }); }}>bewerken</button>
|
||||||
<button className="link danger" onClick={() => verwijder(s.id)}>verwijderen</button>
|
<button className="link danger" onClick={() => verwijder(s.id)}>verwijderen</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -1224,63 +1310,10 @@ export function StagePage() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
)}
|
)}
|
||||||
<StagelocatieBeheer locaties={stagelocaties} onChange={laadStagelocaties} />
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StagelocatieBeheer({ locaties, onChange }) {
|
|
||||||
const leeg = { naam: "", omschrijving: "" };
|
|
||||||
const [form, setForm] = useState(leeg);
|
|
||||||
const [editId, setEditId] = useState(null);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
async function opslaan(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
if (editId) await updateResource("stagelocaties", editId, form);
|
|
||||||
else await createResource("stagelocaties", form);
|
|
||||||
setForm(leeg); setEditId(null); await onChange();
|
|
||||||
} catch (err) { setError(err.message); }
|
|
||||||
}
|
|
||||||
async function verwijder(id) {
|
|
||||||
if (!confirm("Deze stagelocatie verwijderen?")) return;
|
|
||||||
try { await deleteResource("stagelocaties", id); await onChange(); } catch (err) { setError(err.message); }
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="subsectie">
|
|
||||||
<h2>Stagelocaties</h2>
|
|
||||||
<p className="muted">Aparte stageruimtes, los van de gewone lokalen.</p>
|
|
||||||
<Foutmelding msg={error} />
|
|
||||||
<form className="form-card" onSubmit={opslaan}>
|
|
||||||
<div className="form-row">
|
|
||||||
<input placeholder="Naam" value={form.naam} required onChange={(e) => setForm({ ...form, naam: e.target.value })} />
|
|
||||||
<input placeholder="Omschrijving (optioneel)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} />
|
|
||||||
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
|
|
||||||
{editId && <button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<table className="grid">
|
|
||||||
<thead><tr><th>Naam</th><th>Omschrijving</th><th></th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
{locaties.map((l) => (
|
|
||||||
<tr key={l.id}>
|
|
||||||
<td>{l.naam}</td>
|
|
||||||
<td className="muted">{l.omschrijving || "—"}</td>
|
|
||||||
<td className="row-actions">
|
|
||||||
<button className="link" onClick={() => { setEditId(l.id); setForm({ naam: l.naam, omschrijving: l.omschrijving || "" }); }}>bewerken</button>
|
|
||||||
<button className="link danger" onClick={() => verwijder(l.id)}>verwijderen</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{locaties.length === 0 && <tr><td colSpan="3" className="muted">Nog geen stagelocaties.</td></tr>}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------- Gebruikers ------------------------------ */
|
/* -------------------------------- Gebruikers ------------------------------ */
|
||||||
const GEBRUIKERSROLLEN = [
|
const GEBRUIKERSROLLEN = [
|
||||||
{ value: "beheerder", label: "Beheerder" },
|
{ value: "beheerder", label: "Beheerder" },
|
||||||
|
|
@ -1459,11 +1492,12 @@ function bovenliggendeGroep(blok, subgroepen) {
|
||||||
|
|
||||||
/* ----------------------------- Blok bewerken/kopiëren --------------------- */
|
/* ----------------------------- Blok bewerken/kopiëren --------------------- */
|
||||||
function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
|
function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
|
||||||
const { activiteiten, locaties, groepen, subgroepen, leerlingen, medewerkers, tijdsloten } = ctx;
|
const { activiteiten, locaties, lokalen, groepen, subgroepen, leerlingen, medewerkers, tijdsloten } = ctx;
|
||||||
const [f, setF] = useState({
|
const [f, setF] = useState({
|
||||||
tijdslot: blok.tijdslot,
|
tijdslot: blok.tijdslot,
|
||||||
activiteit: blok.activiteit,
|
activiteit: blok.activiteit,
|
||||||
locatie: blok.locatie || "",
|
locatie: blok.locatie || "",
|
||||||
|
lokaal: blok.lokaal || "",
|
||||||
doel_type: blok.doel_type,
|
doel_type: blok.doel_type,
|
||||||
doel: blok.groep || blok.subgroep || blok.leerling || "",
|
doel: blok.groep || blok.subgroep || blok.leerling || "",
|
||||||
begeleiders: [...(blok.begeleiders || [])],
|
begeleiders: [...(blok.begeleiders || [])],
|
||||||
|
|
@ -1514,7 +1548,7 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
|
||||||
if (splitSubs.length === 0) return;
|
if (splitSubs.length === 0) return;
|
||||||
try {
|
try {
|
||||||
for (const sid of splitSubs) {
|
for (const sid of splitSubs) {
|
||||||
await createResource("roosterblokken", { schooljaar: blok.schooljaar, tijdslot: f.tijdslot, activiteit: f.activiteit, locatie: f.locatie || null, subgroep: sid, begeleiders: f.begeleiders, opmerkingen: f.opmerkingen });
|
await createResource("roosterblokken", { schooljaar: blok.schooljaar, tijdslot: f.tijdslot, activiteit: f.activiteit, locatie: f.locatie || null, lokaal: f.lokaal || null, subgroep: sid, begeleiders: f.begeleiders, opmerkingen: f.opmerkingen });
|
||||||
}
|
}
|
||||||
if (verwijderOrig) await deleteResource("roosterblokken", blok.id);
|
if (verwijderOrig) await deleteResource("roosterblokken", blok.id);
|
||||||
await onKlaar();
|
await onKlaar();
|
||||||
|
|
@ -1528,13 +1562,13 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
|
||||||
leerling: f.doel_type === "leerling" ? f.doel : null,
|
leerling: f.doel_type === "leerling" ? f.doel : null,
|
||||||
});
|
});
|
||||||
async function opslaan() {
|
async function opslaan() {
|
||||||
try { await updateResource("roosterblokken", blok.id, { tijdslot: f.tijdslot, activiteit: f.activiteit, locatie: f.locatie || null, ...doelPayload(), begeleiders: f.begeleiders, opmerkingen: f.opmerkingen }); await onKlaar(); }
|
try { await updateResource("roosterblokken", blok.id, { tijdslot: f.tijdslot, activiteit: f.activiteit, locatie: f.locatie || null, lokaal: f.lokaal || null, ...doelPayload(), begeleiders: f.begeleiders, opmerkingen: f.opmerkingen }); await onKlaar(); }
|
||||||
catch (e) { setError(e.message); }
|
catch (e) { setError(e.message); }
|
||||||
}
|
}
|
||||||
async function kopieer() {
|
async function kopieer() {
|
||||||
try {
|
try {
|
||||||
for (const ts of kopieDoelen) {
|
for (const ts of kopieDoelen) {
|
||||||
await createResource("roosterblokken", { schooljaar: blok.schooljaar, tijdslot: ts, activiteit: f.activiteit, locatie: f.locatie || null, ...doelPayload(), begeleiders: f.begeleiders, opmerkingen: f.opmerkingen });
|
await createResource("roosterblokken", { schooljaar: blok.schooljaar, tijdslot: ts, activiteit: f.activiteit, locatie: f.locatie || null, lokaal: f.lokaal || null, ...doelPayload(), begeleiders: f.begeleiders, opmerkingen: f.opmerkingen });
|
||||||
}
|
}
|
||||||
await onKlaar();
|
await onKlaar();
|
||||||
} catch (e) { setError(e.message); }
|
} catch (e) { setError(e.message); }
|
||||||
|
|
@ -1570,6 +1604,10 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
|
||||||
<option value="">— locatie —</option>
|
<option value="">— locatie —</option>
|
||||||
{locaties.map((l) => <option key={l.id} value={l.id}>{l.naam}</option>)}
|
{locaties.map((l) => <option key={l.id} value={l.id}>{l.naam}</option>)}
|
||||||
</select>
|
</select>
|
||||||
|
<select value={f.lokaal} title="Lokaal" onChange={(e) => setF({ ...f, lokaal: e.target.value })}>
|
||||||
|
<option value="">— lokaal —</option>
|
||||||
|
{lokalen.map((l) => <option key={l.id} value={l.id}>{l.naam}</option>)}
|
||||||
|
</select>
|
||||||
<input placeholder="Opmerking" value={f.opmerkingen} onChange={(e) => setF({ ...f, opmerkingen: e.target.value })} />
|
<input placeholder="Opmerking" value={f.opmerkingen} onChange={(e) => setF({ ...f, opmerkingen: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1779,6 +1817,7 @@ export function RoosterScherm({ mode }) {
|
||||||
const [tijdsloten, setTijdsloten] = useState([]);
|
const [tijdsloten, setTijdsloten] = useState([]);
|
||||||
const [activiteiten, setActiviteiten] = useState([]);
|
const [activiteiten, setActiviteiten] = useState([]);
|
||||||
const [locaties, setLocaties] = useState([]);
|
const [locaties, setLocaties] = useState([]);
|
||||||
|
const [lokalen, setLokalen] = useState([]);
|
||||||
const [groepen, setGroepen] = useState([]);
|
const [groepen, setGroepen] = useState([]);
|
||||||
const [subgroepen, setSubgroepen] = useState([]);
|
const [subgroepen, setSubgroepen] = useState([]);
|
||||||
const [leerlingen, setLeerlingen] = useState([]);
|
const [leerlingen, setLeerlingen] = useState([]);
|
||||||
|
|
@ -1799,7 +1838,7 @@ export function RoosterScherm({ mode }) {
|
||||||
// Blok bewerken/kopiëren (alleen in de Roostermaker).
|
// Blok bewerken/kopiëren (alleen in de Roostermaker).
|
||||||
const [bewerkBlok, setBewerkBlok] = useState(null);
|
const [bewerkBlok, setBewerkBlok] = useState(null);
|
||||||
|
|
||||||
const leeg = { tijdslot: "", activiteit: "", locatie: "", doel_type: "groep", doel: "", begeleiders: [], opmerkingen: "" };
|
const leeg = { tijdslot: "", activiteit: "", locatie: "", lokaal: "", doel_type: "groep", doel: "", begeleiders: [], opmerkingen: "" };
|
||||||
const [form, setForm] = useState(leeg);
|
const [form, setForm] = useState(leeg);
|
||||||
const [splitNieuwBlok, setSplitNieuwBlok] = useState(false);
|
const [splitNieuwBlok, setSplitNieuwBlok] = useState(false);
|
||||||
const [splitNieuwDoelen, setSplitNieuwDoelen] = useState([]);
|
const [splitNieuwDoelen, setSplitNieuwDoelen] = useState([]);
|
||||||
|
|
@ -1815,6 +1854,7 @@ export function RoosterScherm({ mode }) {
|
||||||
listResource("tijdsloten").then(setTijdsloten).catch(() => {});
|
listResource("tijdsloten").then(setTijdsloten).catch(() => {});
|
||||||
listResource("activiteiten").then(setActiviteiten).catch(() => {});
|
listResource("activiteiten").then(setActiviteiten).catch(() => {});
|
||||||
listResource("locaties").then(setLocaties).catch(() => {});
|
listResource("locaties").then(setLocaties).catch(() => {});
|
||||||
|
listResource("lokalen").then(setLokalen).catch(() => {});
|
||||||
listResource("groepen").then(setGroepen).catch(() => {});
|
listResource("groepen").then(setGroepen).catch(() => {});
|
||||||
listResource("subgroepen").then(setSubgroepen).catch(() => {});
|
listResource("subgroepen").then(setSubgroepen).catch(() => {});
|
||||||
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
|
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
|
||||||
|
|
@ -1940,7 +1980,7 @@ export function RoosterScherm({ mode }) {
|
||||||
}
|
}
|
||||||
async function opslaan(e) {
|
async function opslaan(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const basis = { schooljaar, tijdslot: form.tijdslot, activiteit: form.activiteit, locatie: form.locatie || null, begeleiders: form.begeleiders, opmerkingen: form.opmerkingen };
|
const basis = { schooljaar, tijdslot: form.tijdslot, activiteit: form.activiteit, locatie: form.locatie || null, lokaal: form.lokaal || null, begeleiders: form.begeleiders, opmerkingen: form.opmerkingen };
|
||||||
try {
|
try {
|
||||||
if (form.doel_type === "groep" && splitNieuwBlok && splitNieuwDoelen.length > 0) {
|
if (form.doel_type === "groep" && splitNieuwBlok && splitNieuwDoelen.length > 0) {
|
||||||
for (const subgroep of splitNieuwDoelen) {
|
for (const subgroep of splitNieuwDoelen) {
|
||||||
|
|
@ -2012,7 +2052,7 @@ export function RoosterScherm({ mode }) {
|
||||||
if (namen.length === 0) return null;
|
if (namen.length === 0) return null;
|
||||||
return <div className="small rb-stage">📍 Op stage: {namen.join(", ")}</div>;
|
return <div className="small rb-stage">📍 Op stage: {namen.join(", ")}</div>;
|
||||||
}
|
}
|
||||||
const editorCtx = { activiteiten, locaties, groepen, subgroepen, leerlingen, medewerkers, tijdsloten };
|
const editorCtx = { activiteiten, locaties, lokalen, groepen, subgroepen, leerlingen, medewerkers, tijdsloten };
|
||||||
const isOpenBlok = (b) => edit && bewerkBlok && bewerkBlok.id === b.id;
|
const isOpenBlok = (b) => edit && bewerkBlok && bewerkBlok.id === b.id;
|
||||||
const toggleOpenBlok = (b) => setBewerkBlok((huidig) => (huidig && huidig.id === b.id ? null : b));
|
const toggleOpenBlok = (b) => setBewerkBlok((huidig) => (huidig && huidig.id === b.id ? null : b));
|
||||||
function renderRoosterBlok(b, iso, kal) {
|
function renderRoosterBlok(b, iso, kal) {
|
||||||
|
|
@ -2034,6 +2074,7 @@ export function RoosterScherm({ mode }) {
|
||||||
</div>
|
</div>
|
||||||
<div className="small">Doel: {b.doel_naam} <span className="muted">({b.doel_type})</span></div>
|
<div className="small">Doel: {b.doel_naam} <span className="muted">({b.doel_type})</span></div>
|
||||||
{b.locatie_naam && <div className="small">Locatie: {b.locatie_naam}</div>}
|
{b.locatie_naam && <div className="small">Locatie: {b.locatie_naam}</div>}
|
||||||
|
{b.lokaal_naam && <div className="small">Lokaal: {b.lokaal_naam}</div>}
|
||||||
{b.begeleiders_namen.length > 0 && (
|
{b.begeleiders_namen.length > 0 && (
|
||||||
<div className="small muted">Begeleiding: {b.begeleiders_namen.map((x, idx) => (
|
<div className="small muted">Begeleiding: {b.begeleiders_namen.map((x, idx) => (
|
||||||
<span key={x.id} className={persoonAfwezig(x.id, iso) ? "afwezig" : ""}>{idx > 0 ? ", " : ""}{x.naam}{rolAfk(x.id) ? ` (${rolAfk(x.id)})` : ""}</span>
|
<span key={x.id} className={persoonAfwezig(x.id, iso) ? "afwezig" : ""}>{idx > 0 ? ", " : ""}{x.naam}{rolAfk(x.id) ? ` (${rolAfk(x.id)})` : ""}</span>
|
||||||
|
|
@ -2068,6 +2109,7 @@ export function RoosterScherm({ mode }) {
|
||||||
</div>
|
</div>
|
||||||
<div className="split-vak-meta">
|
<div className="split-vak-meta">
|
||||||
{b.locatie_naam && <span>{b.locatie_naam}</span>}
|
{b.locatie_naam && <span>{b.locatie_naam}</span>}
|
||||||
|
{b.lokaal_naam && <span>{b.lokaal_naam}</span>}
|
||||||
{b.begeleiders_namen.length > 0 && <span>{b.begeleiders_namen.map((x) => x.naam).join(", ")}</span>}
|
{b.begeleiders_namen.length > 0 && <span>{b.begeleiders_namen.map((x) => x.naam).join(", ")}</span>}
|
||||||
{vervallen && <span className="rb-status">Vervalt</span>}
|
{vervallen && <span className="rb-status">Vervalt</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2264,6 +2306,10 @@ export function RoosterScherm({ mode }) {
|
||||||
<option value="">— locatie (optioneel) —</option>
|
<option value="">— locatie (optioneel) —</option>
|
||||||
{locaties.map((l) => <option key={l.id} value={l.id}>{l.naam}</option>)}
|
{locaties.map((l) => <option key={l.id} value={l.id}>{l.naam}</option>)}
|
||||||
</select>
|
</select>
|
||||||
|
<select value={form.lokaal} onChange={(e) => setForm({ ...form, lokaal: e.target.value })}>
|
||||||
|
<option value="">— lokaal (optioneel) —</option>
|
||||||
|
{lokalen.map((l) => <option key={l.id} value={l.id}>{l.naam}</option>)}
|
||||||
|
</select>
|
||||||
<input placeholder="Opmerking (optioneel)" value={form.opmerkingen} onChange={(e) => setForm({ ...form, opmerkingen: e.target.value })} />
|
<input placeholder="Opmerking (optioneel)" value={form.opmerkingen} onChange={(e) => setForm({ ...form, opmerkingen: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -2371,11 +2417,12 @@ export function RoosterScherm({ mode }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{schooljaar && tab === "instellingen" && (
|
{tab === "instellingen" && (
|
||||||
<div className="instellingen">
|
<div className="instellingen">
|
||||||
<p className="muted">Bouwstenen van het rooster — beheer ze hier zonder het Rooster te verlaten.</p>
|
<p className="muted">Bouwstenen van het rooster — beheer ze hier zonder het Rooster te verlaten.</p>
|
||||||
<TijdslotenPage />
|
<TijdslotenPage />
|
||||||
<ActiviteitenPage />
|
<ActiviteitenPage />
|
||||||
|
<LokalenPage />
|
||||||
<LocatiesPage />
|
<LocatiesPage />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -355,3 +355,5 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
|
||||||
.split-vak.vervallen .rb-stage { opacity: .7; }
|
.split-vak.vervallen .rb-stage { opacity: .7; }
|
||||||
.subsectie { margin-top: 28px; padding-top: 18px; border-top: 1px solid var(--line); }
|
.subsectie { margin-top: 28px; padding-top: 18px; border-top: 1px solid var(--line); }
|
||||||
.subsectie h2 { margin: 0 0 4px; font-size: 18px; }
|
.subsectie h2 { margin: 0 0 4px; font-size: 18px; }
|
||||||
|
.veld-kol { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 120px; }
|
||||||
|
.veld-kop { font-size: 11px; font-weight: 700; color: var(--muted); }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue