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
|
||||
|
||||
- [ ] **Personeel koppelen aan leerplein en subgroep** (nu alleen stamgroep
|
||||
mogelijk in het personeelsformulier).
|
||||
- [ ] **Theorievakken automatisch beschikbaar voor leerkrachten** (een leerkracht
|
||||
mag altijd op een theorievak ingezet worden).
|
||||
- [ ] **Inzetregel begeleider ↔ vak:** een leerkracht of praktijkondersteuner kan
|
||||
alleen aan een vak/blok gekoppeld worden als hij/zij aan dat vak gekoppeld
|
||||
is; een **klassenondersteuner kan overal** ingezet worden.
|
||||
- [ ] **Hiërarchie vastleggen:** subgroepen verbonden aan groepen, en groepen aan
|
||||
leerpleinen (subgroep → groep → leerplein).
|
||||
- [x] **Personeel koppelen aan leerplein en subgroep** (personeelsformulier:
|
||||
stamgroep, subgroep-begeleiding én leerpleinen).
|
||||
- [x] **Theorievakken automatisch beschikbaar voor leerkrachten** (een leerkracht
|
||||
mag altijd op een theorievak ingezet worden; daarnaast worden theorievakken
|
||||
bij het aanmaken automatisch gekoppeld).
|
||||
- [x] **Inzetregel begeleider ↔ vak:** leerkracht/praktijkondersteuner alleen op
|
||||
gekoppelde vakken (leerkracht bovendien altijd op theorievakken);
|
||||
**klassenondersteuner overal**.
|
||||
- [x] **Hiërarchie vastgelegd:** subgroep → groep (Subgroep.groep) en
|
||||
groep → leerplein (Leerplein.groepen).
|
||||
|
||||
## Module: Urenoverzicht
|
||||
|
||||
|
|
|
|||
|
|
@ -31,10 +31,11 @@ class PersoonAdmin(admin.ModelAdmin):
|
|||
|
||||
@admin.register(Subgroep)
|
||||
class SubgroepAdmin(admin.ModelAdmin):
|
||||
list_display = ("naam", "omschrijving", "actief")
|
||||
list_filter = ("actief",)
|
||||
list_display = ("naam", "groep", "omschrijving", "actief")
|
||||
list_filter = ("actief", "groep")
|
||||
search_fields = ("naam",)
|
||||
filter_horizontal = ("leden",)
|
||||
autocomplete_fields = ("groep",)
|
||||
filter_horizontal = ("leden", "begeleiders")
|
||||
|
||||
|
||||
@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
|
||||
)
|
||||
vakken_namen = serializers.SerializerMethodField()
|
||||
begeleide_subgroepen = serializers.PrimaryKeyRelatedField(
|
||||
many=True, queryset=Subgroep.objects.all(), required=False
|
||||
)
|
||||
begeleide_subgroepen_namen = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Persoon
|
||||
|
|
@ -46,6 +50,7 @@ class PersoonSerializer(serializers.ModelSerializer):
|
|||
"rol", "rol_label", "groep", "groep_naam",
|
||||
"stamgroep", "stamgroep_naam",
|
||||
"functies", "functies_namen", "vakken", "vakken_namen",
|
||||
"begeleide_subgroepen", "begeleide_subgroepen_namen",
|
||||
"geboortedatum", "actief", "opmerkingen", "aangemaakt_op",
|
||||
]
|
||||
read_only_fields = ["aangemaakt_op"]
|
||||
|
|
@ -56,13 +61,17 @@ class PersoonSerializer(serializers.ModelSerializer):
|
|||
def get_vakken_namen(self, obj):
|
||||
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):
|
||||
# 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.)
|
||||
rol = data.get("rol", getattr(self.instance, "rol", None))
|
||||
if rol == "leerling":
|
||||
data["functies"] = []
|
||||
data["vakken"] = []
|
||||
data["begeleide_subgroepen"] = []
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -73,17 +82,24 @@ class PersoonSerializer(serializers.ModelSerializer):
|
|||
return gekozen + [a for a in theorie if a.id not in ids]
|
||||
|
||||
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":
|
||||
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):
|
||||
# Ook bij bewerken houdt een leerkracht altijd de theorievakken.
|
||||
subg = validated_data.pop("begeleide_subgroepen", "__keep__")
|
||||
rol = validated_data.get("rol", instance.rol)
|
||||
if rol == "leerkracht" and "vakken" in validated_data:
|
||||
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):
|
||||
|
|
@ -92,12 +108,19 @@ class SubgroepSerializer(serializers.ModelSerializer):
|
|||
)
|
||||
leden_namen = 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:
|
||||
model = Subgroep
|
||||
fields = [
|
||||
"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"]
|
||||
|
||||
|
|
@ -107,6 +130,9 @@ class SubgroepSerializer(serializers.ModelSerializer):
|
|||
def get_aantal_leden(self, obj):
|
||||
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 ---------------------------------------------------------------
|
||||
|
||||
|
|
@ -207,13 +233,31 @@ class RoosterblokSerializer(serializers.ModelSerializer):
|
|||
raise serializers.ValidationError(
|
||||
"Kies precies één doelgroep: groep, subgroep of leerling."
|
||||
)
|
||||
# Begeleiders zijn medewerkers; een leerling kan geen begeleider zijn.
|
||||
# (Het Roosterblok-model kent geen rolbeperking, dus hier afdwingen.)
|
||||
for p in data.get("begeleiders", []):
|
||||
# Inzetregels voor begeleiders t.o.v. het vak (de activiteit):
|
||||
# - een leerling kan geen begeleider zijn;
|
||||
# - 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":
|
||||
raise serializers.ValidationError(
|
||||
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ class LeerpleinAdmin(admin.ModelAdmin):
|
|||
list_display = ("naam", "omschrijving", "actief")
|
||||
list_filter = ("actief",)
|
||||
search_fields = ("naam",)
|
||||
filter_horizontal = ("groepen", "subgroepen", "leerlingen")
|
||||
filter_horizontal = ("groepen", "subgroepen", "leerlingen", "medewerkers")
|
||||
|
||||
|
||||
@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",
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
|
|
|
|||
|
|
@ -15,16 +15,20 @@ class LeerpleinSerializer(serializers.ModelSerializer):
|
|||
leerlingen = serializers.PrimaryKeyRelatedField(
|
||||
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()
|
||||
subgroepen_namen = serializers.SerializerMethodField()
|
||||
leerlingen_namen = serializers.SerializerMethodField()
|
||||
medewerkers_namen = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Leerplein
|
||||
fields = [
|
||||
"id", "naam", "omschrijving", "actief",
|
||||
"groepen", "subgroepen", "leerlingen",
|
||||
"groepen_namen", "subgroepen_namen", "leerlingen_namen",
|
||||
"groepen", "subgroepen", "leerlingen", "medewerkers",
|
||||
"groepen_namen", "subgroepen_namen", "leerlingen_namen", "medewerkers_namen",
|
||||
"aangemaakt_op",
|
||||
]
|
||||
read_only_fields = ["aangemaakt_op"]
|
||||
|
|
@ -38,6 +42,9 @@ class LeerpleinSerializer(serializers.ModelSerializer):
|
|||
def get_leerlingen_namen(self, obj):
|
||||
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):
|
||||
groep_naam = serializers.CharField(source="groep.naam", read_only=True)
|
||||
|
|
|
|||
|
|
@ -262,19 +262,36 @@ export function PersonenPage() {
|
|||
const [groepen, setGroepen] = useState([]);
|
||||
const [functies, setFuncties] = useState([]);
|
||||
const [vakken, setVakken] = useState([]);
|
||||
const [subgroepen, setSubgroepen] = useState([]);
|
||||
const [leerpleinen, setLeerpleinen] = useState([]);
|
||||
// Twee losse flows: medewerker en 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 [form, setForm] = useState(leegLeerling);
|
||||
const [editId, setEditId] = useState(null);
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
function laadHulplijsten() {
|
||||
listResource("groepen").then(setGroepen).catch(() => {});
|
||||
listResource("functies").then(setFuncties).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) {
|
||||
setModus(m);
|
||||
|
|
@ -291,14 +308,19 @@ export function PersonenPage() {
|
|||
? {
|
||||
voornaam: form.voornaam, achternaam: form.achternaam, rol: form.rol,
|
||||
stamgroep: form.stamgroep || null, functies: form.functies, vakken: form.vakken,
|
||||
begeleide_subgroepen: form.begeleide_subgroepen,
|
||||
}
|
||||
: {
|
||||
voornaam: form.voornaam, achternaam: form.achternaam, rol: "leerling",
|
||||
groep: form.groep || null, geboortedatum: form.geboortedatum || null,
|
||||
};
|
||||
try {
|
||||
if (editId) await updateResource("personen", editId, payload);
|
||||
else await createResource("personen", payload);
|
||||
const opgeslagen = editId
|
||||
? 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);
|
||||
setEditId(null);
|
||||
await reload();
|
||||
|
|
@ -322,6 +344,8 @@ export function PersonenPage() {
|
|||
stamgroep: p.stamgroep || "",
|
||||
functies: p.functies_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>}
|
||||
</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() {
|
||||
const { items, error, loading, reload, setError } = useList("subgroepen");
|
||||
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 [editId, setEditId] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
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) {
|
||||
setForm((f) =>
|
||||
f.leden.includes(id)
|
||||
? { ...f, leden: f.leden.filter((x) => x !== id) }
|
||||
: { ...f, leden: [...f.leden, id] }
|
||||
);
|
||||
function toggle(veld, id) {
|
||||
setForm((f) => f[veld].includes(id) ? { ...f, [veld]: f[veld].filter((x) => x !== id) } : { ...f, [veld]: [...f[veld], id] });
|
||||
}
|
||||
|
||||
async function opslaan(e) {
|
||||
e.preventDefault();
|
||||
const payload = { ...form, groep: form.groep || null };
|
||||
try {
|
||||
if (editId) await updateResource("subgroepen", editId, form);
|
||||
else await createResource("subgroepen", form);
|
||||
if (editId) await updateResource("subgroepen", editId, payload);
|
||||
else await createResource("subgroepen", payload);
|
||||
setForm(leeg);
|
||||
setEditId(null);
|
||||
await reload();
|
||||
|
|
@ -481,22 +521,35 @@ export function SubgroepenPage() {
|
|||
return (
|
||||
<section>
|
||||
<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} />
|
||||
<form className="form-card" onSubmit={opslaan}>
|
||||
<div className="form-row">
|
||||
<input placeholder="Naam" value={form.naam} required onChange={(e) => setForm({ ...form, naam: e.target.value })} />
|
||||
<input placeholder="Omschrijving (optioneel)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} />
|
||||
<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 className="leden-keuze">
|
||||
<div className="muted small">Leden (leerlingen):</div>
|
||||
<div className="chips">
|
||||
{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>}
|
||||
</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">
|
||||
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
|
||||
{editId && <button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>}
|
||||
|
|
@ -504,19 +557,21 @@ export function SubgroepenPage() {
|
|||
</form>
|
||||
{loading ? <p>Laden…</p> : (
|
||||
<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>
|
||||
{items.map((s) => (
|
||||
<tr key={s.id}>
|
||||
<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 className="muted small">{s.begeleiders_namen.map((x) => x.naam).join(", ") || "—"}</td>
|
||||
<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>
|
||||
</td>
|
||||
</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>
|
||||
</table>
|
||||
)}
|
||||
|
|
@ -784,7 +839,8 @@ export function LeerpleinPage() {
|
|||
const [groepen, setGroepen] = useState([]);
|
||||
const [subgroepen, setSubgroepen] = 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 [editId, setEditId] = useState(null);
|
||||
|
||||
|
|
@ -792,6 +848,7 @@ export function LeerpleinPage() {
|
|||
listResource("groepen").then(setGroepen).catch(() => {});
|
||||
listResource("subgroepen").then(setSubgroepen).catch(() => {});
|
||||
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
|
||||
listResource("personen").then((ps) => setMedewerkers(ps.filter((p) => p.rol !== "leerling"))).catch(() => {});
|
||||
}, []);
|
||||
|
||||
function toggle(veld, id) {
|
||||
|
|
@ -841,6 +898,13 @@ export function LeerpleinPage() {
|
|||
{leerlingen.length === 0 && <span className="muted small">Geen leerlingen.</span>}
|
||||
</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">
|
||||
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</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.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.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 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>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
Loading…
Reference in a new issue