Koppelingen: personeel aan subgroep/leerplein, subgroep->groep, leerplein-medewerkers; inzetregel begeleider-vak (klassenondersteuner overal, leerkracht ook theorievakken)
This commit is contained in:
parent
e39f65706e
commit
4e461a9471
9 changed files with 211 additions and 46 deletions
19
ROADMAP.md
19
ROADMAP.md
|
|
@ -50,15 +50,16 @@ volgt.
|
||||||
|
|
||||||
## Koppelingen & inzetregels
|
## Koppelingen & inzetregels
|
||||||
|
|
||||||
- [ ] **Personeel koppelen aan leerplein en subgroep** (nu alleen stamgroep
|
- [x] **Personeel koppelen aan leerplein en subgroep** (personeelsformulier:
|
||||||
mogelijk in het personeelsformulier).
|
stamgroep, subgroep-begeleiding én leerpleinen).
|
||||||
- [ ] **Theorievakken automatisch beschikbaar voor leerkrachten** (een leerkracht
|
- [x] **Theorievakken automatisch beschikbaar voor leerkrachten** (een leerkracht
|
||||||
mag altijd op een theorievak ingezet worden).
|
mag altijd op een theorievak ingezet worden; daarnaast worden theorievakken
|
||||||
- [ ] **Inzetregel begeleider ↔ vak:** een leerkracht of praktijkondersteuner kan
|
bij het aanmaken automatisch gekoppeld).
|
||||||
alleen aan een vak/blok gekoppeld worden als hij/zij aan dat vak gekoppeld
|
- [x] **Inzetregel begeleider ↔ vak:** leerkracht/praktijkondersteuner alleen op
|
||||||
is; een **klassenondersteuner kan overal** ingezet worden.
|
gekoppelde vakken (leerkracht bovendien altijd op theorievakken);
|
||||||
- [ ] **Hiërarchie vastleggen:** subgroepen verbonden aan groepen, en groepen aan
|
**klassenondersteuner overal**.
|
||||||
leerpleinen (subgroep → groep → leerplein).
|
- [x] **Hiërarchie vastgelegd:** subgroep → groep (Subgroep.groep) en
|
||||||
|
groep → leerplein (Leerplein.groepen).
|
||||||
|
|
||||||
## Module: Urenoverzicht
|
## Module: Urenoverzicht
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,11 @@ class PersoonAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(Subgroep)
|
@admin.register(Subgroep)
|
||||||
class SubgroepAdmin(admin.ModelAdmin):
|
class SubgroepAdmin(admin.ModelAdmin):
|
||||||
list_display = ("naam", "omschrijving", "actief")
|
list_display = ("naam", "groep", "omschrijving", "actief")
|
||||||
list_filter = ("actief",)
|
list_filter = ("actief", "groep")
|
||||||
search_fields = ("naam",)
|
search_fields = ("naam",)
|
||||||
filter_horizontal = ("leden",)
|
autocomplete_fields = ("groep",)
|
||||||
|
filter_horizontal = ("leden", "begeleiders")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Schooljaar)
|
@admin.register(Schooljaar)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 5.2.15
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0004_activiteit_theorievak_persoon_geboortedatum_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='subgroep',
|
||||||
|
name='begeleiders',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Medewerkers die aan deze subgroep gekoppeld zijn.', related_name='begeleide_subgroepen', to='core.persoon', verbose_name='begeleiders'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='subgroep',
|
||||||
|
name='groep',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='De groep waar deze subgroep onder valt.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subgroepen', to='core.groep', verbose_name='groep'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -38,6 +38,10 @@ class PersoonSerializer(serializers.ModelSerializer):
|
||||||
many=True, queryset=Activiteit.objects.all(), required=False
|
many=True, queryset=Activiteit.objects.all(), required=False
|
||||||
)
|
)
|
||||||
vakken_namen = serializers.SerializerMethodField()
|
vakken_namen = serializers.SerializerMethodField()
|
||||||
|
begeleide_subgroepen = serializers.PrimaryKeyRelatedField(
|
||||||
|
many=True, queryset=Subgroep.objects.all(), required=False
|
||||||
|
)
|
||||||
|
begeleide_subgroepen_namen = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Persoon
|
model = Persoon
|
||||||
|
|
@ -46,6 +50,7 @@ class PersoonSerializer(serializers.ModelSerializer):
|
||||||
"rol", "rol_label", "groep", "groep_naam",
|
"rol", "rol_label", "groep", "groep_naam",
|
||||||
"stamgroep", "stamgroep_naam",
|
"stamgroep", "stamgroep_naam",
|
||||||
"functies", "functies_namen", "vakken", "vakken_namen",
|
"functies", "functies_namen", "vakken", "vakken_namen",
|
||||||
|
"begeleide_subgroepen", "begeleide_subgroepen_namen",
|
||||||
"geboortedatum", "actief", "opmerkingen", "aangemaakt_op",
|
"geboortedatum", "actief", "opmerkingen", "aangemaakt_op",
|
||||||
]
|
]
|
||||||
read_only_fields = ["aangemaakt_op"]
|
read_only_fields = ["aangemaakt_op"]
|
||||||
|
|
@ -56,13 +61,17 @@ class PersoonSerializer(serializers.ModelSerializer):
|
||||||
def get_vakken_namen(self, obj):
|
def get_vakken_namen(self, obj):
|
||||||
return [{"id": a.id, "naam": a.naam} for a in obj.vakken.all()]
|
return [{"id": a.id, "naam": a.naam} for a in obj.vakken.all()]
|
||||||
|
|
||||||
|
def get_begeleide_subgroepen_namen(self, obj):
|
||||||
|
return [{"id": s.id, "naam": s.naam} for s in obj.begeleide_subgroepen.all()]
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
# Functies/vakken horen alleen bij medewerkers; een leerling krijgt er geen.
|
# Functies/vakken/subgroep-begeleiding horen alleen bij medewerkers.
|
||||||
# (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"] = []
|
data["vakken"] = []
|
||||||
|
data["begeleide_subgroepen"] = []
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -73,17 +82,24 @@ class PersoonSerializer(serializers.ModelSerializer):
|
||||||
return gekozen + [a for a in theorie if a.id not in ids]
|
return gekozen + [a for a in theorie if a.id not in ids]
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
# Een leerkracht krijgt de theorievakken standaard direct gekoppeld.
|
# begeleide_subgroepen is een reverse-M2M: apart afhandelen na save.
|
||||||
|
subg = validated_data.pop("begeleide_subgroepen", None)
|
||||||
if validated_data.get("rol") == "leerkracht":
|
if validated_data.get("rol") == "leerkracht":
|
||||||
validated_data["vakken"] = self._met_theorievakken(validated_data.get("vakken", []))
|
validated_data["vakken"] = self._met_theorievakken(validated_data.get("vakken", []))
|
||||||
return super().create(validated_data)
|
instance = super().create(validated_data)
|
||||||
|
if subg is not None:
|
||||||
|
instance.begeleide_subgroepen.set(subg)
|
||||||
|
return instance
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
# Ook bij bewerken houdt een leerkracht altijd de theorievakken.
|
subg = validated_data.pop("begeleide_subgroepen", "__keep__")
|
||||||
rol = validated_data.get("rol", instance.rol)
|
rol = validated_data.get("rol", instance.rol)
|
||||||
if rol == "leerkracht" and "vakken" in validated_data:
|
if rol == "leerkracht" and "vakken" in validated_data:
|
||||||
validated_data["vakken"] = self._met_theorievakken(validated_data["vakken"])
|
validated_data["vakken"] = self._met_theorievakken(validated_data["vakken"])
|
||||||
return super().update(instance, validated_data)
|
instance = super().update(instance, validated_data)
|
||||||
|
if subg != "__keep__":
|
||||||
|
instance.begeleide_subgroepen.set(subg)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
class SubgroepSerializer(serializers.ModelSerializer):
|
class SubgroepSerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -92,12 +108,19 @@ class SubgroepSerializer(serializers.ModelSerializer):
|
||||||
)
|
)
|
||||||
leden_namen = serializers.SerializerMethodField()
|
leden_namen = serializers.SerializerMethodField()
|
||||||
aantal_leden = serializers.SerializerMethodField()
|
aantal_leden = serializers.SerializerMethodField()
|
||||||
|
groep_naam = serializers.CharField(source="groep.naam", read_only=True, default=None)
|
||||||
|
begeleiders = serializers.PrimaryKeyRelatedField(
|
||||||
|
many=True, queryset=Persoon.objects.exclude(rol="leerling"), required=False
|
||||||
|
)
|
||||||
|
begeleiders_namen = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Subgroep
|
model = Subgroep
|
||||||
fields = [
|
fields = [
|
||||||
"id", "naam", "omschrijving", "actief",
|
"id", "naam", "omschrijving", "actief",
|
||||||
"leden", "leden_namen", "aantal_leden", "aangemaakt_op",
|
"groep", "groep_naam",
|
||||||
|
"leden", "leden_namen", "aantal_leden",
|
||||||
|
"begeleiders", "begeleiders_namen", "aangemaakt_op",
|
||||||
]
|
]
|
||||||
read_only_fields = ["aangemaakt_op"]
|
read_only_fields = ["aangemaakt_op"]
|
||||||
|
|
||||||
|
|
@ -107,6 +130,9 @@ class SubgroepSerializer(serializers.ModelSerializer):
|
||||||
def get_aantal_leden(self, obj):
|
def get_aantal_leden(self, obj):
|
||||||
return obj.leden.count()
|
return obj.leden.count()
|
||||||
|
|
||||||
|
def get_begeleiders_namen(self, obj):
|
||||||
|
return [{"id": p.id, "naam": p.volledige_naam} for p in obj.begeleiders.all()]
|
||||||
|
|
||||||
|
|
||||||
# --- Fase 2 ---------------------------------------------------------------
|
# --- Fase 2 ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -207,13 +233,31 @@ class RoosterblokSerializer(serializers.ModelSerializer):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
"Kies precies één doelgroep: groep, subgroep of leerling."
|
"Kies precies één doelgroep: groep, subgroep of leerling."
|
||||||
)
|
)
|
||||||
# Begeleiders zijn medewerkers; een leerling kan geen begeleider zijn.
|
# Inzetregels voor begeleiders t.o.v. het vak (de activiteit):
|
||||||
# (Het Roosterblok-model kent geen rolbeperking, dus hier afdwingen.)
|
# - een leerling kan geen begeleider zijn;
|
||||||
for p in data.get("begeleiders", []):
|
# - een klassenondersteuner kan overal worden ingezet;
|
||||||
|
# - een leerkracht mag op theorievakken én op de aan hem/haar gekoppelde
|
||||||
|
# vakken; een praktijkondersteuner alleen op de gekoppelde vakken.
|
||||||
|
activiteit = data.get("activiteit", getattr(self.instance, "activiteit", None))
|
||||||
|
begeleiders = data.get("begeleiders", None)
|
||||||
|
if begeleiders is None and self.instance is not None:
|
||||||
|
begeleiders = list(self.instance.begeleiders.all())
|
||||||
|
for p in (begeleiders or []):
|
||||||
if p.rol == "leerling":
|
if p.rol == "leerling":
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
f"'{p.volledige_naam}' is een leerling en kan geen begeleider zijn."
|
f"'{p.volledige_naam}' is een leerling en kan geen begeleider zijn."
|
||||||
)
|
)
|
||||||
|
if p.rol == "klassenondersteuner":
|
||||||
|
continue
|
||||||
|
if activiteit is None:
|
||||||
|
continue
|
||||||
|
mag = p.vakken.filter(pk=activiteit.pk).exists()
|
||||||
|
if p.rol == "leerkracht" and getattr(activiteit, "theorievak", False):
|
||||||
|
mag = True
|
||||||
|
if not mag:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"'{p.volledige_naam}' is niet gekoppeld aan het vak '{activiteit.naam}'."
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ class LeerpleinAdmin(admin.ModelAdmin):
|
||||||
list_display = ("naam", "omschrijving", "actief")
|
list_display = ("naam", "omschrijving", "actief")
|
||||||
list_filter = ("actief",)
|
list_filter = ("actief",)
|
||||||
search_fields = ("naam",)
|
search_fields = ("naam",)
|
||||||
filter_horizontal = ("groepen", "subgroepen", "leerlingen")
|
filter_horizontal = ("groepen", "subgroepen", "leerlingen", "medewerkers")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SubgroepGroep)
|
@admin.register(SubgroepGroep)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 5.2.15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0005_subgroep_begeleiders_subgroep_groep'),
|
||||||
|
('module_leerplein', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='leerplein',
|
||||||
|
name='medewerkers',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='leerpleinen_medewerker', to='core.persoon', verbose_name='medewerkers'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -26,6 +26,10 @@ class Leerplein(models.Model):
|
||||||
"core.Persoon", verbose_name="losse leerlingen", related_name="leerpleinen",
|
"core.Persoon", verbose_name="losse leerlingen", related_name="leerpleinen",
|
||||||
blank=True, limit_choices_to={"rol": "leerling"},
|
blank=True, limit_choices_to={"rol": "leerling"},
|
||||||
)
|
)
|
||||||
|
medewerkers = models.ManyToManyField(
|
||||||
|
"core.Persoon", verbose_name="medewerkers", related_name="leerpleinen_medewerker",
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
aangemaakt_op = models.DateTimeField(auto_now_add=True)
|
aangemaakt_op = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,20 @@ class LeerpleinSerializer(serializers.ModelSerializer):
|
||||||
leerlingen = serializers.PrimaryKeyRelatedField(
|
leerlingen = serializers.PrimaryKeyRelatedField(
|
||||||
many=True, queryset=Persoon.objects.filter(rol="leerling"), required=False
|
many=True, queryset=Persoon.objects.filter(rol="leerling"), required=False
|
||||||
)
|
)
|
||||||
|
medewerkers = serializers.PrimaryKeyRelatedField(
|
||||||
|
many=True, queryset=Persoon.objects.exclude(rol="leerling"), required=False
|
||||||
|
)
|
||||||
groepen_namen = serializers.SerializerMethodField()
|
groepen_namen = serializers.SerializerMethodField()
|
||||||
subgroepen_namen = serializers.SerializerMethodField()
|
subgroepen_namen = serializers.SerializerMethodField()
|
||||||
leerlingen_namen = serializers.SerializerMethodField()
|
leerlingen_namen = serializers.SerializerMethodField()
|
||||||
|
medewerkers_namen = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Leerplein
|
model = Leerplein
|
||||||
fields = [
|
fields = [
|
||||||
"id", "naam", "omschrijving", "actief",
|
"id", "naam", "omschrijving", "actief",
|
||||||
"groepen", "subgroepen", "leerlingen",
|
"groepen", "subgroepen", "leerlingen", "medewerkers",
|
||||||
"groepen_namen", "subgroepen_namen", "leerlingen_namen",
|
"groepen_namen", "subgroepen_namen", "leerlingen_namen", "medewerkers_namen",
|
||||||
"aangemaakt_op",
|
"aangemaakt_op",
|
||||||
]
|
]
|
||||||
read_only_fields = ["aangemaakt_op"]
|
read_only_fields = ["aangemaakt_op"]
|
||||||
|
|
@ -38,6 +42,9 @@ class LeerpleinSerializer(serializers.ModelSerializer):
|
||||||
def get_leerlingen_namen(self, obj):
|
def get_leerlingen_namen(self, obj):
|
||||||
return [{"id": p.id, "naam": p.volledige_naam} for p in obj.leerlingen.all()]
|
return [{"id": p.id, "naam": p.volledige_naam} for p in obj.leerlingen.all()]
|
||||||
|
|
||||||
|
def get_medewerkers_namen(self, obj):
|
||||||
|
return [{"id": p.id, "naam": p.volledige_naam} for p in obj.medewerkers.all()]
|
||||||
|
|
||||||
|
|
||||||
class SubgroepGroepSerializer(serializers.ModelSerializer):
|
class SubgroepGroepSerializer(serializers.ModelSerializer):
|
||||||
groep_naam = serializers.CharField(source="groep.naam", read_only=True)
|
groep_naam = serializers.CharField(source="groep.naam", read_only=True)
|
||||||
|
|
|
||||||
|
|
@ -262,19 +262,36 @@ export function PersonenPage() {
|
||||||
const [groepen, setGroepen] = useState([]);
|
const [groepen, setGroepen] = useState([]);
|
||||||
const [functies, setFuncties] = useState([]);
|
const [functies, setFuncties] = useState([]);
|
||||||
const [vakken, setVakken] = useState([]);
|
const [vakken, setVakken] = useState([]);
|
||||||
|
const [subgroepen, setSubgroepen] = useState([]);
|
||||||
|
const [leerpleinen, setLeerpleinen] = useState([]);
|
||||||
// Twee losse flows: medewerker en leerling.
|
// Twee losse flows: medewerker en leerling.
|
||||||
const [modus, setModus] = useState("leerling");
|
const [modus, setModus] = useState("leerling");
|
||||||
const leegMedewerker = { voornaam: "", achternaam: "", rol: "leerkracht", stamgroep: "", functies: [], vakken: [] };
|
const leegMedewerker = { voornaam: "", achternaam: "", rol: "leerkracht", stamgroep: "", functies: [], vakken: [], begeleide_subgroepen: [], leerpleinen: [] };
|
||||||
const leegLeerling = { voornaam: "", achternaam: "", groep: "", geboortedatum: "" };
|
const leegLeerling = { voornaam: "", achternaam: "", groep: "", geboortedatum: "" };
|
||||||
const [form, setForm] = useState(leegLeerling);
|
const [form, setForm] = useState(leegLeerling);
|
||||||
const [editId, setEditId] = useState(null);
|
const [editId, setEditId] = useState(null);
|
||||||
const [filter, setFilter] = useState("");
|
const [filter, setFilter] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
function laadHulplijsten() {
|
||||||
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(() => {});
|
listResource("activiteiten").then(setVakken).catch(() => {});
|
||||||
}, []);
|
listResource("subgroepen").then(setSubgroepen).catch(() => {});
|
||||||
|
listResource("leerpleinen").then(setLeerpleinen).catch(() => setLeerpleinen([]));
|
||||||
|
}
|
||||||
|
useEffect(() => { laadHulplijsten(); }, []);
|
||||||
|
|
||||||
|
// Leerplein-koppeling loopt via de leerplein-API (de module bezit die relatie).
|
||||||
|
async function syncLeerpleinen(persoonId, gekozenIds) {
|
||||||
|
const huidige = await listResource("leerpleinen").catch(() => []);
|
||||||
|
for (const lp of huidige) {
|
||||||
|
const inLijst = (lp.medewerkers || []).includes(persoonId);
|
||||||
|
const moet = gekozenIds.includes(lp.id);
|
||||||
|
if (moet && !inLijst) await updateResource("leerpleinen", lp.id, { medewerkers: [...lp.medewerkers, persoonId] });
|
||||||
|
else if (!moet && inLijst) await updateResource("leerpleinen", lp.id, { medewerkers: lp.medewerkers.filter((x) => x !== persoonId) });
|
||||||
|
}
|
||||||
|
listResource("leerpleinen").then(setLeerpleinen).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
function kiesModus(m) {
|
function kiesModus(m) {
|
||||||
setModus(m);
|
setModus(m);
|
||||||
|
|
@ -291,14 +308,19 @@ export function PersonenPage() {
|
||||||
? {
|
? {
|
||||||
voornaam: form.voornaam, achternaam: form.achternaam, rol: form.rol,
|
voornaam: form.voornaam, achternaam: form.achternaam, rol: form.rol,
|
||||||
stamgroep: form.stamgroep || null, functies: form.functies, vakken: form.vakken,
|
stamgroep: form.stamgroep || null, functies: form.functies, vakken: form.vakken,
|
||||||
|
begeleide_subgroepen: form.begeleide_subgroepen,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
voornaam: form.voornaam, achternaam: form.achternaam, rol: "leerling",
|
voornaam: form.voornaam, achternaam: form.achternaam, rol: "leerling",
|
||||||
groep: form.groep || null, geboortedatum: form.geboortedatum || null,
|
groep: form.groep || null, geboortedatum: form.geboortedatum || null,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
if (editId) await updateResource("personen", editId, payload);
|
const opgeslagen = editId
|
||||||
else await createResource("personen", payload);
|
? await updateResource("personen", editId, payload)
|
||||||
|
: await createResource("personen", payload);
|
||||||
|
if (modus === "medewerker" && opgeslagen && opgeslagen.id) {
|
||||||
|
await syncLeerpleinen(opgeslagen.id, form.leerpleinen || []);
|
||||||
|
}
|
||||||
setForm(modus === "medewerker" ? leegMedewerker : leegLeerling);
|
setForm(modus === "medewerker" ? leegMedewerker : leegLeerling);
|
||||||
setEditId(null);
|
setEditId(null);
|
||||||
await reload();
|
await reload();
|
||||||
|
|
@ -322,6 +344,8 @@ export function PersonenPage() {
|
||||||
stamgroep: p.stamgroep || "",
|
stamgroep: p.stamgroep || "",
|
||||||
functies: p.functies_namen.map((x) => x.id),
|
functies: p.functies_namen.map((x) => x.id),
|
||||||
vakken: p.vakken_namen.map((x) => x.id),
|
vakken: p.vakken_namen.map((x) => x.id),
|
||||||
|
begeleide_subgroepen: (p.begeleide_subgroepen_namen || []).map((x) => x.id),
|
||||||
|
leerpleinen: leerpleinen.filter((lp) => (lp.medewerkers || []).includes(p.id)).map((lp) => lp.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -382,6 +406,21 @@ export function PersonenPage() {
|
||||||
{vakken.length === 0 && <span className="muted small">Nog geen vakken/activiteiten.</span>}
|
{vakken.length === 0 && <span className="muted small">Nog geen vakken/activiteiten.</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="leden-keuze">
|
||||||
|
<div className="muted small">Subgroepen (begeleiding):</div>
|
||||||
|
<div className="chips">
|
||||||
|
{subgroepen.map((s) => <button type="button" key={s.id} className={form.begeleide_subgroepen.includes(s.id) ? "chip on" : "chip"} onClick={() => toggle("begeleide_subgroepen", s.id)}>{s.naam}</button>)}
|
||||||
|
{subgroepen.length === 0 && <span className="muted small">Nog geen subgroepen.</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{leerpleinen.length > 0 && (
|
||||||
|
<div className="leden-keuze">
|
||||||
|
<div className="muted small">Leerpleinen:</div>
|
||||||
|
<div className="chips">
|
||||||
|
{leerpleinen.map((lp) => <button type="button" key={lp.id} className={form.leerpleinen.includes(lp.id) ? "chip on" : "chip"} onClick={() => toggle("leerpleinen", lp.id)}>{lp.naam}</button>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -440,27 +479,28 @@ export function PersonenPage() {
|
||||||
export function SubgroepenPage() {
|
export function SubgroepenPage() {
|
||||||
const { items, error, loading, reload, setError } = useList("subgroepen");
|
const { items, error, loading, reload, setError } = useList("subgroepen");
|
||||||
const [leerlingen, setLeerlingen] = useState([]);
|
const [leerlingen, setLeerlingen] = useState([]);
|
||||||
const leeg = { naam: "", omschrijving: "", leden: [] };
|
const [groepen, setGroepen] = useState([]);
|
||||||
|
const [medewerkers, setMedewerkers] = useState([]);
|
||||||
|
const leeg = { naam: "", omschrijving: "", groep: "", leden: [], begeleiders: [] };
|
||||||
const [form, setForm] = useState(leeg);
|
const [form, setForm] = useState(leeg);
|
||||||
const [editId, setEditId] = useState(null);
|
const [editId, setEditId] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
|
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
|
||||||
|
listResource("groepen").then(setGroepen).catch(() => {});
|
||||||
|
listResource("personen").then((ps) => setMedewerkers(ps.filter((p) => p.rol !== "leerling"))).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function toggleLid(id) {
|
function toggle(veld, id) {
|
||||||
setForm((f) =>
|
setForm((f) => f[veld].includes(id) ? { ...f, [veld]: f[veld].filter((x) => x !== id) } : { ...f, [veld]: [...f[veld], id] });
|
||||||
f.leden.includes(id)
|
|
||||||
? { ...f, leden: f.leden.filter((x) => x !== id) }
|
|
||||||
: { ...f, leden: [...f.leden, id] }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function opslaan(e) {
|
async function opslaan(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const payload = { ...form, groep: form.groep || null };
|
||||||
try {
|
try {
|
||||||
if (editId) await updateResource("subgroepen", editId, form);
|
if (editId) await updateResource("subgroepen", editId, payload);
|
||||||
else await createResource("subgroepen", form);
|
else await createResource("subgroepen", payload);
|
||||||
setForm(leeg);
|
setForm(leeg);
|
||||||
setEditId(null);
|
setEditId(null);
|
||||||
await reload();
|
await reload();
|
||||||
|
|
@ -481,22 +521,35 @@ export function SubgroepenPage() {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1>Subgroepen</h1>
|
<h1>Subgroepen</h1>
|
||||||
<p className="muted">Vrije, groep-overstijgende groepjes leerlingen (bv. niveaugroep rekenen).</p>
|
<p className="muted">Groepjes leerlingen binnen een groep (bv. niveaugroep rekenen), met eigen begeleiders.</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">
|
||||||
<input placeholder="Naam" value={form.naam} required onChange={(e) => setForm({ ...form, naam: e.target.value })} />
|
<input placeholder="Naam" value={form.naam} required onChange={(e) => setForm({ ...form, naam: e.target.value })} />
|
||||||
<input placeholder="Omschrijving (optioneel)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} />
|
<input placeholder="Omschrijving (optioneel)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} />
|
||||||
|
<select value={form.groep || ""} onChange={(e) => setForm({ ...form, groep: e.target.value })}>
|
||||||
|
<option value="">— groep —</option>
|
||||||
|
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam}</option>)}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="leden-keuze">
|
<div className="leden-keuze">
|
||||||
<div className="muted small">Leden (leerlingen):</div>
|
<div className="muted small">Leden (leerlingen):</div>
|
||||||
<div className="chips">
|
<div className="chips">
|
||||||
{leerlingen.map((l) => (
|
{leerlingen.map((l) => (
|
||||||
<button type="button" key={l.id} className={form.leden.includes(l.id) ? "chip on" : "chip"} onClick={() => toggleLid(l.id)}>{l.volledige_naam}</button>
|
<button type="button" key={l.id} className={form.leden.includes(l.id) ? "chip on" : "chip"} onClick={() => toggle("leden", l.id)}>{l.volledige_naam}</button>
|
||||||
))}
|
))}
|
||||||
{leerlingen.length === 0 && <span className="muted small">Nog geen leerlingen ingevoerd.</span>}
|
{leerlingen.length === 0 && <span className="muted small">Nog geen leerlingen ingevoerd.</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="leden-keuze">
|
||||||
|
<div className="muted small">Begeleiders:</div>
|
||||||
|
<div className="chips">
|
||||||
|
{medewerkers.map((m) => (
|
||||||
|
<button type="button" key={m.id} className={form.begeleiders.includes(m.id) ? "chip on" : "chip"} onClick={() => toggle("begeleiders", m.id)}>{m.volledige_naam}</button>
|
||||||
|
))}
|
||||||
|
{medewerkers.length === 0 && <span className="muted small">Nog geen medewerkers.</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={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>}
|
||||||
|
|
@ -504,19 +557,21 @@ export function SubgroepenPage() {
|
||||||
</form>
|
</form>
|
||||||
{loading ? <p>Laden…</p> : (
|
{loading ? <p>Laden…</p> : (
|
||||||
<table className="grid">
|
<table className="grid">
|
||||||
<thead><tr><th>Naam</th><th>Leden</th><th></th></tr></thead>
|
<thead><tr><th>Naam</th><th>Groep</th><th>Leden</th><th>Begeleiders</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{items.map((s) => (
|
{items.map((s) => (
|
||||||
<tr key={s.id}>
|
<tr key={s.id}>
|
||||||
<td>{s.naam}{s.omschrijving && <div className="muted small">{s.omschrijving}</div>}</td>
|
<td>{s.naam}{s.omschrijving && <div className="muted small">{s.omschrijving}</div>}</td>
|
||||||
|
<td className="muted">{s.groep_naam || "—"}</td>
|
||||||
<td><span className="badge">{s.aantal_leden}</span> <span className="muted small">{s.leden_namen.map((x) => x.naam).join(", ")}</span></td>
|
<td><span className="badge">{s.aantal_leden}</span> <span className="muted small">{s.leden_namen.map((x) => x.naam).join(", ")}</span></td>
|
||||||
|
<td className="muted small">{s.begeleiders_namen.map((x) => x.naam).join(", ") || "—"}</td>
|
||||||
<td className="row-actions">
|
<td className="row-actions">
|
||||||
<button className="link" onClick={() => { setEditId(s.id); setForm({ naam: s.naam, omschrijving: s.omschrijving, leden: s.leden_namen.map((x) => x.id) }); }}>bewerken</button>
|
<button className="link" onClick={() => { setEditId(s.id); setForm({ naam: s.naam, omschrijving: s.omschrijving, groep: s.groep || "", leden: s.leden_namen.map((x) => x.id), begeleiders: s.begeleiders_namen.map((x) => x.id) }); }}>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>
|
||||||
))}
|
))}
|
||||||
{items.length === 0 && <tr><td colSpan="3" className="muted">Nog geen subgroepen.</td></tr>}
|
{items.length === 0 && <tr><td colSpan="5" className="muted">Nog geen subgroepen.</td></tr>}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
)}
|
)}
|
||||||
|
|
@ -784,7 +839,8 @@ export function LeerpleinPage() {
|
||||||
const [groepen, setGroepen] = useState([]);
|
const [groepen, setGroepen] = useState([]);
|
||||||
const [subgroepen, setSubgroepen] = useState([]);
|
const [subgroepen, setSubgroepen] = useState([]);
|
||||||
const [leerlingen, setLeerlingen] = useState([]);
|
const [leerlingen, setLeerlingen] = useState([]);
|
||||||
const leeg = { naam: "", omschrijving: "", groepen: [], subgroepen: [], leerlingen: [] };
|
const [medewerkers, setMedewerkers] = useState([]);
|
||||||
|
const leeg = { naam: "", omschrijving: "", groepen: [], subgroepen: [], leerlingen: [], medewerkers: [] };
|
||||||
const [form, setForm] = useState(leeg);
|
const [form, setForm] = useState(leeg);
|
||||||
const [editId, setEditId] = useState(null);
|
const [editId, setEditId] = useState(null);
|
||||||
|
|
||||||
|
|
@ -792,6 +848,7 @@ export function LeerpleinPage() {
|
||||||
listResource("groepen").then(setGroepen).catch(() => {});
|
listResource("groepen").then(setGroepen).catch(() => {});
|
||||||
listResource("subgroepen").then(setSubgroepen).catch(() => {});
|
listResource("subgroepen").then(setSubgroepen).catch(() => {});
|
||||||
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
|
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
|
||||||
|
listResource("personen").then((ps) => setMedewerkers(ps.filter((p) => p.rol !== "leerling"))).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function toggle(veld, id) {
|
function toggle(veld, id) {
|
||||||
|
|
@ -841,6 +898,13 @@ export function LeerpleinPage() {
|
||||||
{leerlingen.length === 0 && <span className="muted small">Geen leerlingen.</span>}
|
{leerlingen.length === 0 && <span className="muted small">Geen leerlingen.</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="leden-keuze">
|
||||||
|
<div className="muted small">Medewerkers:</div>
|
||||||
|
<div className="chips">
|
||||||
|
{medewerkers.map((m) => <button type="button" key={m.id} className={form.medewerkers.includes(m.id) ? "chip on" : "chip"} onClick={() => toggle("medewerkers", m.id)}>{m.volledige_naam}</button>)}
|
||||||
|
{medewerkers.length === 0 && <span className="muted small">Geen medewerkers.</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={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>}
|
||||||
|
|
@ -857,10 +921,11 @@ export function LeerpleinPage() {
|
||||||
{lp.groepen_namen.length > 0 && <div>🗂️ {lp.groepen_namen.map((x) => x.naam).join(", ")}</div>}
|
{lp.groepen_namen.length > 0 && <div>🗂️ {lp.groepen_namen.map((x) => x.naam).join(", ")}</div>}
|
||||||
{lp.subgroepen_namen.length > 0 && <div>🔹 {lp.subgroepen_namen.map((x) => x.naam).join(", ")}</div>}
|
{lp.subgroepen_namen.length > 0 && <div>🔹 {lp.subgroepen_namen.map((x) => x.naam).join(", ")}</div>}
|
||||||
{lp.leerlingen_namen.length > 0 && <div>👤 {lp.leerlingen_namen.map((x) => x.naam).join(", ")}</div>}
|
{lp.leerlingen_namen.length > 0 && <div>👤 {lp.leerlingen_namen.map((x) => x.naam).join(", ")}</div>}
|
||||||
{lp.groepen_namen.length + lp.subgroepen_namen.length + lp.leerlingen_namen.length === 0 && <span className="muted">leeg</span>}
|
{(lp.medewerkers_namen || []).length > 0 && <div>🧑🏫 {lp.medewerkers_namen.map((x) => x.naam).join(", ")}</div>}
|
||||||
|
{lp.groepen_namen.length + lp.subgroepen_namen.length + lp.leerlingen_namen.length + (lp.medewerkers_namen || []).length === 0 && <span className="muted">leeg</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className="row-actions">
|
<td className="row-actions">
|
||||||
<button className="link" onClick={() => { setEditId(lp.id); setForm({ naam: lp.naam, omschrijving: lp.omschrijving, groepen: lp.groepen_namen.map((x) => x.id), subgroepen: lp.subgroepen_namen.map((x) => x.id), leerlingen: lp.leerlingen_namen.map((x) => x.id) }); }}>bewerken</button>
|
<button className="link" onClick={() => { setEditId(lp.id); setForm({ naam: lp.naam, omschrijving: lp.omschrijving, groepen: lp.groepen_namen.map((x) => x.id), subgroepen: lp.subgroepen_namen.map((x) => x.id), leerlingen: lp.leerlingen_namen.map((x) => x.id), medewerkers: (lp.medewerkers_namen || []).map((x) => x.id) }); }}>bewerken</button>
|
||||||
<button className="link danger" onClick={() => verwijder(lp.id)}>verwijderen</button>
|
<button className="link danger" onClick={() => verwijder(lp.id)}>verwijderen</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue