Fase 5: per-groep tijden, Stage-module (intern/extern), gesplitste personen-flow met functies/stamgroep/vakken en automatische theorievakken
This commit is contained in:
parent
e8f31f6cca
commit
52feabd744
21 changed files with 646 additions and 151 deletions
60
.gitignore
vendored
60
.gitignore
vendored
|
|
@ -1,20 +1,70 @@
|
||||||
|
# ======================================================================
|
||||||
# Geheimen / omgeving
|
# Geheimen / omgeving
|
||||||
|
# ======================================================================
|
||||||
.env
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# Python
|
# ======================================================================
|
||||||
|
# Python / Django
|
||||||
|
# ======================================================================
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.py[cod]
|
||||||
backend/db.sqlite3
|
*$py.class
|
||||||
backend/staticfiles/
|
*.egg-info/
|
||||||
|
.eggs/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
# Node
|
# Databases (lokaal SQLite)
|
||||||
|
*.sqlite3
|
||||||
|
*.sqlite3-journal
|
||||||
|
backend/db.sqlite3
|
||||||
|
backend/db.sqlite3-journal
|
||||||
|
|
||||||
|
# Door Django verzamelde/geüploade bestanden
|
||||||
|
backend/staticfiles/
|
||||||
|
staticfiles/
|
||||||
|
media/
|
||||||
|
|
||||||
|
# Test- en coverage-artefacten
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# Node / frontend
|
||||||
|
# ======================================================================
|
||||||
|
node_modules/
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
|
frontend/.vite/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# Logs
|
||||||
|
# ======================================================================
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
# Editor / OS
|
# Editor / OS
|
||||||
|
# ======================================================================
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -17,9 +17,18 @@ menu rustiger is). De zijbalk groepeert items onder inklapbare submenu's
|
||||||
(Personen, Groepen).
|
(Personen, Groepen).
|
||||||
|
|
||||||
Optionele module **Leerplein**: koppelt subgroepen aan groepen en bundelt
|
Optionele module **Leerplein**: koppelt subgroepen aan groepen en bundelt
|
||||||
groepen, subgroepen én losse leerlingen onder een overkoepelend leerplein. Aan/uit
|
groepen, subgroepen én losse leerlingen onder een overkoepelend leerplein.
|
||||||
|
Optionele module **Stage**: interne en externe stages van leerlingen — beide met
|
||||||
|
een begeleider, een interne stage ook met een ruimte. Beide modules zijn aan/uit
|
||||||
te zetten via Modulebeheer.
|
te zetten via Modulebeheer.
|
||||||
|
|
||||||
|
**Fase 5 (recent)**: vaste tijden zijn per groep aan te passen (een tijdslot
|
||||||
|
zonder groep = algemene tijd, met groep = afwijkende tijd). Personeel en
|
||||||
|
leerlingen hebben gescheiden invoerschermen. Bij personeel koppel je direct
|
||||||
|
functie(s), stamgroep/plein en de vakken die ze kunnen geven; een leerkracht
|
||||||
|
krijgt de theorievakken automatisch. Bij een leerling leg je groep en
|
||||||
|
geboortedatum vast.
|
||||||
|
|
||||||
## Wat er nu staat
|
## Wat er nu staat
|
||||||
|
|
||||||
- **Backend** (`backend/`) — Django + Django REST Framework.
|
- **Backend** (`backend/`) — Django + Django REST Framework.
|
||||||
|
|
@ -74,6 +83,9 @@ API-eindpunten:
|
||||||
- `GET /api/conflicten/?schooljaar=<id>` — roosterconflicten (dubbele inzet,
|
- `GET /api/conflicten/?schooljaar=<id>` — roosterconflicten (dubbele inzet,
|
||||||
locatiebotsing, onderbezetting).
|
locatiebotsing, onderbezetting).
|
||||||
- Module Leerplein: `/api/leerpleinen/`, `/api/subgroep-groep-koppelingen/`.
|
- Module Leerplein: `/api/leerpleinen/`, `/api/subgroep-groep-koppelingen/`.
|
||||||
|
- Module Stage: `/api/stages/` (filters `?type=intern|extern`, `?leerling=<id>`).
|
||||||
|
- Tijdsloten kennen een optionele `groep` (per-groep tijden); personen hebben
|
||||||
|
`stamgroep`, `vakken` en `geboortedatum`; activiteiten een `theorievak`-vlag.
|
||||||
- `/admin/` — Django-admin (personen, groepen, subgroepen en modulestatus).
|
- `/admin/` — Django-admin (personen, groepen, subgroepen en modulestatus).
|
||||||
|
|
||||||
> Draait SQLite niet op je projectmap (bv. op een netwerkschijf)? Zet dan
|
> Draait SQLite niet op je projectmap (bv. op een netwerkschijf)? Zet dan
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ MODULE_APPS = [
|
||||||
"modules.printing", # Printen/Exporteren
|
"modules.printing", # Printen/Exporteren
|
||||||
"modules.accounts", # Multi-user & toegang
|
"modules.accounts", # Multi-user & toegang
|
||||||
"modules.leerplein", # Leerplein (groepsstructuur)
|
"modules.leerplein", # Leerplein (groepsstructuur)
|
||||||
|
"modules.stage", # Stage (intern/extern)
|
||||||
]
|
]
|
||||||
|
|
||||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + CORE_APPS + MODULE_APPS
|
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + CORE_APPS + MODULE_APPS
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,5 @@ urlpatterns = [
|
||||||
path("api/", include("core.urls")),
|
path("api/", include("core.urls")),
|
||||||
# Module-API's.
|
# Module-API's.
|
||||||
path("api/", include("modules.leerplein.urls")),
|
path("api/", include("modules.leerplein.urls")),
|
||||||
|
path("api/", include("modules.stage.urls")),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,11 @@ class GroepAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(Persoon)
|
@admin.register(Persoon)
|
||||||
class PersoonAdmin(admin.ModelAdmin):
|
class PersoonAdmin(admin.ModelAdmin):
|
||||||
list_display = ("achternaam", "voornaam", "rol", "groep", "actief")
|
list_display = ("achternaam", "voornaam", "rol", "groep", "stamgroep", "actief")
|
||||||
list_filter = ("rol", "actief", "groep", "functies")
|
list_filter = ("rol", "actief", "groep", "stamgroep", "functies")
|
||||||
search_fields = ("voornaam", "achternaam")
|
search_fields = ("voornaam", "achternaam")
|
||||||
autocomplete_fields = ("groep",)
|
autocomplete_fields = ("groep", "stamgroep")
|
||||||
filter_horizontal = ("functies",)
|
filter_horizontal = ("functies", "vakken")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Subgroep)
|
@admin.register(Subgroep)
|
||||||
|
|
@ -53,14 +53,14 @@ class KalenderDagAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(Tijdslot)
|
@admin.register(Tijdslot)
|
||||||
class TijdslotAdmin(admin.ModelAdmin):
|
class TijdslotAdmin(admin.ModelAdmin):
|
||||||
list_display = ("naam", "dag", "start_tijd", "eind_tijd", "volgorde", "actief")
|
list_display = ("naam", "dag", "start_tijd", "eind_tijd", "volgorde", "groep", "actief")
|
||||||
list_filter = ("dag", "actief")
|
list_filter = ("dag", "actief", "groep")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Activiteit)
|
@admin.register(Activiteit)
|
||||||
class ActiviteitAdmin(admin.ModelAdmin):
|
class ActiviteitAdmin(admin.ModelAdmin):
|
||||||
list_display = ("naam", "kleur", "actief")
|
list_display = ("naam", "kleur", "theorievak", "actief")
|
||||||
list_filter = ("actief",)
|
list_filter = ("theorievak", "actief")
|
||||||
search_fields = ("naam",)
|
search_fields = ("naam",)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Generated by Django 5.2.15
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0003_afwezigheid_blokuitzondering'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='activiteit',
|
||||||
|
name='theorievak',
|
||||||
|
field=models.BooleanField(default=False, help_text='Theorievakken worden standaard aan nieuwe leerkrachten gekoppeld.', verbose_name='theorievak'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='persoon',
|
||||||
|
name='geboortedatum',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='geboortedatum'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='persoon',
|
||||||
|
name='stamgroep',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Stamgroep/plein van een medewerker.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='medewerkers', to='core.groep', verbose_name='stamgroep'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='persoon',
|
||||||
|
name='vakken',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Vakken die een medewerker kan geven.', related_name='docenten', to='core.activiteit', verbose_name='vakken'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tijdslot',
|
||||||
|
name='groep',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Leeg = algemene (vaste) tijden; gevuld = afwijkende tijden voor die groep.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tijdsloten', to='core.groep', verbose_name='groep'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -86,6 +86,23 @@ class Persoon(models.Model):
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Alleen van toepassing op medewerkers.",
|
help_text="Alleen van toepassing op medewerkers.",
|
||||||
)
|
)
|
||||||
|
stamgroep = models.ForeignKey(
|
||||||
|
Groep,
|
||||||
|
verbose_name="stamgroep",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="medewerkers",
|
||||||
|
help_text="Stamgroep/plein van een medewerker.",
|
||||||
|
)
|
||||||
|
vakken = models.ManyToManyField(
|
||||||
|
"Activiteit",
|
||||||
|
verbose_name="vakken",
|
||||||
|
related_name="docenten",
|
||||||
|
blank=True,
|
||||||
|
help_text="Vakken die een medewerker kan geven.",
|
||||||
|
)
|
||||||
|
geboortedatum = models.DateField("geboortedatum", null=True, blank=True)
|
||||||
actief = models.BooleanField("actief", default=True)
|
actief = models.BooleanField("actief", default=True)
|
||||||
opmerkingen = models.TextField("opmerkingen", blank=True)
|
opmerkingen = models.TextField("opmerkingen", blank=True)
|
||||||
aangemaakt_op = models.DateTimeField(auto_now_add=True)
|
aangemaakt_op = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
@ -107,9 +124,11 @@ class Persoon(models.Model):
|
||||||
return self.rol == Rol.LEERLING
|
return self.rol == Rol.LEERLING
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# Een groepskoppeling hoort alleen bij leerlingen.
|
# 'groep' is leerling-only; 'stamgroep' is medewerker-only.
|
||||||
if self.rol != Rol.LEERLING:
|
if self.rol != Rol.LEERLING:
|
||||||
self.groep = None
|
self.groep = None
|
||||||
|
else:
|
||||||
|
self.stamgroep = None
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
if self.pk:
|
if self.pk:
|
||||||
# Wie geen leerling (meer) is, hoort niet in subgroepen thuis.
|
# Wie geen leerling (meer) is, hoort niet in subgroepen thuis.
|
||||||
|
|
@ -117,8 +136,9 @@ class Persoon(models.Model):
|
||||||
if self.rol != Rol.LEERLING:
|
if self.rol != Rol.LEERLING:
|
||||||
self.subgroepen.clear()
|
self.subgroepen.clear()
|
||||||
else:
|
else:
|
||||||
# Een leerling heeft geen medewerkersfuncties.
|
# Een leerling heeft geen medewerkersfuncties of -vakken.
|
||||||
self.functies.clear()
|
self.functies.clear()
|
||||||
|
self.vakken.clear()
|
||||||
|
|
||||||
|
|
||||||
class Subgroep(models.Model):
|
class Subgroep(models.Model):
|
||||||
|
|
@ -223,6 +243,15 @@ class Tijdslot(models.Model):
|
||||||
start_tijd = models.TimeField("starttijd")
|
start_tijd = models.TimeField("starttijd")
|
||||||
eind_tijd = models.TimeField("eindtijd")
|
eind_tijd = models.TimeField("eindtijd")
|
||||||
volgorde = models.PositiveSmallIntegerField("volgorde", default=0)
|
volgorde = models.PositiveSmallIntegerField("volgorde", default=0)
|
||||||
|
groep = models.ForeignKey(
|
||||||
|
Groep,
|
||||||
|
verbose_name="groep",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="tijdsloten",
|
||||||
|
help_text="Leeg = algemene (vaste) tijden; gevuld = afwijkende tijden voor die groep.",
|
||||||
|
)
|
||||||
actief = models.BooleanField("actief", default=True)
|
actief = models.BooleanField("actief", default=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -231,7 +260,8 @@ class Tijdslot(models.Model):
|
||||||
ordering = ["dag", "volgorde", "start_tijd"]
|
ordering = ["dag", "volgorde", "start_tijd"]
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.get_dag_display()} {self.start_tijd:%H:%M}-{self.eind_tijd:%H:%M} {self.naam}"
|
suffix = f" ({self.groep.naam})" if self.groep_id else ""
|
||||||
|
return f"{self.get_dag_display()} {self.start_tijd:%H:%M}-{self.eind_tijd:%H:%M} {self.naam}{suffix}"
|
||||||
|
|
||||||
|
|
||||||
class Activiteit(models.Model):
|
class Activiteit(models.Model):
|
||||||
|
|
@ -240,6 +270,10 @@ class Activiteit(models.Model):
|
||||||
naam = models.CharField("naam", max_length=80, unique=True)
|
naam = models.CharField("naam", max_length=80, unique=True)
|
||||||
kleur = models.CharField("kleur", max_length=7, default="#2563eb", help_text="hex, bv. #2563eb")
|
kleur = models.CharField("kleur", max_length=7, default="#2563eb", help_text="hex, bv. #2563eb")
|
||||||
omschrijving = models.CharField("omschrijving", max_length=255, blank=True)
|
omschrijving = models.CharField("omschrijving", max_length=255, blank=True)
|
||||||
|
theorievak = models.BooleanField(
|
||||||
|
"theorievak", default=False,
|
||||||
|
help_text="Theorievakken worden standaard aan nieuwe leerkrachten gekoppeld.",
|
||||||
|
)
|
||||||
actief = models.BooleanField("actief", default=True)
|
actief = models.BooleanField("actief", default=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
|
|
@ -29,32 +29,51 @@ class PersoonSerializer(serializers.ModelSerializer):
|
||||||
volledige_naam = serializers.CharField(read_only=True)
|
volledige_naam = serializers.CharField(read_only=True)
|
||||||
rol_label = serializers.CharField(source="get_rol_display", read_only=True)
|
rol_label = serializers.CharField(source="get_rol_display", read_only=True)
|
||||||
groep_naam = serializers.CharField(source="groep.naam", read_only=True, default=None)
|
groep_naam = serializers.CharField(source="groep.naam", read_only=True, default=None)
|
||||||
|
stamgroep_naam = serializers.CharField(source="stamgroep.naam", read_only=True, default=None)
|
||||||
functies = serializers.PrimaryKeyRelatedField(
|
functies = serializers.PrimaryKeyRelatedField(
|
||||||
many=True, queryset=Functie.objects.all(), required=False
|
many=True, queryset=Functie.objects.all(), required=False
|
||||||
)
|
)
|
||||||
functies_namen = serializers.SerializerMethodField()
|
functies_namen = serializers.SerializerMethodField()
|
||||||
|
vakken = serializers.PrimaryKeyRelatedField(
|
||||||
|
many=True, queryset=Activiteit.objects.all(), required=False
|
||||||
|
)
|
||||||
|
vakken_namen = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Persoon
|
model = Persoon
|
||||||
fields = [
|
fields = [
|
||||||
"id", "voornaam", "achternaam", "volledige_naam",
|
"id", "voornaam", "achternaam", "volledige_naam",
|
||||||
"rol", "rol_label", "groep", "groep_naam",
|
"rol", "rol_label", "groep", "groep_naam",
|
||||||
"functies", "functies_namen",
|
"stamgroep", "stamgroep_naam",
|
||||||
"actief", "opmerkingen", "aangemaakt_op",
|
"functies", "functies_namen", "vakken", "vakken_namen",
|
||||||
|
"geboortedatum", "actief", "opmerkingen", "aangemaakt_op",
|
||||||
]
|
]
|
||||||
read_only_fields = ["aangemaakt_op"]
|
read_only_fields = ["aangemaakt_op"]
|
||||||
|
|
||||||
def get_functies_namen(self, obj):
|
def get_functies_namen(self, obj):
|
||||||
return [{"id": f.id, "naam": f.naam} for f in obj.functies.all()]
|
return [{"id": f.id, "naam": f.naam} for f in obj.functies.all()]
|
||||||
|
|
||||||
|
def get_vakken_namen(self, obj):
|
||||||
|
return [{"id": a.id, "naam": a.naam} for a in obj.vakken.all()]
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
# Functies horen alleen bij medewerkers; een leerling krijgt er geen.
|
# Functies/vakken horen alleen bij medewerkers; een leerling krijgt er geen.
|
||||||
# (De M2M wordt door DRF ná save() gezet, dus hier afdwingen.)
|
# (De M2M wordt door DRF ná save() gezet, dus hier afdwingen.)
|
||||||
rol = data.get("rol", getattr(self.instance, "rol", None))
|
rol = data.get("rol", getattr(self.instance, "rol", None))
|
||||||
if rol == "leerling":
|
if rol == "leerling":
|
||||||
data["functies"] = []
|
data["functies"] = []
|
||||||
|
data["vakken"] = []
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
# Een leerkracht krijgt de theorievakken standaard direct gekoppeld.
|
||||||
|
if validated_data.get("rol") == "leerkracht":
|
||||||
|
theorie = list(Activiteit.objects.filter(theorievak=True, actief=True))
|
||||||
|
gekozen = validated_data.get("vakken", [])
|
||||||
|
ids = {v.id for v in gekozen}
|
||||||
|
validated_data["vakken"] = gekozen + [a for a in theorie if a.id not in ids]
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
class SubgroepSerializer(serializers.ModelSerializer):
|
class SubgroepSerializer(serializers.ModelSerializer):
|
||||||
leden = serializers.PrimaryKeyRelatedField(
|
leden = serializers.PrimaryKeyRelatedField(
|
||||||
|
|
@ -104,9 +123,11 @@ class KalenderDagSerializer(serializers.ModelSerializer):
|
||||||
class TijdslotSerializer(serializers.ModelSerializer):
|
class TijdslotSerializer(serializers.ModelSerializer):
|
||||||
dag_label = serializers.CharField(source="get_dag_display", read_only=True)
|
dag_label = serializers.CharField(source="get_dag_display", read_only=True)
|
||||||
|
|
||||||
|
groep_naam = serializers.CharField(source="groep.naam", read_only=True, default=None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tijdslot
|
model = Tijdslot
|
||||||
fields = ["id", "naam", "dag", "dag_label", "start_tijd", "eind_tijd", "volgorde", "actief"]
|
fields = ["id", "naam", "dag", "dag_label", "start_tijd", "eind_tijd", "volgorde", "groep", "groep_naam", "actief"]
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
start = data.get("start_tijd", getattr(self.instance, "start_tijd", None))
|
start = data.get("start_tijd", getattr(self.instance, "start_tijd", None))
|
||||||
|
|
@ -124,7 +145,7 @@ class ActiviteitSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Activiteit
|
model = Activiteit
|
||||||
fields = ["id", "naam", "kleur", "omschrijving", "actief"]
|
fields = ["id", "naam", "kleur", "omschrijving", "theorievak", "actief"]
|
||||||
|
|
||||||
|
|
||||||
class LocatieSerializer(serializers.ModelSerializer):
|
class LocatieSerializer(serializers.ModelSerializer):
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ class PersoonViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = PersoonSerializer
|
serializer_class = PersoonSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = Persoon.objects.select_related("groep").prefetch_related("functies").all()
|
qs = Persoon.objects.select_related("groep", "stamgroep").prefetch_related("functies", "vakken").all()
|
||||||
rol = self.request.query_params.get("rol")
|
rol = self.request.query_params.get("rol")
|
||||||
if rol:
|
if rol:
|
||||||
qs = qs.filter(rol=rol)
|
qs = qs.filter(rol=rol)
|
||||||
|
|
|
||||||
Binary file not shown.
0
backend/modules/stage/__init__.py
Normal file
0
backend/modules/stage/__init__.py
Normal file
10
backend/modules/stage/admin.py
Normal file
10
backend/modules/stage/admin.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import Stage
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Stage)
|
||||||
|
class StageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("leerling", "type", "begeleider", "locatie", "organisatie", "actief")
|
||||||
|
list_filter = ("type", "actief", "dag")
|
||||||
|
search_fields = ("leerling__voornaam", "leerling__achternaam", "organisatie")
|
||||||
29
backend/modules/stage/apps.py
Normal file
29
backend/modules/stage/apps.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class StageConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "modules.stage"
|
||||||
|
label = "module_stage"
|
||||||
|
verbose_name = "Module: Stage (intern/extern)"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from plugins.registry import MenuItem, ModuleSpec, registry
|
||||||
|
|
||||||
|
registry.register(
|
||||||
|
ModuleSpec(
|
||||||
|
key="stage",
|
||||||
|
name="Stage (intern/extern)",
|
||||||
|
description=(
|
||||||
|
"Beheer interne en externe stages van leerlingen. Beide "
|
||||||
|
"krijgen een begeleider; een interne stage ook een ruimte."
|
||||||
|
),
|
||||||
|
version="0.1.0",
|
||||||
|
category="Structuur",
|
||||||
|
core=False,
|
||||||
|
default_enabled=False,
|
||||||
|
menu_items=(
|
||||||
|
MenuItem("Stage", "/stage", icon="pin", order=60, group="Groepen"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
36
backend/modules/stage/migrations/0001_initial.py
Normal file
36
backend/modules/stage/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Generated by Django 5.2.15
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0004_activiteit_theorievak_persoon_geboortedatum_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Stage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('type', models.CharField(choices=[('intern', 'Interne stage'), ('extern', 'Externe stage')], max_length=10, verbose_name='type')),
|
||||||
|
('organisatie', models.CharField(blank=True, max_length=160, verbose_name='externe organisatie')),
|
||||||
|
('dag', models.IntegerField(blank=True, choices=[(0, 'Maandag'), (1, 'Dinsdag'), (2, 'Woensdag'), (3, 'Donderdag'), (4, 'Vrijdag')], null=True, verbose_name='dag')),
|
||||||
|
('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)),
|
||||||
|
('begeleider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='stagebegeleidingen', to='core.persoon')),
|
||||||
|
('leerling', models.ForeignKey(limit_choices_to={'rol': 'leerling'}, on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='core.persoon')),
|
||||||
|
('locatie', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stages', to='core.locatie')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'stage',
|
||||||
|
'verbose_name_plural': 'stages',
|
||||||
|
'ordering': ['leerling__achternaam', 'leerling__voornaam'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
backend/modules/stage/migrations/__init__.py
Normal file
0
backend/modules/stage/migrations/__init__.py
Normal file
60
backend/modules/stage/models.py
Normal file
60
backend/modules/stage/models.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
"""
|
||||||
|
Stage-module: interne en externe stages van leerlingen.
|
||||||
|
|
||||||
|
- Beide stagevormen krijgen een begeleider (verplicht).
|
||||||
|
- Een interne stage krijgt daarnaast een ruimte (locatie).
|
||||||
|
- Een externe stage legt de externe organisatie vast.
|
||||||
|
"""
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class StageType(models.TextChoices):
|
||||||
|
INTERN = "intern", "Interne stage"
|
||||||
|
EXTERN = "extern", "Externe stage"
|
||||||
|
|
||||||
|
|
||||||
|
class Weekdag(models.IntegerChoices):
|
||||||
|
MAANDAG = 0, "Maandag"
|
||||||
|
DINSDAG = 1, "Dinsdag"
|
||||||
|
WOENSDAG = 2, "Woensdag"
|
||||||
|
DONDERDAG = 3, "Donderdag"
|
||||||
|
VRIJDAG = 4, "Vrijdag"
|
||||||
|
|
||||||
|
|
||||||
|
class Stage(models.Model):
|
||||||
|
leerling = models.ForeignKey(
|
||||||
|
"core.Persoon", on_delete=models.CASCADE, related_name="stages",
|
||||||
|
limit_choices_to={"rol": "leerling"},
|
||||||
|
)
|
||||||
|
type = models.CharField("type", max_length=10, choices=StageType.choices)
|
||||||
|
begeleider = models.ForeignKey(
|
||||||
|
"core.Persoon", on_delete=models.PROTECT, related_name="stagebegeleidingen",
|
||||||
|
help_text="Verplicht voor zowel interne als externe stage.",
|
||||||
|
)
|
||||||
|
# Interne stage: een ruimte binnen de school.
|
||||||
|
locatie = models.ForeignKey(
|
||||||
|
"core.Locatie", on_delete=models.SET_NULL, null=True, blank=True,
|
||||||
|
related_name="stages",
|
||||||
|
)
|
||||||
|
# Externe stage: de externe organisatie.
|
||||||
|
organisatie = models.CharField("externe organisatie", max_length=160, blank=True)
|
||||||
|
|
||||||
|
dag = models.IntegerField("dag", choices=Weekdag.choices, null=True, blank=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 = "stage"
|
||||||
|
verbose_name_plural = "stages"
|
||||||
|
ordering = ["leerling__achternaam", "leerling__voornaam"]
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if self.type == StageType.INTERN and not self.locatie_id:
|
||||||
|
raise ValidationError("Een interne stage vereist een ruimte (locatie).")
|
||||||
|
if self.type == StageType.EXTERN and not self.organisatie:
|
||||||
|
raise ValidationError("Een externe stage vereist een externe organisatie.")
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.leerling} – {self.get_type_display()}"
|
||||||
35
backend/modules/stage/serializers.py
Normal file
35
backend/modules/stage/serializers.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from .models import Stage, StageType
|
||||||
|
|
||||||
|
|
||||||
|
class StageSerializer(serializers.ModelSerializer):
|
||||||
|
leerling_naam = serializers.CharField(source="leerling.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)
|
||||||
|
type_label = serializers.CharField(source="get_type_display", read_only=True)
|
||||||
|
dag_label = serializers.CharField(source="get_dag_display", read_only=True, default=None)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Stage
|
||||||
|
fields = [
|
||||||
|
"id", "leerling", "leerling_naam", "type", "type_label",
|
||||||
|
"begeleider", "begeleider_naam", "locatie", "locatie_naam",
|
||||||
|
"organisatie", "dag", "dag_label", "omschrijving", "actief",
|
||||||
|
"aangemaakt_op",
|
||||||
|
]
|
||||||
|
read_only_fields = ["aangemaakt_op"]
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
type_ = data.get("type", getattr(self.instance, "type", None))
|
||||||
|
locatie = data.get("locatie", getattr(self.instance, "locatie", None))
|
||||||
|
organisatie = data.get("organisatie", getattr(self.instance, "organisatie", ""))
|
||||||
|
begeleider = data.get("begeleider", getattr(self.instance, "begeleider", None))
|
||||||
|
|
||||||
|
if begeleider is not None and begeleider.rol == "leerling":
|
||||||
|
raise serializers.ValidationError("Een leerling kan geen begeleider zijn.")
|
||||||
|
if type_ == StageType.INTERN and not locatie:
|
||||||
|
raise serializers.ValidationError("Een interne stage vereist een ruimte (locatie).")
|
||||||
|
if type_ == StageType.EXTERN and not organisatie:
|
||||||
|
raise serializers.ValidationError("Een externe stage vereist een externe organisatie.")
|
||||||
|
return data
|
||||||
9
backend/modules/stage/urls.py
Normal file
9
backend/modules/stage/urls.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from django.urls import include, path
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register("stages", views.StageViewSet, basename="stage")
|
||||||
|
|
||||||
|
urlpatterns = [path("", include(router.urls))]
|
||||||
18
backend/modules/stage/views.py
Normal file
18
backend/modules/stage/views.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from rest_framework import viewsets
|
||||||
|
|
||||||
|
from .models import Stage
|
||||||
|
from .serializers import StageSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class StageViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = StageSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = Stage.objects.select_related("leerling", "begeleider", "locatie").all()
|
||||||
|
type_ = self.request.query_params.get("type")
|
||||||
|
if type_:
|
||||||
|
qs = qs.filter(type=type_)
|
||||||
|
leerling = self.request.query_params.get("leerling")
|
||||||
|
if leerling:
|
||||||
|
qs = qs.filter(leerling_id=leerling)
|
||||||
|
return qs
|
||||||
|
|
@ -4,7 +4,7 @@ import Logo from "./Logo.jsx";
|
||||||
import {
|
import {
|
||||||
GroepenPage, PersonenPage, SubgroepenPage, FunctiesPage,
|
GroepenPage, PersonenPage, SubgroepenPage, FunctiesPage,
|
||||||
ActiviteitenPage, LocatiesPage, TijdslotenPage, SchooljaarPage, RoosterPage,
|
ActiviteitenPage, LocatiesPage, TijdslotenPage, SchooljaarPage, RoosterPage,
|
||||||
AfwezigheidPage, LeerpleinPage,
|
AfwezigheidPage, LeerpleinPage, StagePage,
|
||||||
} from "./pages.jsx";
|
} from "./pages.jsx";
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
|
|
@ -27,6 +27,7 @@ const PAGES = {
|
||||||
"/schooljaar": SchooljaarPage,
|
"/schooljaar": SchooljaarPage,
|
||||||
"/afwezigheid": AfwezigheidPage,
|
"/afwezigheid": AfwezigheidPage,
|
||||||
"/leerplein": LeerpleinPage,
|
"/leerplein": LeerpleinPage,
|
||||||
|
"/stage": StagePage,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,16 @@ function Foutmelding({ msg }) {
|
||||||
|
|
||||||
/* ----------------------- Generieke eenvoudige catalogus ------------------- */
|
/* ----------------------- Generieke eenvoudige catalogus ------------------- */
|
||||||
// Voor lijsten met alleen naam + omschrijving (Functies, Activiteiten, Locaties).
|
// Voor lijsten met alleen naam + omschrijving (Functies, Activiteiten, Locaties).
|
||||||
function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom }) {
|
function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTheorievak = false }) {
|
||||||
const { items, error, loading, reload, setError } = useList(resource);
|
const { items, error, loading, reload, setError } = useList(resource);
|
||||||
const leeg = kleur
|
const leeg = {
|
||||||
? { naam: "", omschrijving: "", kleur: "#2563eb" }
|
naam: "", omschrijving: "",
|
||||||
: { naam: "", omschrijving: "" };
|
...(kleur ? { kleur: "#2563eb" } : {}),
|
||||||
|
...(metTheorievak ? { theorievak: false } : {}),
|
||||||
|
};
|
||||||
const [form, setForm] = useState(leeg);
|
const [form, setForm] = useState(leeg);
|
||||||
const [editId, setEditId] = useState(null);
|
const [editId, setEditId] = useState(null);
|
||||||
|
const kolommen = 3 + (telKolom ? 1 : 0) + (metTheorievak ? 1 : 0);
|
||||||
|
|
||||||
async function opslaan(e) {
|
async function opslaan(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -84,6 +87,14 @@ function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom }) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function bewerk(it) {
|
||||||
|
setEditId(it.id);
|
||||||
|
setForm({
|
||||||
|
naam: it.naam, omschrijving: it.omschrijving,
|
||||||
|
...(kleur ? { kleur: it.kleur } : {}),
|
||||||
|
...(metTheorievak ? { theorievak: it.theorievak } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
|
|
@ -91,32 +102,20 @@ function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom }) {
|
||||||
{uitleg && <p className="muted">{uitleg}</p>}
|
{uitleg && <p className="muted">{uitleg}</p>}
|
||||||
<Foutmelding msg={error} />
|
<Foutmelding msg={error} />
|
||||||
<form className="form-row" onSubmit={opslaan}>
|
<form className="form-row" onSubmit={opslaan}>
|
||||||
<input
|
<input placeholder="Naam" value={form.naam} required onChange={(e) => setForm({ ...form, naam: e.target.value })} />
|
||||||
placeholder="Naam"
|
<input placeholder="Omschrijving (optioneel)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} />
|
||||||
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 })}
|
|
||||||
/>
|
|
||||||
{kleur && (
|
{kleur && (
|
||||||
<input
|
<input type="color" value={form.kleur} title="Kleur" onChange={(e) => setForm({ ...form, kleur: e.target.value })} />
|
||||||
type="color"
|
|
||||||
value={form.kleur}
|
|
||||||
title="Kleur"
|
|
||||||
onChange={(e) => setForm({ ...form, kleur: e.target.value })}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<button className="btn primary" type="submit">
|
{metTheorievak && (
|
||||||
{editId ? "Opslaan" : "Toevoegen"}
|
<label className="veld">
|
||||||
</button>
|
<input type="checkbox" checked={!!form.theorievak} onChange={(e) => setForm({ ...form, theorievak: e.target.checked })} />
|
||||||
|
theorievak
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
|
||||||
{editId && (
|
{editId && (
|
||||||
<button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>
|
<button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>
|
||||||
Annuleren
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|
@ -127,6 +126,7 @@ function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom }) {
|
||||||
<tr>
|
<tr>
|
||||||
<th>Naam</th>
|
<th>Naam</th>
|
||||||
<th>Omschrijving</th>
|
<th>Omschrijving</th>
|
||||||
|
{metTheorievak && <th>Theorievak</th>}
|
||||||
{telKolom && <th>{telKolom}</th>}
|
{telKolom && <th>{telKolom}</th>}
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -135,37 +135,20 @@ function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom }) {
|
||||||
{items.map((it) => (
|
{items.map((it) => (
|
||||||
<tr key={it.id}>
|
<tr key={it.id}>
|
||||||
<td>
|
<td>
|
||||||
{kleur && (
|
{kleur && <span className="kleur-stip" style={{ background: it.kleur }} />}
|
||||||
<span className="kleur-stip" style={{ background: it.kleur }} />
|
|
||||||
)}
|
|
||||||
{it.naam}
|
{it.naam}
|
||||||
</td>
|
</td>
|
||||||
<td className="muted">{it.omschrijving || "—"}</td>
|
<td className="muted">{it.omschrijving || "—"}</td>
|
||||||
|
{metTheorievak && <td>{it.theorievak ? "✓" : "—"}</td>}
|
||||||
{telKolom && <td>{it.aantal_personen ?? "—"}</td>}
|
{telKolom && <td>{it.aantal_personen ?? "—"}</td>}
|
||||||
<td className="row-actions">
|
<td className="row-actions">
|
||||||
<button
|
<button className="link" onClick={() => bewerk(it)}>bewerken</button>
|
||||||
className="link"
|
<button className="link danger" onClick={() => verwijder(it.id)}>verwijderen</button>
|
||||||
onClick={() => {
|
|
||||||
setEditId(it.id);
|
|
||||||
setForm(
|
|
||||||
kleur
|
|
||||||
? { naam: it.naam, omschrijving: it.omschrijving, kleur: it.kleur }
|
|
||||||
: { naam: it.naam, omschrijving: it.omschrijving }
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
bewerken
|
|
||||||
</button>
|
|
||||||
<button className="link danger" onClick={() => verwijder(it.id)}>
|
|
||||||
verwijderen
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{items.length === 0 && (
|
{items.length === 0 && (
|
||||||
<tr>
|
<tr><td colSpan={kolommen} className="muted">Nog niets.</td></tr>
|
||||||
<td colSpan={telKolom ? 4 : 3} className="muted">Nog niets.</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -188,10 +171,11 @@ export function FunctiesPage() {
|
||||||
export function ActiviteitenPage() {
|
export function ActiviteitenPage() {
|
||||||
return (
|
return (
|
||||||
<CatalogusPage
|
<CatalogusPage
|
||||||
titel="Activiteiten"
|
titel="Activiteiten / vakken"
|
||||||
uitleg="Wat er gedaan wordt: rekenen, gym, therapie, pauze … Elke activiteit heeft een kleur voor in het rooster."
|
uitleg="Wat er gedaan wordt: rekenen, gym, therapie, pauze … Elke activiteit heeft een kleur. Markeer theorievakken; die koppelen automatisch aan nieuwe leerkrachten."
|
||||||
resource="activiteiten"
|
resource="activiteiten"
|
||||||
kleur
|
kleur
|
||||||
|
metTheorievak
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -270,43 +254,51 @@ export function GroepenPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------- Personen -------------------------------- */
|
/* -------------------------------- Personen -------------------------------- */
|
||||||
|
const STAFF_ROLLEN = ROLLEN.filter((r) => r.value !== "leerling");
|
||||||
|
|
||||||
export function PersonenPage() {
|
export function PersonenPage() {
|
||||||
const { items, error, loading, reload, setError } = useList("personen");
|
const { items, error, loading, reload, setError } = useList("personen");
|
||||||
const [groepen, setGroepen] = useState([]);
|
const [groepen, setGroepen] = useState([]);
|
||||||
const [functies, setFuncties] = useState([]);
|
const [functies, setFuncties] = useState([]);
|
||||||
const leeg = { voornaam: "", achternaam: "", rol: "leerling", groep: "", functies: [] };
|
const [vakken, setVakken] = useState([]);
|
||||||
const [form, setForm] = useState(leeg);
|
// Twee losse flows: medewerker en leerling.
|
||||||
|
const [modus, setModus] = useState("leerling");
|
||||||
|
const leegMedewerker = { voornaam: "", achternaam: "", rol: "leerkracht", stamgroep: "", functies: [], vakken: [] };
|
||||||
|
const leegLeerling = { voornaam: "", achternaam: "", groep: "", geboortedatum: "" };
|
||||||
|
const [form, setForm] = useState(leegLeerling);
|
||||||
const [editId, setEditId] = useState(null);
|
const [editId, setEditId] = useState(null);
|
||||||
const [filter, setFilter] = useState("");
|
const [filter, setFilter] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listResource("groepen").then(setGroepen).catch(() => {});
|
listResource("groepen").then(setGroepen).catch(() => {});
|
||||||
listResource("functies").then(setFuncties).catch(() => {});
|
listResource("functies").then(setFuncties).catch(() => {});
|
||||||
|
listResource("activiteiten").then(setVakken).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isLeerling = form.rol === "leerling";
|
function kiesModus(m) {
|
||||||
|
setModus(m);
|
||||||
function toggleFunctie(id) {
|
setEditId(null);
|
||||||
setForm((f) =>
|
setForm(m === "medewerker" ? leegMedewerker : leegLeerling);
|
||||||
f.functies.includes(id)
|
}
|
||||||
? { ...f, functies: f.functies.filter((x) => x !== id) }
|
function toggle(veld, id) {
|
||||||
: { ...f, functies: [...f.functies, id] }
|
setForm((f) => f[veld].includes(id) ? { ...f, [veld]: f[veld].filter((x) => x !== id) } : { ...f, [veld]: [...f[veld], id] });
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function opslaan(e) {
|
async function opslaan(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const payload = {
|
const payload = modus === "medewerker"
|
||||||
voornaam: form.voornaam,
|
? {
|
||||||
achternaam: form.achternaam,
|
voornaam: form.voornaam, achternaam: form.achternaam, rol: form.rol,
|
||||||
rol: form.rol,
|
stamgroep: form.stamgroep || null, functies: form.functies, vakken: form.vakken,
|
||||||
groep: isLeerling && form.groep ? form.groep : null,
|
}
|
||||||
functies: isLeerling ? [] : form.functies,
|
: {
|
||||||
};
|
voornaam: form.voornaam, achternaam: form.achternaam, rol: "leerling",
|
||||||
|
groep: form.groep || null, geboortedatum: form.geboortedatum || null,
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
if (editId) await updateResource("personen", editId, payload);
|
if (editId) await updateResource("personen", editId, payload);
|
||||||
else await createResource("personen", payload);
|
else await createResource("personen", payload);
|
||||||
setForm(leeg);
|
setForm(modus === "medewerker" ? leegMedewerker : leegLeerling);
|
||||||
setEditId(null);
|
setEditId(null);
|
||||||
await reload();
|
await reload();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -315,92 +307,122 @@ export function PersonenPage() {
|
||||||
}
|
}
|
||||||
async function verwijder(id) {
|
async function verwijder(id) {
|
||||||
if (!confirm("Deze persoon verwijderen?")) return;
|
if (!confirm("Deze persoon verwijderen?")) return;
|
||||||
try {
|
try { await deleteResource("personen", id); await reload(); } catch (err) { setError(err.message); }
|
||||||
await deleteResource("personen", id);
|
}
|
||||||
await reload();
|
function bewerk(p) {
|
||||||
} catch (err) {
|
setEditId(p.id);
|
||||||
setError(err.message);
|
if (p.rol === "leerling") {
|
||||||
|
setModus("leerling");
|
||||||
|
setForm({ voornaam: p.voornaam, achternaam: p.achternaam, groep: p.groep || "", geboortedatum: p.geboortedatum || "" });
|
||||||
|
} else {
|
||||||
|
setModus("medewerker");
|
||||||
|
setForm({
|
||||||
|
voornaam: p.voornaam, achternaam: p.achternaam, rol: p.rol,
|
||||||
|
stamgroep: p.stamgroep || "",
|
||||||
|
functies: p.functies_namen.map((x) => x.id),
|
||||||
|
vakken: p.vakken_namen.map((x) => x.id),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const zichtbaar = filter ? items.filter((p) => p.rol === filter) : items;
|
const zichtbaar = filter === "leerling" ? items.filter((p) => p.rol === "leerling")
|
||||||
|
: filter === "medewerker" ? items.filter((p) => p.rol !== "leerling")
|
||||||
|
: items;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1>Personen</h1>
|
<h1>Personen</h1>
|
||||||
<p className="muted">Leerlingen en medewerkers. Medewerkers kunnen meerdere functies hebben.</p>
|
<p className="muted">Personeel en leerlingen staan los van elkaar. Kies wat je wilt toevoegen.</p>
|
||||||
<Foutmelding msg={error} />
|
<Foutmelding msg={error} />
|
||||||
|
|
||||||
|
<div className="tabbar">
|
||||||
|
<button className={modus === "leerling" ? "tab on" : "tab"} onClick={() => kiesModus("leerling")}>Leerling toevoegen</button>
|
||||||
|
<button className={modus === "medewerker" ? "tab on" : "tab"} onClick={() => kiesModus("medewerker")}>Medewerker toevoegen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form className="form-card" onSubmit={opslaan}>
|
<form className="form-card" onSubmit={opslaan}>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<input placeholder="Voornaam" value={form.voornaam} required onChange={(e) => setForm({ ...form, voornaam: e.target.value })} />
|
<input placeholder="Voornaam" value={form.voornaam} required onChange={(e) => setForm({ ...form, voornaam: e.target.value })} />
|
||||||
<input placeholder="Achternaam" value={form.achternaam} required onChange={(e) => setForm({ ...form, achternaam: e.target.value })} />
|
<input placeholder="Achternaam" value={form.achternaam} required onChange={(e) => setForm({ ...form, achternaam: e.target.value })} />
|
||||||
<select value={form.rol} onChange={(e) => setForm({ ...form, rol: e.target.value })}>
|
{modus === "leerling" ? (
|
||||||
{ROLLEN.map((r) => <option key={r.value} value={r.value}>{r.label}</option>)}
|
<>
|
||||||
</select>
|
<select value={form.groep || ""} onChange={(e) => setForm({ ...form, groep: e.target.value })}>
|
||||||
{isLeerling && (
|
<option value="">— groep —</option>
|
||||||
<select value={form.groep || ""} onChange={(e) => setForm({ ...form, groep: e.target.value })}>
|
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam}</option>)}
|
||||||
<option value="">— geen groep —</option>
|
</select>
|
||||||
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam}</option>)}
|
<label className="veld">Geb. <input type="date" value={form.geboortedatum || ""} onChange={(e) => setForm({ ...form, geboortedatum: e.target.value })} /></label>
|
||||||
</select>
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<select value={form.rol} onChange={(e) => setForm({ ...form, rol: e.target.value })}>
|
||||||
|
{STAFF_ROLLEN.map((r) => <option key={r.value} value={r.value}>{r.label}</option>)}
|
||||||
|
</select>
|
||||||
|
<select value={form.stamgroep || ""} onChange={(e) => setForm({ ...form, stamgroep: e.target.value })}>
|
||||||
|
<option value="">— stamgroep/plein —</option>
|
||||||
|
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam}</option>)}
|
||||||
|
</select>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isLeerling && (
|
|
||||||
<div className="leden-keuze">
|
{modus === "medewerker" && (
|
||||||
<div className="muted small">Functies (meerdere mogelijk):</div>
|
<>
|
||||||
<div className="chips">
|
<div className="leden-keuze">
|
||||||
{functies.map((f) => (
|
<div className="muted small">Functies:</div>
|
||||||
<button type="button" key={f.id} className={form.functies.includes(f.id) ? "chip on" : "chip"} onClick={() => toggleFunctie(f.id)}>
|
<div className="chips">
|
||||||
{f.naam}
|
{functies.map((f) => <button type="button" key={f.id} className={form.functies.includes(f.id) ? "chip on" : "chip"} onClick={() => toggle("functies", f.id)}>{f.naam}</button>)}
|
||||||
</button>
|
{functies.length === 0 && <span className="muted small">Nog geen functies.</span>}
|
||||||
))}
|
</div>
|
||||||
{functies.length === 0 && <span className="muted small">Nog geen functies — voeg ze toe bij Functies.</span>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="leden-keuze">
|
||||||
|
<div className="muted small">Vakken die gegeven kunnen worden{form.rol === "leerkracht" ? " (theorievakken worden automatisch gekoppeld)" : ""}:</div>
|
||||||
|
<div className="chips">
|
||||||
|
{vakken.map((a) => <button type="button" key={a.id} className={form.vakken.includes(a.id) ? "chip on" : "chip"} onClick={() => toggle("vakken", a.id)}>{a.naam}{a.theorievak ? " ●" : ""}</button>)}
|
||||||
|
{vakken.length === 0 && <span className="muted small">Nog geen vakken/activiteiten.</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
|
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
|
||||||
{editId && <button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>}
|
{editId && <button type="button" className="btn" onClick={() => kiesModus(modus)}>Annuleren</button>}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="filterbar">
|
<div className="filterbar">
|
||||||
<span className="muted">Filter:</span>
|
<span className="muted">Toon:</span>
|
||||||
<button className={filter === "" ? "chip on" : "chip"} onClick={() => setFilter("")}>Alle</button>
|
<button className={filter === "" ? "chip on" : "chip"} onClick={() => setFilter("")}>Alle</button>
|
||||||
{ROLLEN.map((r) => (
|
<button className={filter === "leerling" ? "chip on" : "chip"} onClick={() => setFilter("leerling")}>Leerlingen</button>
|
||||||
<button key={r.value} className={filter === r.value ? "chip on" : "chip"} onClick={() => setFilter(r.value)}>{r.label}</button>
|
<button className={filter === "medewerker" ? "chip on" : "chip"} onClick={() => setFilter("medewerker")}>Medewerkers</button>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? <p>Laden…</p> : (
|
{loading ? <p>Laden…</p> : (
|
||||||
<table className="grid">
|
<table className="grid">
|
||||||
<thead><tr><th>Naam</th><th>Rol</th><th>Groep / Functies</th><th></th></tr></thead>
|
<thead><tr><th>Naam</th><th>Rol</th><th>Details</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{zichtbaar.map((p) => (
|
{zichtbaar.map((p) => (
|
||||||
<tr key={p.id}>
|
<tr key={p.id}>
|
||||||
<td>{p.volledige_naam}</td>
|
<td>{p.volledige_naam}</td>
|
||||||
<td>{p.rol_label}</td>
|
<td>{p.rol_label}</td>
|
||||||
<td className="muted">
|
<td className="muted small">
|
||||||
{p.rol === "leerling"
|
{p.rol === "leerling" ? (
|
||||||
? (p.groep_naam || "—")
|
<>
|
||||||
: (p.functies_namen.length ? p.functies_namen.map((x) => x.naam).join(", ") : "—")}
|
{p.groep_naam ? <span>🗂️ {p.groep_naam} </span> : null}
|
||||||
|
{p.geboortedatum ? <span>🎂 {p.geboortedatum}</span> : null}
|
||||||
|
{!p.groep_naam && !p.geboortedatum ? "—" : null}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{p.stamgroep_naam ? <div>🗂️ {p.stamgroep_naam}</div> : null}
|
||||||
|
{p.functies_namen.length ? <div>🪪 {p.functies_namen.map((x) => x.naam).join(", ")}</div> : null}
|
||||||
|
{p.vakken_namen.length ? <div>📚 {p.vakken_namen.map((x) => x.naam).join(", ")}</div> : null}
|
||||||
|
{!p.stamgroep_naam && !p.functies_namen.length && !p.vakken_namen.length ? "—" : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="row-actions">
|
<td className="row-actions">
|
||||||
<button
|
<button className="link" onClick={() => bewerk(p)}>bewerken</button>
|
||||||
className="link"
|
|
||||||
onClick={() => {
|
|
||||||
setEditId(p.id);
|
|
||||||
setForm({
|
|
||||||
voornaam: p.voornaam,
|
|
||||||
achternaam: p.achternaam,
|
|
||||||
rol: p.rol,
|
|
||||||
groep: p.groep || "",
|
|
||||||
functies: p.functies_namen.map((x) => x.id),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
bewerken
|
|
||||||
</button>
|
|
||||||
<button className="link danger" onClick={() => verwijder(p.id)}>verwijderen</button>
|
<button className="link danger" onClick={() => verwijder(p.id)}>verwijderen</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -504,15 +526,19 @@ export function SubgroepenPage() {
|
||||||
/* ------------------------------- Tijdsloten ------------------------------- */
|
/* ------------------------------- Tijdsloten ------------------------------- */
|
||||||
export function TijdslotenPage() {
|
export function TijdslotenPage() {
|
||||||
const { items, error, loading, reload, setError } = useList("tijdsloten");
|
const { items, error, loading, reload, setError } = useList("tijdsloten");
|
||||||
const leeg = { naam: "", dag: 0, start_tijd: "08:30", eind_tijd: "10:00", volgorde: 1 };
|
const [groepen, setGroepen] = useState([]);
|
||||||
|
const leeg = { naam: "", dag: 0, start_tijd: "08:30", eind_tijd: "10:00", volgorde: 1, groep: "" };
|
||||||
const [form, setForm] = useState(leeg);
|
const [form, setForm] = useState(leeg);
|
||||||
const [editId, setEditId] = useState(null);
|
const [editId, setEditId] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => { listResource("groepen").then(setGroepen).catch(() => {}); }, []);
|
||||||
|
|
||||||
async function opslaan(e) {
|
async function opslaan(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
if (editId) await updateResource("tijdsloten", editId, form);
|
const payload = { ...form, groep: form.groep || null };
|
||||||
else await createResource("tijdsloten", form);
|
if (editId) await updateResource("tijdsloten", editId, payload);
|
||||||
|
else await createResource("tijdsloten", payload);
|
||||||
setForm(leeg);
|
setForm(leeg);
|
||||||
setEditId(null);
|
setEditId(null);
|
||||||
await reload();
|
await reload();
|
||||||
|
|
@ -533,7 +559,7 @@ export function TijdslotenPage() {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1>Tijdsloten</h1>
|
<h1>Tijdsloten</h1>
|
||||||
<p className="muted">Vaste tijdblokken per schooldag (bv. Blok 1, maandag 08:30–10:00).</p>
|
<p className="muted">Vaste tijden per schooldag. Laat de groep leeg voor de algemene tijden, of kies een groep voor afwijkende tijden.</p>
|
||||||
<Foutmelding msg={error} />
|
<Foutmelding msg={error} />
|
||||||
<form className="form-row" onSubmit={opslaan}>
|
<form className="form-row" onSubmit={opslaan}>
|
||||||
<input placeholder="Naam (bv. Blok 1)" value={form.naam} required onChange={(e) => setForm({ ...form, naam: e.target.value })} />
|
<input placeholder="Naam (bv. Blok 1)" value={form.naam} required onChange={(e) => setForm({ ...form, naam: e.target.value })} />
|
||||||
|
|
@ -543,25 +569,30 @@ export function TijdslotenPage() {
|
||||||
<input type="time" value={form.start_tijd} onChange={(e) => setForm({ ...form, start_tijd: e.target.value })} />
|
<input type="time" value={form.start_tijd} onChange={(e) => setForm({ ...form, start_tijd: e.target.value })} />
|
||||||
<input type="time" value={form.eind_tijd} onChange={(e) => setForm({ ...form, eind_tijd: e.target.value })} />
|
<input type="time" value={form.eind_tijd} onChange={(e) => setForm({ ...form, eind_tijd: e.target.value })} />
|
||||||
<input type="number" min="0" style={{ width: 80 }} title="Volgorde" value={form.volgorde} onChange={(e) => setForm({ ...form, volgorde: Number(e.target.value) })} />
|
<input type="number" min="0" style={{ width: 80 }} title="Volgorde" value={form.volgorde} onChange={(e) => setForm({ ...form, volgorde: Number(e.target.value) })} />
|
||||||
|
<select value={form.groep || ""} title="Groep (leeg = algemeen)" onChange={(e) => setForm({ ...form, groep: e.target.value })}>
|
||||||
|
<option value="">algemeen</option>
|
||||||
|
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam}</option>)}
|
||||||
|
</select>
|
||||||
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
|
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
|
||||||
{editId && <button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>}
|
{editId && <button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>}
|
||||||
</form>
|
</form>
|
||||||
{loading ? <p>Laden…</p> : (
|
{loading ? <p>Laden…</p> : (
|
||||||
<table className="grid">
|
<table className="grid">
|
||||||
<thead><tr><th>Dag</th><th>Tijd</th><th>Naam</th><th></th></tr></thead>
|
<thead><tr><th>Dag</th><th>Tijd</th><th>Naam</th><th>Geldt voor</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{items.map((t) => (
|
{items.map((t) => (
|
||||||
<tr key={t.id}>
|
<tr key={t.id}>
|
||||||
<td>{t.dag_label}</td>
|
<td>{t.dag_label}</td>
|
||||||
<td>{t.start_tijd?.slice(0, 5)}–{t.eind_tijd?.slice(0, 5)}</td>
|
<td>{t.start_tijd?.slice(0, 5)}–{t.eind_tijd?.slice(0, 5)}</td>
|
||||||
<td>{t.naam}</td>
|
<td>{t.naam}</td>
|
||||||
|
<td className="muted">{t.groep_naam || "algemeen"}</td>
|
||||||
<td className="row-actions">
|
<td className="row-actions">
|
||||||
<button className="link" onClick={() => { setEditId(t.id); setForm({ naam: t.naam, dag: t.dag, start_tijd: t.start_tijd?.slice(0, 5), eind_tijd: t.eind_tijd?.slice(0, 5), volgorde: t.volgorde }); }}>bewerken</button>
|
<button className="link" onClick={() => { setEditId(t.id); setForm({ naam: t.naam, dag: t.dag, start_tijd: t.start_tijd?.slice(0, 5), eind_tijd: t.eind_tijd?.slice(0, 5), volgorde: t.volgorde, groep: t.groep || "" }); }}>bewerken</button>
|
||||||
<button className="link danger" onClick={() => verwijder(t.id)}>verwijderen</button>
|
<button className="link danger" onClick={() => verwijder(t.id)}>verwijderen</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{items.length === 0 && <tr><td colSpan="4" className="muted">Nog geen tijdsloten.</td></tr>}
|
{items.length === 0 && <tr><td colSpan="5" className="muted">Nog geen tijdsloten.</td></tr>}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
)}
|
)}
|
||||||
|
|
@ -841,6 +872,114 @@ export function LeerpleinPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------- Stage --------------------------------- */
|
||||||
|
// Module-scherm: interne en externe stages. Beide met begeleider; intern + ruimte.
|
||||||
|
export function StagePage() {
|
||||||
|
const { items, error, loading, reload, setError } = useList("stages");
|
||||||
|
const [leerlingen, setLeerlingen] = useState([]);
|
||||||
|
const [medewerkers, setMedewerkers] = useState([]);
|
||||||
|
const [locaties, setLocaties] = useState([]);
|
||||||
|
const leeg = { leerling: "", type: "intern", begeleider: "", locatie: "", organisatie: "", dag: "", omschrijving: "" };
|
||||||
|
const [form, setForm] = useState(leeg);
|
||||||
|
const [editId, setEditId] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
|
||||||
|
listResource("personen").then((ps) => setMedewerkers(ps.filter((p) => p.rol !== "leerling"))).catch(() => {});
|
||||||
|
listResource("locaties").then(setLocaties).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isIntern = form.type === "intern";
|
||||||
|
|
||||||
|
async function opslaan(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const payload = {
|
||||||
|
leerling: form.leerling, type: form.type, begeleider: form.begeleider,
|
||||||
|
locatie: isIntern ? (form.locatie || null) : null,
|
||||||
|
organisatie: isIntern ? "" : form.organisatie,
|
||||||
|
dag: form.dag === "" ? null : Number(form.dag),
|
||||||
|
omschrijving: form.omschrijving,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
if (editId) await updateResource("stages", editId, payload);
|
||||||
|
else await createResource("stages", payload);
|
||||||
|
setForm(leeg);
|
||||||
|
setEditId(null);
|
||||||
|
await reload();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function verwijder(id) {
|
||||||
|
if (!confirm("Deze stage verwijderen?")) return;
|
||||||
|
try { await deleteResource("stages", id); await reload(); } catch (err) { setError(err.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h1>Stage</h1>
|
||||||
|
<p className="muted">Interne en externe stages. Beide krijgen een begeleider; een interne stage ook een ruimte.</p>
|
||||||
|
<Foutmelding msg={error} />
|
||||||
|
<form className="form-card" onSubmit={opslaan}>
|
||||||
|
<div className="form-row">
|
||||||
|
<select value={form.leerling} required onChange={(e) => setForm({ ...form, leerling: e.target.value })}>
|
||||||
|
<option value="">— leerling —</option>
|
||||||
|
{leerlingen.map((l) => <option key={l.id} value={l.id}>{l.volledige_naam}</option>)}
|
||||||
|
</select>
|
||||||
|
<select value={form.type} onChange={(e) => setForm({ ...form, type: e.target.value })}>
|
||||||
|
<option value="intern">Interne stage</option>
|
||||||
|
<option value="extern">Externe stage</option>
|
||||||
|
</select>
|
||||||
|
<select value={form.begeleider} required onChange={(e) => setForm({ ...form, begeleider: e.target.value })}>
|
||||||
|
<option value="">— begeleider —</option>
|
||||||
|
{medewerkers.map((m) => <option key={m.id} value={m.id}>{m.volledige_naam}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
{isIntern ? (
|
||||||
|
<select value={form.locatie} required onChange={(e) => setForm({ ...form, locatie: e.target.value })}>
|
||||||
|
<option value="">— ruimte —</option>
|
||||||
|
{locaties.map((l) => <option key={l.id} value={l.id}>{l.naam}</option>)}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<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 })}>
|
||||||
|
<option value="">— dag (optioneel) —</option>
|
||||||
|
{WEEKDAGEN.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
|
||||||
|
</select>
|
||||||
|
<input placeholder="Opmerking (optioneel)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<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>Leerling</th><th>Type</th><th>Begeleider</th><th>Waar</th><th>Dag</th><th></th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{items.map((s) => (
|
||||||
|
<tr key={s.id}>
|
||||||
|
<td>{s.leerling_naam}</td>
|
||||||
|
<td>{s.type_label}</td>
|
||||||
|
<td>{s.begeleider_naam}</td>
|
||||||
|
<td className="muted">{s.type === "intern" ? (s.locatie_naam || "—") : (s.organisatie || "—")}</td>
|
||||||
|
<td className="muted">{s.dag_label || "—"}</td>
|
||||||
|
<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 danger" onClick={() => verwijder(s.id)}>verwijderen</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{items.length === 0 && <tr><td colSpan="6" className="muted">Nog geen stages.</td></tr>}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------- Conflicten ------------------------------ */
|
/* -------------------------------- Conflicten ------------------------------ */
|
||||||
function Conflicten({ schooljaar }) {
|
function Conflicten({ schooljaar }) {
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue