auto-rooster voor meerdere klassen + vak voor meerdere klassen (gedeelde begeleider/lokaal)
This commit is contained in:
parent
54a9dd5dc2
commit
170a036c4e
4 changed files with 173 additions and 117 deletions
|
|
@ -135,6 +135,9 @@ volgt.
|
||||||
- [x] **Auto-rooster slimmer**: voorkeuren — vak liever ochtend/middag (zachte
|
- [x] **Auto-rooster slimmer**: voorkeuren — vak liever ochtend/middag (zachte
|
||||||
voorkeur met fallback), bepaalde dagen vrijhouden, en max. blokken per dag
|
voorkeur met fallback), bepaalde dagen vrijhouden, en max. blokken per dag
|
||||||
per groep.
|
per groep.
|
||||||
|
- [x] **Auto-rooster voor meerdere klassen tegelijk**: kies meerdere klassen; een
|
||||||
|
vak (bv. gym/atelier) kan voor meerdere klassen gelden, met gedeelde
|
||||||
|
bewaking van begeleider en lokaal over alle klassen heen.
|
||||||
- [ ] **Algemene visuele opfris** (UX/uiterlijk).
|
- [ ] **Algemene visuele opfris** (UX/uiterlijk).
|
||||||
|
|
||||||
- [ ] **Module: automatisch rooster genereren.** De gebruiker ge
|
- [ ] **Module: automatisch rooster genereren.** De gebruiker ge
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"""Automatisch rooster: stelt een conflictvrij voorstel voor (greedy).
|
"""Automatisch rooster: stelt een conflictvrij voorstel voor (greedy).
|
||||||
|
|
||||||
Houdt rekening met voorkeuren: een vak liever in de ochtend of middag, bepaalde
|
Werkt voor meerdere klassen tegelijk. Een wens (vak) kan voor meerdere klassen
|
||||||
weekdagen vrijhouden, en een maximum aantal blokken per dag per groep.
|
gelden; begeleiders en lokalen worden over álle klassen heen niet dubbel geboekt.
|
||||||
|
Voorkeuren: vak liever ochtend/middag, dagen vrijhouden, max. blokken per dag.
|
||||||
"""
|
"""
|
||||||
from django.db.models import Q
|
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
|
@ -17,7 +17,14 @@ def _is_ochtend(slot):
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
def voorstel(request):
|
def voorstel(request):
|
||||||
sjid = request.data.get("schooljaar")
|
sjid = request.data.get("schooljaar")
|
||||||
groep_id = request.data.get("groep")
|
# Meerdere klassen; val terug op het oude enkele 'groep'-veld.
|
||||||
|
groepen_ids = request.data.get("groepen")
|
||||||
|
if not groepen_ids and request.data.get("groep"):
|
||||||
|
groepen_ids = [request.data.get("groep")]
|
||||||
|
try:
|
||||||
|
groepen_ids = [int(g) for g in (groepen_ids or [])]
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
groepen_ids = []
|
||||||
wensen = request.data.get("wensen") or []
|
wensen = request.data.get("wensen") or []
|
||||||
respecteer = request.data.get("respecteer_bestaand", True)
|
respecteer = request.data.get("respecteer_bestaand", True)
|
||||||
vrije_dagen = {int(d) for d in (request.data.get("vrije_dagen") or [])}
|
vrije_dagen = {int(d) for d in (request.data.get("vrije_dagen") or [])}
|
||||||
|
|
@ -26,47 +33,45 @@ def voorstel(request):
|
||||||
max_per_dag = int(mpd) if mpd not in (None, "", 0, "0") else None
|
max_per_dag = int(mpd) if mpd not in (None, "", 0, "0") else None
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
max_per_dag = None
|
max_per_dag = None
|
||||||
if not sjid or not groep_id:
|
|
||||||
return Response({"detail": "Geef een schooljaar en een groep op."}, status=400)
|
if not sjid or not groepen_ids:
|
||||||
|
return Response({"detail": "Geef een schooljaar en minstens één klas op."}, status=400)
|
||||||
try:
|
try:
|
||||||
sj = Schooljaar.objects.get(pk=sjid)
|
sj = Schooljaar.objects.get(pk=sjid)
|
||||||
groep = Groep.objects.get(pk=groep_id)
|
except Schooljaar.DoesNotExist:
|
||||||
except (Schooljaar.DoesNotExist, Groep.DoesNotExist):
|
return Response({"detail": "Schooljaar niet gevonden."}, status=404)
|
||||||
return Response({"detail": "Schooljaar of groep niet gevonden."}, status=404)
|
groepen = {g.id: g for g in Groep.objects.filter(id__in=groepen_ids)}
|
||||||
|
ordered_gids = [gid for gid in groepen_ids if gid in groepen]
|
||||||
|
if not ordered_gids:
|
||||||
|
return Response({"detail": "Geen geldige klassen."}, status=404)
|
||||||
|
|
||||||
# Kandidaat-tijdsloten: groep-specifiek of algemeen, vrije dagen uitgesloten.
|
alle_sloten = list(Tijdslot.objects.filter(actief=True).order_by("dag", "volgorde", "start_tijd"))
|
||||||
sloten = [
|
slot_dag = {s.id: s.dag for s in alle_sloten}
|
||||||
s for s in Tijdslot.objects.filter(actief=True)
|
|
||||||
.filter(Q(groep=groep) | Q(groep__isnull=True))
|
|
||||||
.order_by("dag", "volgorde", "start_tijd")
|
|
||||||
if s.dag not in vrije_dagen
|
|
||||||
]
|
|
||||||
if not sloten:
|
|
||||||
return Response({"voorstel": [], "niet_geplaatst": [], "detail": "Geen bruikbare tijdsloten (let op vrije dagen)."})
|
|
||||||
|
|
||||||
alle_slot_dag = {t.id: t.dag for t in Tijdslot.objects.all()}
|
def sloten_voor(gid):
|
||||||
|
return [s for s in alle_sloten if s.groep_id in (None, gid) and s.dag not in vrije_dagen]
|
||||||
|
|
||||||
# Bezetting bijhouden (eventueel inclusief bestaande blokken).
|
# Gedeelde bezetting over alle klassen heen.
|
||||||
groep_bezet = set()
|
groep_bezet = {gid: set() for gid in ordered_gids}
|
||||||
beg_bezet = {}
|
per_dag = {} # (gid, weekdag) -> aantal
|
||||||
lok_bezet = {}
|
beg_bezet = {} # tijdslot -> set(begeleider-id) (globaal)
|
||||||
per_dag = {} # weekdag -> aantal blokken voor deze groep
|
lok_bezet = {} # tijdslot -> set(lokaal-id) (globaal)
|
||||||
if respecteer:
|
if respecteer:
|
||||||
for b in Roosterblok.objects.filter(schooljaar=sj).prefetch_related("begeleiders"):
|
for b in Roosterblok.objects.filter(schooljaar=sj).prefetch_related("begeleiders"):
|
||||||
if b.groep_id == groep.id:
|
|
||||||
groep_bezet.add(b.tijdslot_id)
|
|
||||||
wd = alle_slot_dag.get(b.tijdslot_id)
|
|
||||||
if wd is not None:
|
|
||||||
per_dag[wd] = per_dag.get(wd, 0) + 1
|
|
||||||
for p in b.begeleiders.all():
|
for p in b.begeleiders.all():
|
||||||
beg_bezet.setdefault(b.tijdslot_id, set()).add(p.id)
|
beg_bezet.setdefault(b.tijdslot_id, set()).add(p.id)
|
||||||
if b.lokaal_id:
|
if b.lokaal_id:
|
||||||
lok_bezet.setdefault(b.tijdslot_id, set()).add(b.lokaal_id)
|
lok_bezet.setdefault(b.tijdslot_id, set()).add(b.lokaal_id)
|
||||||
|
if b.groep_id in groep_bezet:
|
||||||
|
groep_bezet[b.groep_id].add(b.tijdslot_id)
|
||||||
|
wd = slot_dag.get(b.tijdslot_id)
|
||||||
|
if wd is not None:
|
||||||
|
per_dag[(b.groep_id, wd)] = per_dag.get((b.groep_id, wd), 0) + 1
|
||||||
|
|
||||||
act_naam = {a.id: a.naam for a in Activiteit.objects.all()}
|
act_naam = {a.id: a.naam for a in Activiteit.objects.all()}
|
||||||
pers_naam = {p.id: p.volledige_naam for p in Persoon.objects.all()}
|
pers_naam = {p.id: p.volledige_naam for p in Persoon.objects.all()}
|
||||||
lok_naam = {l.id: l.naam for l in Lokaal.objects.all()}
|
lok_naam = {l.id: l.naam for l in Lokaal.objects.all()}
|
||||||
slot_info = {s.id: {"label": f"{s.get_dag_display()} {s.start_tijd:%H:%M} {s.naam}", "dag": s.dag, "dag_label": s.get_dag_display()} for s in sloten}
|
slot_info = {s.id: {"label": f"{s.get_dag_display()} {s.start_tijd:%H:%M} {s.naam}", "dag": s.dag, "dag_label": s.get_dag_display()} for s in alle_sloten}
|
||||||
|
|
||||||
voorstel_lijst = []
|
voorstel_lijst = []
|
||||||
niet = []
|
niet = []
|
||||||
|
|
@ -80,8 +85,15 @@ def voorstel(request):
|
||||||
continue
|
continue
|
||||||
beg = int(beg) if beg else None
|
beg = int(beg) if beg else None
|
||||||
lok = int(lok) if lok else None
|
lok = int(lok) if lok else None
|
||||||
|
# Doelklassen van deze wens (leeg = alle gekozen klassen).
|
||||||
|
try:
|
||||||
|
doel = [int(g) for g in (w.get("groepen") or [])]
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
doel = []
|
||||||
|
doel = [gid for gid in doel if gid in groepen] or list(ordered_gids)
|
||||||
|
|
||||||
# Voorkeur dagdeel: zet de voorkeurssloten vooraan (zachte voorkeur).
|
for gid in doel:
|
||||||
|
sloten = sloten_voor(gid)
|
||||||
if dagdeel == "ochtend":
|
if dagdeel == "ochtend":
|
||||||
kandidaten = [s for s in sloten if _is_ochtend(s)] + [s for s in sloten if not _is_ochtend(s)]
|
kandidaten = [s for s in sloten if _is_ochtend(s)] + [s for s in sloten if not _is_ochtend(s)]
|
||||||
elif dagdeel == "middag":
|
elif dagdeel == "middag":
|
||||||
|
|
@ -91,15 +103,15 @@ def voorstel(request):
|
||||||
|
|
||||||
geplaatst = 0
|
geplaatst = 0
|
||||||
vak_dagen = set()
|
vak_dagen = set()
|
||||||
for pas in (1, 2): # pas 1 = spreiden (max 1 per dag), pas 2 = opvullen
|
for pas in (1, 2):
|
||||||
for s in kandidaten:
|
for s in kandidaten:
|
||||||
if geplaatst >= aantal:
|
if geplaatst >= aantal:
|
||||||
break
|
break
|
||||||
if s.id in groep_bezet:
|
if s.id in groep_bezet[gid]:
|
||||||
continue
|
continue
|
||||||
if pas == 1 and s.dag in vak_dagen:
|
if pas == 1 and s.dag in vak_dagen:
|
||||||
continue
|
continue
|
||||||
if max_per_dag is not None and per_dag.get(s.dag, 0) >= max_per_dag:
|
if max_per_dag is not None and per_dag.get((gid, s.dag), 0) >= max_per_dag:
|
||||||
continue
|
continue
|
||||||
if beg and beg in beg_bezet.get(s.id, set()):
|
if beg and beg in beg_bezet.get(s.id, set()):
|
||||||
continue
|
continue
|
||||||
|
|
@ -113,15 +125,16 @@ def voorstel(request):
|
||||||
"dagdeel": "ochtend" if _is_ochtend(s) else "middag",
|
"dagdeel": "ochtend" if _is_ochtend(s) else "middag",
|
||||||
"activiteit": act_id,
|
"activiteit": act_id,
|
||||||
"activiteit_naam": act_naam.get(act_id, "?"),
|
"activiteit_naam": act_naam.get(act_id, "?"),
|
||||||
"groep": groep.id,
|
"groep": gid,
|
||||||
|
"groep_naam": groepen[gid].naam,
|
||||||
"begeleider": beg,
|
"begeleider": beg,
|
||||||
"begeleider_naam": pers_naam.get(beg) if beg else None,
|
"begeleider_naam": pers_naam.get(beg) if beg else None,
|
||||||
"lokaal": lok,
|
"lokaal": lok,
|
||||||
"lokaal_naam": lok_naam.get(lok) if lok else None,
|
"lokaal_naam": lok_naam.get(lok) if lok else None,
|
||||||
})
|
})
|
||||||
groep_bezet.add(s.id)
|
groep_bezet[gid].add(s.id)
|
||||||
vak_dagen.add(s.dag)
|
vak_dagen.add(s.dag)
|
||||||
per_dag[s.dag] = per_dag.get(s.dag, 0) + 1
|
per_dag[(gid, s.dag)] = per_dag.get((gid, s.dag), 0) + 1
|
||||||
if beg:
|
if beg:
|
||||||
beg_bezet.setdefault(s.id, set()).add(beg)
|
beg_bezet.setdefault(s.id, set()).add(beg)
|
||||||
if lok:
|
if lok:
|
||||||
|
|
@ -130,7 +143,7 @@ def voorstel(request):
|
||||||
if geplaatst >= aantal:
|
if geplaatst >= aantal:
|
||||||
break
|
break
|
||||||
if geplaatst < aantal:
|
if geplaatst < aantal:
|
||||||
niet.append({"activiteit_naam": act_naam.get(act_id, "?"), "tekort": aantal - geplaatst})
|
niet.append({"activiteit_naam": act_naam.get(act_id, "?"), "groep_naam": groepen[gid].naam, "tekort": aantal - geplaatst})
|
||||||
|
|
||||||
voorstel_lijst.sort(key=lambda x: (x["dag"], x["tijdslot_label"]))
|
voorstel_lijst.sort(key=lambda x: (x["groep_naam"], x["dag"], x["tijdslot_label"]))
|
||||||
return Response({"voorstel": voorstel_lijst, "niet_geplaatst": niet})
|
return Response({"voorstel": voorstel_lijst, "niet_geplaatst": niet})
|
||||||
|
|
|
||||||
|
|
@ -3222,11 +3222,11 @@ export function AutoRoosterPage() {
|
||||||
const [schooljaren, setSchooljaren] = useState([]);
|
const [schooljaren, setSchooljaren] = useState([]);
|
||||||
const [schooljaar, setSchooljaar] = useState("");
|
const [schooljaar, setSchooljaar] = useState("");
|
||||||
const [groepen, setGroepen] = useState([]);
|
const [groepen, setGroepen] = useState([]);
|
||||||
const [groep, setGroep] = useState("");
|
const [gekozenGroepen, setGekozenGroepen] = useState([]);
|
||||||
const [activiteiten, setActiviteiten] = useState([]);
|
const [activiteiten, setActiviteiten] = useState([]);
|
||||||
const [medewerkers, setMedewerkers] = useState([]);
|
const [medewerkers, setMedewerkers] = useState([]);
|
||||||
const [lokalen, setLokalen] = useState([]);
|
const [lokalen, setLokalen] = useState([]);
|
||||||
const [wensen, setWensen] = useState([{ activiteit: "", aantal: 1, begeleider: "", lokaal: "", dagdeel: "" }]);
|
const [wensen, setWensen] = useState([{ activiteit: "", aantal: 1, groepen: [], begeleider: "", lokaal: "", dagdeel: "" }]);
|
||||||
const [vrijeDagen, setVrijeDagen] = useState([]);
|
const [vrijeDagen, setVrijeDagen] = useState([]);
|
||||||
const [maxPerDag, setMaxPerDag] = useState("");
|
const [maxPerDag, setMaxPerDag] = useState("");
|
||||||
const [voorstel, setVoorstel] = useState(null);
|
const [voorstel, setVoorstel] = useState(null);
|
||||||
|
|
@ -3244,19 +3244,39 @@ export function AutoRoosterPage() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setWens = (i, veld, val) => setWensen((ws) => ws.map((w, idx) => idx === i ? { ...w, [veld]: val } : w));
|
const setWens = (i, veld, val) => setWensen((ws) => ws.map((w, idx) => idx === i ? { ...w, [veld]: val } : w));
|
||||||
const addWens = () => setWensen((ws) => [...ws, { activiteit: "", aantal: 1, begeleider: "", lokaal: "", dagdeel: "" }]);
|
const addWens = () => setWensen((ws) => [...ws, { activiteit: "", aantal: 1, groepen: [...gekozenGroepen], begeleider: "", lokaal: "", dagdeel: "" }]);
|
||||||
const delWens = (i) => setWensen((ws) => ws.filter((_, idx) => idx !== i));
|
const delWens = (i) => setWensen((ws) => ws.filter((_, idx) => idx !== i));
|
||||||
|
function toggleGroep(gid) {
|
||||||
|
setGekozenGroepen((g) => {
|
||||||
|
const aan = g.includes(gid);
|
||||||
|
const nieuw = aan ? g.filter((x) => x !== gid) : [...g, gid];
|
||||||
|
if (aan) setWensen((ws) => ws.map((w) => ({ ...w, groepen: w.groepen.filter((x) => x !== gid) })));
|
||||||
|
return nieuw;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const effGroepen = (w) => (w.groepen && w.groepen.length ? w.groepen : gekozenGroepen);
|
||||||
|
const toggleWensGroep = (i, gid) => setWensen((ws) => ws.map((w, idx) => {
|
||||||
|
if (idx !== i) return w;
|
||||||
|
const eff = (w.groepen && w.groepen.length ? w.groepen : gekozenGroepen);
|
||||||
|
return { ...w, groepen: eff.includes(gid) ? eff.filter((x) => x !== gid) : [...eff, gid] };
|
||||||
|
}));
|
||||||
|
const groepNaam = (gid) => (groepen.find((g) => g.id === gid) || {}).naam || "?";
|
||||||
|
|
||||||
async function genereer() {
|
async function genereer() {
|
||||||
setError(null); setResultaat(null); setVoorstel(null); setNiet([]);
|
setError(null); setResultaat(null); setVoorstel(null); setNiet([]);
|
||||||
if (!groep) { setError("Kies eerst een groep."); return; }
|
if (gekozenGroepen.length === 0) { setError("Kies eerst één of meer klassen."); return; }
|
||||||
const geldig = wensen.filter((w) => w.activiteit && Number(w.aantal) > 0);
|
const geldig = wensen.filter((w) => w.activiteit && Number(w.aantal) > 0);
|
||||||
if (geldig.length === 0) { setError("Voeg minstens één wens toe (vak + aantal)."); return; }
|
if (geldig.length === 0) { setError("Voeg minstens één wens toe (vak + aantal)."); return; }
|
||||||
setBezig(true);
|
setBezig(true);
|
||||||
try {
|
try {
|
||||||
const r = await createResource("autorooster/voorstel", {
|
const r = await createResource("autorooster/voorstel", {
|
||||||
schooljaar: Number(schooljaar), groep: Number(groep),
|
schooljaar: Number(schooljaar),
|
||||||
wensen: geldig.map((w) => ({ activiteit: Number(w.activiteit), aantal: Number(w.aantal), begeleider: w.begeleider || null, lokaal: w.lokaal || null, dagdeel: w.dagdeel || "" })),
|
groepen: gekozenGroepen,
|
||||||
|
wensen: geldig.map((w) => ({
|
||||||
|
activiteit: Number(w.activiteit), aantal: Number(w.aantal),
|
||||||
|
groepen: (w.groepen && w.groepen.length ? w.groepen : gekozenGroepen),
|
||||||
|
begeleider: w.begeleider || null, lokaal: w.lokaal || null, dagdeel: w.dagdeel || "",
|
||||||
|
})),
|
||||||
vrije_dagen: vrijeDagen,
|
vrije_dagen: vrijeDagen,
|
||||||
max_per_dag: maxPerDag === "" ? null : Number(maxPerDag),
|
max_per_dag: maxPerDag === "" ? null : Number(maxPerDag),
|
||||||
});
|
});
|
||||||
|
|
@ -3276,32 +3296,32 @@ export function AutoRoosterPage() {
|
||||||
begeleiders: v.begeleider ? [v.begeleider] : [], lokaal: v.lokaal || null,
|
begeleiders: v.begeleider ? [v.begeleider] : [], lokaal: v.lokaal || null,
|
||||||
});
|
});
|
||||||
ok += 1;
|
ok += 1;
|
||||||
} catch (e) { mislukt.push(`${v.activiteit_naam} (${v.tijdslot_label}): ${e.message}`); }
|
} catch (e) { mislukt.push(`${v.activiteit_naam} ${v.groep_naam} (${v.tijdslot_label}): ${e.message}`); }
|
||||||
}
|
}
|
||||||
setResultaat({ ok, mislukt });
|
setResultaat({ ok, mislukt });
|
||||||
setVoorstel(null);
|
setVoorstel(null);
|
||||||
setBezig(false);
|
setBezig(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const perDag = {};
|
const groepenInVoorstel = voorstel ? [...new Set(voorstel.map((v) => v.groep_naam))].sort() : [];
|
||||||
(voorstel || []).forEach((v) => { (perDag[v.dag] = perDag[v.dag] || []).push(v); });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="autorooster">
|
<section className="autorooster">
|
||||||
<h1>Automatisch rooster</h1>
|
<h1>Automatisch rooster</h1>
|
||||||
<p className="muted">Geef per groep je wensen op; het systeem stelt een conflictvrij voorstel voor (geen dubbele begeleider of lokaal, gespreid over de week). Daarna pas je het gewoon aan in de Roostermaker.</p>
|
<p className="muted">Kies de klassen, geef je wensen op (een vak kan voor meerdere klassen gelden, bv. gym of atelier) en genereer een conflictvrij voorstel — begeleiders en lokalen worden over alle klassen heen niet dubbel geboekt. Daarna pas je het aan in de Roostermaker.</p>
|
||||||
<Foutmelding msg={error} />
|
<Foutmelding msg={error} />
|
||||||
|
|
||||||
<div className="form-row">
|
<div className="form-row" style={{ alignItems: "center", flexWrap: "wrap" }}>
|
||||||
{schooljaren.length > 1 && (
|
{schooljaren.length > 1 && (
|
||||||
<select value={schooljaar} onChange={(e) => setSchooljaar(e.target.value)}>
|
<select value={schooljaar} onChange={(e) => setSchooljaar(e.target.value)}>
|
||||||
{schooljaren.map((j) => <option key={j.id} value={j.id}>{j.naam}</option>)}
|
{schooljaren.map((j) => <option key={j.id} value={j.id}>{j.naam}</option>)}
|
||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
<select value={groep} onChange={(e) => setGroep(e.target.value)}>
|
<span className="muted small">Klassen:</span>
|
||||||
<option value="">— kies groep —</option>
|
{groepen.map((g) => (
|
||||||
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam}</option>)}
|
<button type="button" key={g.id} className={gekozenGroepen.includes(g.id) ? "chip on" : "chip"} onClick={() => toggleGroep(g.id)}>{g.naam}</button>
|
||||||
</select>
|
))}
|
||||||
|
{groepen.length === 0 && <span className="muted small">Nog geen klassen.</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Voorkeuren</h2>
|
<h2>Voorkeuren</h2>
|
||||||
|
|
@ -3317,7 +3337,7 @@ export function AutoRoosterPage() {
|
||||||
|
|
||||||
<h2>Wensen</h2>
|
<h2>Wensen</h2>
|
||||||
<table className="grid">
|
<table className="grid">
|
||||||
<thead><tr><th>Vak</th><th>Aantal/week</th><th>Voorkeur dagdeel</th><th>Begeleider (optioneel)</th><th>Lokaal (optioneel)</th><th></th></tr></thead>
|
<thead><tr><th>Vak</th><th>Aantal/week</th><th>Voor welke klassen</th><th>Voorkeur dagdeel</th><th>Begeleider</th><th>Lokaal</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{wensen.map((w, i) => (
|
{wensen.map((w, i) => (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
|
|
@ -3327,7 +3347,15 @@ export function AutoRoosterPage() {
|
||||||
{activiteiten.map((a) => <option key={a.id} value={a.id}>{a.naam}</option>)}
|
{activiteiten.map((a) => <option key={a.id} value={a.id}>{a.naam}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td><input type="number" min="1" max="20" value={w.aantal} onChange={(e) => setWens(i, "aantal", e.target.value)} style={{ width: 70 }} /></td>
|
<td><input type="number" min="1" max="20" value={w.aantal} onChange={(e) => setWens(i, "aantal", e.target.value)} style={{ width: 64 }} /></td>
|
||||||
|
<td>
|
||||||
|
<div className="chips compact">
|
||||||
|
{gekozenGroepen.length === 0 && <span className="muted small">kies eerst klassen</span>}
|
||||||
|
{gekozenGroepen.map((gid) => (
|
||||||
|
<button type="button" key={gid} className={effGroepen(w).includes(gid) ? "chip on" : "chip"} onClick={() => toggleWensGroep(i, gid)}>{groepNaam(gid)}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select value={w.dagdeel} onChange={(e) => setWens(i, "dagdeel", e.target.value)}>
|
<select value={w.dagdeel} onChange={(e) => setWens(i, "dagdeel", e.target.value)}>
|
||||||
<option value="">geen</option>
|
<option value="">geen</option>
|
||||||
|
|
@ -3366,11 +3394,17 @@ export function AutoRoosterPage() {
|
||||||
|
|
||||||
{voorstel && (
|
{voorstel && (
|
||||||
<div className="voorstel">
|
<div className="voorstel">
|
||||||
<h2>Voorstel ({voorstel.length} blokken)</h2>
|
<h2>Voorstel ({voorstel.length} blokken, {groepenInVoorstel.length} klas(sen))</h2>
|
||||||
{niet.length > 0 && (
|
{niet.length > 0 && (
|
||||||
<div className="banner error">Niet alles paste: {niet.map((n) => `${n.activiteit_naam} (${n.tekort} te kort)`).join(", ")}. Voeg tijdsloten toe of verlaag het aantal.</div>
|
<div className="banner error">Niet alles paste: {niet.map((n) => `${n.activiteit_naam} ${n.groep_naam || ""} (${n.tekort} te kort)`).join(", ")}.</div>
|
||||||
)}
|
)}
|
||||||
{voorstel.length === 0 ? <p className="muted">Geen blokken voorgesteld.</p> : (
|
{voorstel.length === 0 ? <p className="muted">Geen blokken voorgesteld.</p> : groepenInVoorstel.map((gn) => {
|
||||||
|
const items = voorstel.filter((v) => v.groep_naam === gn);
|
||||||
|
const perDag = {};
|
||||||
|
items.forEach((v) => { (perDag[v.dag] = perDag[v.dag] || []).push(v); });
|
||||||
|
return (
|
||||||
|
<div key={gn} className="voorstel-klas">
|
||||||
|
<h3>{gn}</h3>
|
||||||
<div className="ez-week">
|
<div className="ez-week">
|
||||||
{WEEKDAGEN.map((d) => (
|
{WEEKDAGEN.map((d) => (
|
||||||
<div className="ez-dag-kolom" key={d.value}>
|
<div className="ez-dag-kolom" key={d.value}>
|
||||||
|
|
@ -3390,7 +3424,9 @@ export function AutoRoosterPage() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<button type="button" className="btn primary" disabled={bezig || voorstel.length === 0} onClick={toepassen}>{bezig ? "Bezig…" : "Voorstel toepassen op rooster"}</button>
|
<button type="button" className="btn primary" disabled={bezig || voorstel.length === 0} onClick={toepassen}>{bezig ? "Bezig…" : "Voorstel toepassen op rooster"}</button>
|
||||||
<button type="button" className="btn" onClick={() => setVoorstel(null)}>Verwerpen</button>
|
<button type="button" className="btn" onClick={() => setVoorstel(null)}>Verwerpen</button>
|
||||||
|
|
@ -3401,6 +3437,7 @@ export function AutoRoosterPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* --------------------------- Wachtwoord opnieuw --------------------------- */
|
/* --------------------------- Wachtwoord opnieuw --------------------------- */
|
||||||
export function WachtwoordResetPage({ uid, token }) {
|
export function WachtwoordResetPage({ uid, token }) {
|
||||||
const [w1, setW1] = useState("");
|
const [w1, setW1] = useState("");
|
||||||
|
|
|
||||||
|
|
@ -489,3 +489,6 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
|
||||||
.banner.ok { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; padding: 10px 14px; border-radius: 8px; }
|
.banner.ok { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; padding: 10px 14px; border-radius: 8px; }
|
||||||
|
|
||||||
.chip.chip-plein { border-style: dashed; }
|
.chip.chip-plein { border-style: dashed; }
|
||||||
|
|
||||||
|
.voorstel-klas { margin-bottom: 18px; }
|
||||||
|
.voorstel-klas h3 { margin: 8px 0 6px; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue