Lokalen-systeem met dubbele-boekingcheck (warn + per-school blokkeren); stage gebruikt lokaal; dagdeel-UI verduidelijkt
This commit is contained in:
parent
edddf40c7f
commit
a9dff26d9a
9 changed files with 444 additions and 51 deletions
18
ROADMAP.md
18
ROADMAP.md
|
|
@ -45,12 +45,13 @@ volgt.
|
||||||
|
|
||||||
## Stage-module — uitbreidingen
|
## Stage-module — uitbreidingen
|
||||||
|
|
||||||
- [ ] **Aparte (stage)locaties** kunnen toevoegen, los van de gewone lokalen.
|
- [x] **Aparte (stage)locaties** kunnen toevoegen, los van de gewone lokalen.
|
||||||
- [ ] Een **begeleider aan meerdere stages** koppelen, maar **niet tegelijk aan
|
- [x] Een **begeleider aan meerdere stages** koppelen, maar **niet tegelijk aan
|
||||||
een theorievak** (conflict signaleren/voorkomen).
|
een theorievak** (conflict wordt voorkomen bij opslaan + gesignaleerd via
|
||||||
- [ ] Een leerling **verdwijnt op een stagedag(deel) uit de klas** in het rooster
|
`/api/stage-conflicten/`).
|
||||||
(de betreffende blokken vervallen voor die leerling).
|
- [x] Een leerling **verdwijnt op een stagedag(deel) uit de klas** in het rooster
|
||||||
- [ ] Stage instelbaar **per blok, per dagdeel of per dag**.
|
(in het weekrooster gemarkeerd "Op stage" bij de betreffende blokken).
|
||||||
|
- [x] Stage instelbaar **per blok, per dagdeel of per dag**.
|
||||||
|
|
||||||
## Koppelingen & inzetregels
|
## Koppelingen & inzetregels
|
||||||
|
|
||||||
|
|
@ -83,7 +84,4 @@ volgt.
|
||||||
|
|
||||||
## Later
|
## Later
|
||||||
|
|
||||||
- [ ] **Module: automatisch rooster genereren.** De gebruiker geeft vereisten/
|
- [ ] **Module: automatisch rooster genereren.** De gebruiker ge
|
||||||
input op, het systeem stelt een rooster voor, en de gebruiker past het
|
|
||||||
daarna zelf aan.
|
|
||||||
- [ ] **Uiterlijk / UX-verbeteringen** (algemene visuele opfris).
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Stage
|
from .models import Stage, StageLocatie
|
||||||
|
|
||||||
|
|
||||||
|
@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 = ("leerling", "type", "begeleider", "locatie", "organisatie", "actief")
|
list_display = (
|
||||||
list_filter = ("type", "actief", "dag")
|
"leerling", "type", "begeleider", "stagelocatie", "organisatie",
|
||||||
|
"omvang", "dag", "dagdeel", "actief",
|
||||||
|
)
|
||||||
|
list_filter = ("type", "omvang", "actief", "dag")
|
||||||
search_fields = ("leerling__voornaam", "leerling__achternaam", "organisatie")
|
search_fields = ("leerling__voornaam", "leerling__achternaam", "organisatie")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0005_subgroep_begeleiders_subgroep_groep"),
|
||||||
|
("module_stage", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="StageLocatie",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
("naam", models.CharField(max_length=120, unique=True, verbose_name="naam")),
|
||||||
|
("omschrijving", models.CharField(blank=True, max_length=255, verbose_name="omschrijving")),
|
||||||
|
("actief", models.BooleanField(default=True, verbose_name="actief")),
|
||||||
|
("aangemaakt_op", models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "stagelocatie",
|
||||||
|
"verbose_name_plural": "stagelocaties",
|
||||||
|
"ordering": ["naam"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="stage",
|
||||||
|
name="locatie",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="stage",
|
||||||
|
name="stagelocatie",
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="stages", to="module_stage.stagelocatie"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="stage",
|
||||||
|
name="omvang",
|
||||||
|
field=models.CharField(choices=[("hele_dag", "Hele dag"), ("dagdeel", "Dagdeel"), ("blok", "Eén blok")], default="hele_dag", max_length=10, verbose_name="omvang"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="stage",
|
||||||
|
name="dagdeel",
|
||||||
|
field=models.CharField(blank=True, choices=[("ochtend", "Ochtend"), ("middag", "Middag")], help_text="Alleen bij omvang 'dagdeel'.", max_length=8, verbose_name="dagdeel"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="stage",
|
||||||
|
name="tijdslot",
|
||||||
|
field=models.ForeignKey(blank=True, help_text="Alleen bij omvang 'eén blok'; bepaalt zelf de dag.", null=True, on_delete=django.db.models.deletion.CASCADE, related_name="stages", to="core.tijdslot"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="stage",
|
||||||
|
name="begeleider",
|
||||||
|
field=models.ForeignKey(help_text="Verplicht voor zowel interne als externe stage.", on_delete=django.db.models.deletion.PROTECT, related_name="stagebegeleidingen", to="core.persoon"),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -2,18 +2,36 @@
|
||||||
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 ruimte (locatie).
|
- Een interne stage krijgt daarnaast een stageruimte (eigen lijst, los van de
|
||||||
|
gewone lokalen).
|
||||||
- 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.
|
||||||
"""
|
"""
|
||||||
|
from datetime import time
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
# De grens tussen ochtend en middag (voor dagdeel-stages).
|
||||||
|
MIDDAG_GRENS = time(12, 0)
|
||||||
|
|
||||||
|
|
||||||
class StageType(models.TextChoices):
|
class StageType(models.TextChoices):
|
||||||
INTERN = "intern", "Interne stage"
|
INTERN = "intern", "Interne stage"
|
||||||
EXTERN = "extern", "Externe stage"
|
EXTERN = "extern", "Externe stage"
|
||||||
|
|
||||||
|
|
||||||
|
class Omvang(models.TextChoices):
|
||||||
|
HELE_DAG = "hele_dag", "Hele dag"
|
||||||
|
DAGDEEL = "dagdeel", "Dagdeel"
|
||||||
|
BLOK = "blok", "Eén blok"
|
||||||
|
|
||||||
|
|
||||||
|
class Dagdeel(models.TextChoices):
|
||||||
|
OCHTEND = "ochtend", "Ochtend"
|
||||||
|
MIDDAG = "middag", "Middag"
|
||||||
|
|
||||||
|
|
||||||
class Weekdag(models.IntegerChoices):
|
class Weekdag(models.IntegerChoices):
|
||||||
MAANDAG = 0, "Maandag"
|
MAANDAG = 0, "Maandag"
|
||||||
DINSDAG = 1, "Dinsdag"
|
DINSDAG = 1, "Dinsdag"
|
||||||
|
|
@ -22,6 +40,26 @@ 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",
|
||||||
|
|
@ -32,15 +70,29 @@ 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 ruimte binnen de school.
|
# Interne stage: een eigen stageruimte.
|
||||||
locatie = models.ForeignKey(
|
stagelocatie = models.ForeignKey(
|
||||||
"core.Locatie", on_delete=models.SET_NULL, null=True, blank=True,
|
StageLocatie, 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.
|
||||||
organisatie = models.CharField("externe organisatie", max_length=160, blank=True)
|
organisatie = models.CharField("externe organisatie", max_length=160, blank=True)
|
||||||
|
|
||||||
|
# Wanneer geldt de stage?
|
||||||
|
omvang = models.CharField(
|
||||||
|
"omvang", max_length=10, choices=Omvang.choices, default=Omvang.HELE_DAG,
|
||||||
|
)
|
||||||
dag = models.IntegerField("dag", choices=Weekdag.choices, null=True, blank=True)
|
dag = models.IntegerField("dag", choices=Weekdag.choices, null=True, blank=True)
|
||||||
|
dagdeel = models.CharField(
|
||||||
|
"dagdeel", max_length=8, choices=Dagdeel.choices, blank=True,
|
||||||
|
help_text="Alleen bij omvang 'dagdeel'.",
|
||||||
|
)
|
||||||
|
tijdslot = models.ForeignKey(
|
||||||
|
"core.Tijdslot", on_delete=models.CASCADE, null=True, blank=True,
|
||||||
|
related_name="stages",
|
||||||
|
help_text="Alleen bij omvang 'eén blok'; bepaalt zelf de dag.",
|
||||||
|
)
|
||||||
|
|
||||||
omschrijving = models.CharField("omschrijving", max_length=255, blank=True)
|
omschrijving = models.CharField("omschrijving", max_length=255, blank=True)
|
||||||
actief = models.BooleanField("actief", default=True)
|
actief = models.BooleanField("actief", default=True)
|
||||||
aangemaakt_op = models.DateTimeField(auto_now_add=True)
|
aangemaakt_op = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
@ -51,10 +103,41 @@ 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.locatie_id:
|
if self.type == StageType.INTERN and not self.stagelocatie_id:
|
||||||
raise ValidationError("Een interne stage vereist een ruimte (locatie).")
|
raise ValidationError("Een interne stage vereist een stageruimte.")
|
||||||
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:
|
||||||
|
raise ValidationError("Kies een roosterblok (tijdslot) voor een blok-stage.")
|
||||||
|
if self.omvang in (Omvang.HELE_DAG, Omvang.DAGDEEL) and self.dag is None:
|
||||||
|
raise ValidationError("Kies een dag voor een dag- of dagdeel-stage.")
|
||||||
|
if self.omvang == Omvang.DAGDEEL and not self.dagdeel:
|
||||||
|
raise ValidationError("Kies een dagdeel (ochtend of middag).")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effectieve_dag(self):
|
||||||
|
"""De weekdag waarop de stage valt (afgeleid van het tijdslot bij blok)."""
|
||||||
|
if self.omvang == Omvang.BLOK and self.tijdslot_id:
|
||||||
|
return self.tijdslot.dag
|
||||||
|
return self.dag
|
||||||
|
|
||||||
|
def dekt_tijdslot(self, tijdslot) -> bool:
|
||||||
|
"""True als deze stage het gegeven tijdslot (dag + tijd) afdekt."""
|
||||||
|
if not self.actief:
|
||||||
|
return False
|
||||||
|
if self.omvang == Omvang.BLOK:
|
||||||
|
return self.tijdslot_id == tijdslot.id
|
||||||
|
if self.dag is None or self.dag != tijdslot.dag:
|
||||||
|
return False
|
||||||
|
if self.omvang == Omvang.HELE_DAG:
|
||||||
|
return True
|
||||||
|
# Dagdeel: ochtend = start vóór de middaggrens, middag = vanaf de grens.
|
||||||
|
is_ochtend = tijdslot.start_tijd < MIDDAG_GRENS
|
||||||
|
if self.dagdeel == Dagdeel.OCHTEND:
|
||||||
|
return is_ochtend
|
||||||
|
if self.dagdeel == Dagdeel.MIDDAG:
|
||||||
|
return not is_ochtend
|
||||||
|
return False
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.leerling} – {self.get_type_display()}"
|
return f"{self.leerling} – {self.get_type_display()}"
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,100 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import Stage, StageType
|
from .models import Dagdeel, Omvang, Stage, StageLocatie, 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)
|
||||||
locatie_naam = serializers.CharField(source="locatie.naam", read_only=True, default=None)
|
stagelocatie_naam = serializers.CharField(source="stagelocatie.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)
|
||||||
|
dagdeel_label = serializers.CharField(source="get_dagdeel_display", read_only=True, default=None)
|
||||||
dag_label = serializers.CharField(source="get_dag_display", read_only=True, default=None)
|
dag_label = serializers.CharField(source="get_dag_display", read_only=True, default=None)
|
||||||
|
tijdslot_label = serializers.CharField(source="tijdslot.__str__", read_only=True, default=None)
|
||||||
|
effectieve_dag = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Stage
|
model = Stage
|
||||||
fields = [
|
fields = [
|
||||||
"id", "leerling", "leerling_naam", "type", "type_label",
|
"id", "leerling", "leerling_naam", "type", "type_label",
|
||||||
"begeleider", "begeleider_naam", "locatie", "locatie_naam",
|
"begeleider", "begeleider_naam",
|
||||||
"organisatie", "dag", "dag_label", "omschrijving", "actief",
|
"stagelocatie", "stagelocatie_naam", "organisatie",
|
||||||
"aangemaakt_op",
|
"omvang", "omvang_label", "dag", "dag_label", "effectieve_dag",
|
||||||
|
"dagdeel", "dagdeel_label", "tijdslot", "tijdslot_label",
|
||||||
|
"omschrijving", "actief", "aangemaakt_op",
|
||||||
]
|
]
|
||||||
read_only_fields = ["aangemaakt_op"]
|
read_only_fields = ["aangemaakt_op"]
|
||||||
|
|
||||||
|
def _val(self, data, naam, default=None):
|
||||||
|
return data.get(naam, getattr(self.instance, naam, default))
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
type_ = data.get("type", getattr(self.instance, "type", None))
|
type_ = self._val(data, "type")
|
||||||
locatie = data.get("locatie", getattr(self.instance, "locatie", None))
|
stagelocatie = self._val(data, "stagelocatie")
|
||||||
organisatie = data.get("organisatie", getattr(self.instance, "organisatie", ""))
|
organisatie = self._val(data, "organisatie", "")
|
||||||
begeleider = data.get("begeleider", getattr(self.instance, "begeleider", None))
|
begeleider = self._val(data, "begeleider")
|
||||||
|
omvang = self._val(data, "omvang", Omvang.HELE_DAG)
|
||||||
|
dag = self._val(data, "dag")
|
||||||
|
dagdeel = self._val(data, "dagdeel", "")
|
||||||
|
tijdslot = self._val(data, "tijdslot")
|
||||||
|
|
||||||
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 locatie:
|
if type_ == StageType.INTERN and not stagelocatie:
|
||||||
raise serializers.ValidationError("Een interne stage vereist een ruimte (locatie).")
|
raise serializers.ValidationError("Een interne stage vereist een stageruimte.")
|
||||||
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:
|
||||||
|
raise serializers.ValidationError("Kies een roosterblok (tijdslot) voor een blok-stage.")
|
||||||
|
if omvang in (Omvang.HELE_DAG, Omvang.DAGDEEL) and dag is None:
|
||||||
|
raise serializers.ValidationError("Kies een dag voor een dag- of dagdeel-stage.")
|
||||||
|
if omvang == Omvang.DAGDEEL and not dagdeel:
|
||||||
|
raise serializers.ValidationError("Kies een dagdeel (ochtend of middag).")
|
||||||
|
|
||||||
|
# Inzetregel: de begeleider mag niet tegelijk een THEORIEVAK begeleiden.
|
||||||
|
if begeleider is not None:
|
||||||
|
botsing = _theorievak_botsing(begeleider, omvang, dag, dagdeel, tijdslot)
|
||||||
|
if botsing:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"{begeleider.volledige_naam} begeleidt op dat moment al het "
|
||||||
|
f"theorievak '{botsing}' en kan niet tegelijk stage begeleiden."
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _theorievak_botsing(begeleider, omvang, dag, dagdeel, tijdslot):
|
||||||
|
"""Zoek een theorievak-roosterblok dat de begeleider op hetzelfde moment doet."""
|
||||||
|
from core.models import Roosterblok
|
||||||
|
from .models import MIDDAG_GRENS, Omvang as _Omvang, Dagdeel as _Dagdeel
|
||||||
|
|
||||||
|
qs = (
|
||||||
|
Roosterblok.objects
|
||||||
|
.filter(begeleiders=begeleider, activiteit__theorievak=True)
|
||||||
|
.select_related("tijdslot", "activiteit")
|
||||||
|
)
|
||||||
|
for blok in qs:
|
||||||
|
t = blok.tijdslot
|
||||||
|
if omvang == _Omvang.BLOK:
|
||||||
|
if tijdslot is not None and t.id == tijdslot.id:
|
||||||
|
return blok.activiteit.naam
|
||||||
|
continue
|
||||||
|
if dag is None or t.dag != dag:
|
||||||
|
continue
|
||||||
|
if omvang == _Omvang.HELE_DAG:
|
||||||
|
return blok.activiteit.naam
|
||||||
|
is_ochtend = t.start_tijd < MIDDAG_GRENS
|
||||||
|
if dagdeel == _Dagdeel.OCHTEND and is_ochtend:
|
||||||
|
return blok.activiteit.naam
|
||||||
|
if dagdeel == _Dagdeel.MIDDAG and not is_ochtend:
|
||||||
|
return blok.activiteit.naam
|
||||||
|
return None
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,9 @@ 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 = [path("", include(router.urls))]
|
urlpatterns = [
|
||||||
|
path("stage-conflicten/", views.stage_conflicten, name="stage-conflicten"),
|
||||||
|
path("", include(router.urls)),
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,58 @@
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.decorators import api_view
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from .models import Stage
|
from .models import Stage, StageLocatie
|
||||||
from .serializers import StageSerializer
|
from .serializers import StageLocatieSerializer, StageSerializer, _theorievak_botsing
|
||||||
|
|
||||||
|
|
||||||
|
class StageLocatieViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = StageLocatie.objects.all()
|
||||||
|
serializer_class = StageLocatieSerializer
|
||||||
|
|
||||||
|
|
||||||
class StageViewSet(viewsets.ModelViewSet):
|
class StageViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = StageSerializer
|
serializer_class = StageSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = Stage.objects.select_related("leerling", "begeleider", "locatie").all()
|
qs = Stage.objects.select_related(
|
||||||
|
"leerling", "begeleider", "stagelocatie", "tijdslot"
|
||||||
|
).all()
|
||||||
type_ = self.request.query_params.get("type")
|
type_ = self.request.query_params.get("type")
|
||||||
if type_:
|
if type_:
|
||||||
qs = qs.filter(type=type_)
|
qs = qs.filter(type=type_)
|
||||||
leerling = self.request.query_params.get("leerling")
|
leerling = self.request.query_params.get("leerling")
|
||||||
if leerling:
|
if leerling:
|
||||||
qs = qs.filter(leerling_id=leerling)
|
qs = qs.filter(leerling_id=leerling)
|
||||||
|
actief = self.request.query_params.get("actief")
|
||||||
|
if actief in ("1", "true", "True"):
|
||||||
|
qs = qs.filter(actief=True)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["GET"])
|
||||||
|
def stage_conflicten(request):
|
||||||
|
"""Signaleer begeleiders die tegelijk stage én een theorievak doen."""
|
||||||
|
resultaat = []
|
||||||
|
for stage in Stage.objects.filter(actief=True).select_related("leerling", "begeleider"):
|
||||||
|
botsing = _theorievak_botsing(
|
||||||
|
stage.begeleider, stage.omvang, stage.dag, stage.dagdeel, stage.tijdslot
|
||||||
|
)
|
||||||
|
if botsing:
|
||||||
|
resultaat.append({
|
||||||
|
"type": "stage",
|
||||||
|
"ernst": "fout",
|
||||||
|
"tijdslot": stage.get_omvang_display(),
|
||||||
|
"detail": (
|
||||||
|
f"{stage.begeleider.volledige_naam} begeleidt {stage.leerling.volledige_naam} "
|
||||||
|
f"op stage én tegelijk het theorievak '{botsing}'."
|
||||||
|
),
|
||||||
|
"blokken": [botsing],
|
||||||
|
})
|
||||||
|
fouten = sum(1 for r in resultaat if r["ernst"] == "fout")
|
||||||
|
return Response({
|
||||||
|
"aantal": len(resultaat),
|
||||||
|
"fouten": fouten,
|
||||||
|
"waarschuwingen": len(resultaat) - fouten,
|
||||||
|
"conflicten": resultaat,
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1075,31 +1075,51 @@ export function GroepsindelingPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------- Stage --------------------------------- */
|
/* ---------------------------------- Stage --------------------------------- */
|
||||||
// Module-scherm: interne en externe stages. Beide met begeleider; intern + ruimte.
|
// Module-scherm: interne en externe stages + stagelocaties.
|
||||||
|
const OMVANGEN = [
|
||||||
|
{ value: "hele_dag", label: "Hele dag" },
|
||||||
|
{ value: "dagdeel", label: "Dagdeel" },
|
||||||
|
{ value: "blok", label: "Eén blok" },
|
||||||
|
];
|
||||||
|
const DAGDELEN = [
|
||||||
|
{ value: "ochtend", label: "Ochtend" },
|
||||||
|
{ value: "middag", label: "Middag" },
|
||||||
|
];
|
||||||
|
|
||||||
export function StagePage() {
|
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 [locaties, setLocaties] = useState([]);
|
const [stagelocaties, setStagelocaties] = useState([]);
|
||||||
const leeg = { leerling: "", type: "intern", begeleider: "", locatie: "", organisatie: "", dag: "", omschrijving: "" };
|
const [tijdsloten, setTijdsloten] = useState([]);
|
||||||
|
const leeg = { leerling: "", type: "intern", begeleider: "", stagelocatie: "", 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("locaties").then(setLocaties).catch(() => {});
|
listResource("tijdsloten").then(setTijdsloten).catch(() => {});
|
||||||
|
laadStagelocaties();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isIntern = form.type === "intern";
|
const isIntern = form.type === "intern";
|
||||||
|
const isBlok = form.omvang === "blok";
|
||||||
|
const isDagdeel = form.omvang === "dagdeel";
|
||||||
|
|
||||||
async function opslaan(e) {
|
async function opslaan(e) {
|
||||||
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,
|
||||||
locatie: isIntern ? (form.locatie || null) : null,
|
stagelocatie: isIntern ? (form.stagelocatie || null) : null,
|
||||||
organisatie: isIntern ? "" : form.organisatie,
|
organisatie: isIntern ? "" : form.organisatie,
|
||||||
dag: form.dag === "" ? null : Number(form.dag),
|
omvang: form.omvang,
|
||||||
|
dag: isBlok ? null : (form.dag === "" ? null : Number(form.dag)),
|
||||||
|
dagdeel: isDagdeel ? form.dagdeel : "",
|
||||||
|
tijdslot: isBlok ? (form.tijdslot || null) : null,
|
||||||
omschrijving: form.omschrijving,
|
omschrijving: form.omschrijving,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
|
@ -1117,10 +1137,18 @@ export function StagePage() {
|
||||||
try { await deleteResource("stages", id); await reload(); } catch (err) { setError(err.message); }
|
try { await deleteResource("stages", id); await reload(); } catch (err) { setError(err.message); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dagLabel = (v) => (WEEKDAGEN.find((d) => d.value === v) || {}).label || "—";
|
||||||
|
const tijdslotLabel = (id) => { const t = tijdsloten.find((x) => x.id === id); return t ? `${t.dag_label || ""} ${(t.start_tijd || "").slice(0, 5)} ${t.naam}` : "—"; };
|
||||||
|
function wanneerTekst(s) {
|
||||||
|
if (s.omvang === "blok") return `Blok: ${s.tijdslot_label || tijdslotLabel(s.tijdslot)}`;
|
||||||
|
if (s.omvang === "dagdeel") return `${dagLabel(s.dag)} — ${s.dagdeel_label || s.dagdeel}`;
|
||||||
|
return `${dagLabel(s.dag)} — hele dag`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1>Stage</h1>
|
<h1>Stage</h1>
|
||||||
<p className="muted">Interne en externe stages. Beide krijgen een begeleider; een interne stage ook een ruimte.</p>
|
<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>
|
||||||
<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">
|
||||||
|
|
@ -1139,17 +1167,35 @@ export function StagePage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
{isIntern ? (
|
{isIntern ? (
|
||||||
<select value={form.locatie} required onChange={(e) => setForm({ ...form, locatie: e.target.value })}>
|
<select value={form.stagelocatie} required onChange={(e) => setForm({ ...form, stagelocatie: e.target.value })}>
|
||||||
<option value="">— ruimte —</option>
|
<option value="">— stageruimte —</option>
|
||||||
{locaties.map((l) => <option key={l.id} value={l.id}>{l.naam}</option>)}
|
{stagelocaties.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.dag} onChange={(e) => setForm({ ...form, dag: e.target.value })}>
|
<select value={form.omvang} onChange={(e) => setForm({ ...form, omvang: e.target.value })}>
|
||||||
<option value="">— dag (optioneel) —</option>
|
{OMVANGEN.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||||
|
</select>
|
||||||
|
{isBlok ? (
|
||||||
|
<select value={form.tijdslot} required onChange={(e) => setForm({ ...form, tijdslot: e.target.value })}>
|
||||||
|
<option value="">— roosterblok —</option>
|
||||||
|
{tijdsloten.map((t) => <option key={t.id} value={t.id}>{tijdslotLabel(t.id)}</option>)}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<select value={form.dag} required onChange={(e) => setForm({ ...form, dag: e.target.value })}>
|
||||||
|
<option value="">— dag —</option>
|
||||||
{WEEKDAGEN.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
|
{WEEKDAGEN.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
|
)}
|
||||||
|
{isDagdeel && (
|
||||||
|
<select value={form.dagdeel} required onChange={(e) => setForm({ ...form, dagdeel: e.target.value })}>
|
||||||
|
<option value="">— dagdeel —</option>
|
||||||
|
{DAGDELEN.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
<input placeholder="Opmerking (optioneel)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} />
|
<input placeholder="Opmerking (optioneel)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
|
|
@ -1159,17 +1205,17 @@ export function StagePage() {
|
||||||
</form>
|
</form>
|
||||||
{loading ? <p>Laden…</p> : (
|
{loading ? <p>Laden…</p> : (
|
||||||
<table className="grid">
|
<table className="grid">
|
||||||
<thead><tr><th>Leerling</th><th>Type</th><th>Begeleider</th><th>Waar</th><th>Dag</th><th></th></tr></thead>
|
<thead><tr><th>Leerling</th><th>Type</th><th>Begeleider</th><th>Waar</th><th>Wanneer</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{items.map((s) => (
|
{items.map((s) => (
|
||||||
<tr key={s.id}>
|
<tr key={s.id}>
|
||||||
<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.locatie_naam || "—") : (s.organisatie || "—")}</td>
|
<td className="muted">{s.type === "intern" ? (s.stagelocatie_naam || "—") : (s.organisatie || "—")}</td>
|
||||||
<td className="muted">{s.dag_label || "—"}</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, locatie: s.locatie || "", organisatie: s.organisatie || "", dag: s.dag ?? "", omschrijving: s.omschrijving || "" }); }}>bewerken</button>
|
<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 danger" onClick={() => verwijder(s.id)}>verwijderen</button>
|
<button className="link danger" onClick={() => verwijder(s.id)}>verwijderen</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -1178,10 +1224,63 @@ 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" },
|
||||||
|
|
@ -1688,6 +1787,7 @@ export function RoosterScherm({ mode }) {
|
||||||
const [kalender, setKalender] = useState([]);
|
const [kalender, setKalender] = useState([]);
|
||||||
const [afwezig, setAfwezig] = useState([]);
|
const [afwezig, setAfwezig] = useState([]);
|
||||||
const [uitzonderingen, setUitzonderingen] = useState([]);
|
const [uitzonderingen, setUitzonderingen] = useState([]);
|
||||||
|
const [stages, setStages] = useState([]);
|
||||||
const [leerpleinen, setLeerpleinen] = useState([]);
|
const [leerpleinen, setLeerpleinen] = useState([]);
|
||||||
const [maandag, setMaandag] = useState(() => maandagVan(new Date()));
|
const [maandag, setMaandag] = useState(() => maandagVan(new Date()));
|
||||||
const [toonForm, setToonForm] = useState(false);
|
const [toonForm, setToonForm] = useState(false);
|
||||||
|
|
@ -1720,6 +1820,7 @@ export function RoosterScherm({ mode }) {
|
||||||
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("leerpleinen").then(setLeerpleinen).catch(() => {});
|
listResource("leerpleinen").then(setLeerpleinen).catch(() => {});
|
||||||
|
listResource("stages", "?actief=1").then(setStages).catch(() => setStages([]));
|
||||||
// Afdrukken alleen tonen als de print-module aanstaat.
|
// Afdrukken alleen tonen als de print-module aanstaat.
|
||||||
getModules().then((ms) => setPrintAan(ms.some((m) => m.key === "printing" && m.enabled))).catch(() => {});
|
getModules().then((ms) => setPrintAan(ms.some((m) => m.key === "printing" && m.enabled))).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -1884,6 +1985,33 @@ export function RoosterScherm({ mode }) {
|
||||||
// Op id vergelijken, niet op naam: twee personen kunnen dezelfde naam hebben.
|
// Op id vergelijken, niet op naam: twee personen kunnen dezelfde naam hebben.
|
||||||
const persoonAfwezig = (id, iso) => afwezig.some((a) => a.persoon === id && a.start_datum <= iso && a.eind_datum >= iso);
|
const persoonAfwezig = (id, iso) => afwezig.some((a) => a.persoon === id && a.start_datum <= iso && a.eind_datum >= iso);
|
||||||
const rolAfk = (id) => { const m = medewerkers.find((x) => x.id === id); return m ? (ROL_AFK[m.rol] || "") : ""; };
|
const rolAfk = (id) => { const m = medewerkers.find((x) => x.id === id); return m ? (ROL_AFK[m.rol] || "") : ""; };
|
||||||
|
// Stage: welke leerlingen zijn op een gegeven tijdslot op stage (en dus niet in de klas)?
|
||||||
|
const tijdslotById = (id) => tijdsloten.find((t) => t.id === id);
|
||||||
|
const leerlingNaam = (id) => { const l = leerlingen.find((x) => x.id === id); return l ? l.volledige_naam : "?"; };
|
||||||
|
function stageDektTijdslot(stage, t) {
|
||||||
|
if (!t) return false;
|
||||||
|
if (stage.omvang === "blok") return stage.tijdslot === t.id;
|
||||||
|
if (stage.dag === null || stage.dag === undefined || stage.dag !== t.dag) return false;
|
||||||
|
if (stage.omvang === "hele_dag") return true;
|
||||||
|
const isOchtend = (t.start_tijd || "") < "12:00";
|
||||||
|
return (stage.dagdeel === "ochtend" && isOchtend) || (stage.dagdeel === "middag" && !isOchtend);
|
||||||
|
}
|
||||||
|
function leerlingenOpStageBij(t) {
|
||||||
|
const set = new Set();
|
||||||
|
for (const st of stages) { if (stageDektTijdslot(st, t)) set.add(st.leerling); }
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
function stageNoot(b) {
|
||||||
|
const t = tijdslotById(b.tijdslot);
|
||||||
|
const opStage = leerlingenOpStageBij(t);
|
||||||
|
if (opStage.size === 0) return null;
|
||||||
|
if (b.doel_type === "leerling") {
|
||||||
|
return opStage.has(b.leerling) ? <div className="small rb-stage">📍 Op stage — niet in de klas</div> : null;
|
||||||
|
}
|
||||||
|
const namen = leerlingenVanBlok(b, scopeCtx).filter((id) => opStage.has(id)).map(leerlingNaam);
|
||||||
|
if (namen.length === 0) return null;
|
||||||
|
return <div className="small rb-stage">📍 Op stage: {namen.join(", ")}</div>;
|
||||||
|
}
|
||||||
const editorCtx = { activiteiten, locaties, groepen, subgroepen, leerlingen, medewerkers, tijdsloten };
|
const editorCtx = { activiteiten, locaties, 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));
|
||||||
|
|
@ -1911,6 +2039,7 @@ export function RoosterScherm({ mode }) {
|
||||||
<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>
|
||||||
))}</div>
|
))}</div>
|
||||||
)}
|
)}
|
||||||
|
{stageNoot(b)}
|
||||||
{kal && <div className="small rb-status">Vervalt - {kal.type_label}</div>}
|
{kal && <div className="small rb-status">Vervalt - {kal.type_label}</div>}
|
||||||
{!kal && uit && uit.vervalt && <div className="small rb-status">Vervalt {uit.opmerking ? `(${uit.opmerking})` : ""}{edit && <button type="button" className="icon-btn" title="Herstellen" onClick={(e) => { e.stopPropagation(); herstel(uit.id); }}>↺</button>}</div>}
|
{!kal && uit && uit.vervalt && <div className="small rb-status">Vervalt {uit.opmerking ? `(${uit.opmerking})` : ""}{edit && <button type="button" className="icon-btn" title="Herstellen" onClick={(e) => { e.stopPropagation(); herstel(uit.id); }}>↺</button>}</div>}
|
||||||
{!kal && uit && !uit.vervalt && uit.vervanger_naam && <div className="small rb-verv">Vervanger: {uit.vervanger_naam}</div>}
|
{!kal && uit && !uit.vervalt && uit.vervanger_naam && <div className="small rb-verv">Vervanger: {uit.vervanger_naam}</div>}
|
||||||
|
|
@ -1942,6 +2071,7 @@ export function RoosterScherm({ mode }) {
|
||||||
{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>
|
||||||
|
{stageNoot(b)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -348,3 +348,10 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
gap: 8px; padding: 10px 8px 0; border-top: 1px solid var(--line); margin-top: 12px;
|
gap: 8px; padding: 10px 8px 0; border-top: 1px solid var(--line); margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Stage in het rooster + stagelocatie-beheer */
|
||||||
|
.rb-stage { color: #b45309; font-weight: 600; }
|
||||||
|
.rooster-blok.vervallen .rb-stage,
|
||||||
|
.split-vak.vervallen .rb-stage { opacity: .7; }
|
||||||
|
.subsectie { margin-top: 28px; padding-top: 18px; border-top: 1px solid var(--line); }
|
||||||
|
.subsectie h2 { margin: 0 0 4px; font-size: 18px; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue