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
|
||||
voorkeur met fallback), bepaalde dagen vrijhouden, en max. blokken per dag
|
||||
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).
|
||||
|
||||
- [ ] **Module: automatisch rooster genereren.** De gebruiker ge
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
"""Automatisch rooster: stelt een conflictvrij voorstel voor (greedy).
|
||||
|
||||
Houdt rekening met voorkeuren: een vak liever in de ochtend of middag, bepaalde
|
||||
weekdagen vrijhouden, en een maximum aantal blokken per dag per groep.
|
||||
Werkt voor meerdere klassen tegelijk. Een wens (vak) kan voor meerdere klassen
|
||||
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.response import Response
|
||||
|
||||
|
|
@ -17,7 +17,14 @@ def _is_ochtend(slot):
|
|||
@api_view(["POST"])
|
||||
def voorstel(request):
|
||||
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 []
|
||||
respecteer = request.data.get("respecteer_bestaand", True)
|
||||
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
|
||||
except (TypeError, ValueError):
|
||||
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:
|
||||
sj = Schooljaar.objects.get(pk=sjid)
|
||||
groep = Groep.objects.get(pk=groep_id)
|
||||
except (Schooljaar.DoesNotExist, Groep.DoesNotExist):
|
||||
return Response({"detail": "Schooljaar of groep niet gevonden."}, status=404)
|
||||
except Schooljaar.DoesNotExist:
|
||||
return Response({"detail": "Schooljaar 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.
|
||||
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_sloten = list(Tijdslot.objects.filter(actief=True).order_by("dag", "volgorde", "start_tijd"))
|
||||
slot_dag = {s.id: s.dag for s in alle_sloten}
|
||||
|
||||
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).
|
||||
groep_bezet = set()
|
||||
beg_bezet = {}
|
||||
lok_bezet = {}
|
||||
per_dag = {} # weekdag -> aantal blokken voor deze groep
|
||||
# Gedeelde bezetting over alle klassen heen.
|
||||
groep_bezet = {gid: set() for gid in ordered_gids}
|
||||
per_dag = {} # (gid, weekdag) -> aantal
|
||||
beg_bezet = {} # tijdslot -> set(begeleider-id) (globaal)
|
||||
lok_bezet = {} # tijdslot -> set(lokaal-id) (globaal)
|
||||
if respecteer:
|
||||
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():
|
||||
beg_bezet.setdefault(b.tijdslot_id, set()).add(p.id)
|
||||
if 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()}
|
||||
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()}
|
||||
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 = []
|
||||
niet = []
|
||||
|
|
@ -80,57 +85,65 @@ def voorstel(request):
|
|||
continue
|
||||
beg = int(beg) if beg 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).
|
||||
if dagdeel == "ochtend":
|
||||
kandidaten = [s for s in sloten if _is_ochtend(s)] + [s for s in sloten if not _is_ochtend(s)]
|
||||
elif dagdeel == "middag":
|
||||
kandidaten = [s for s in sloten if not _is_ochtend(s)] + [s for s in sloten if _is_ochtend(s)]
|
||||
else:
|
||||
kandidaten = sloten
|
||||
for gid in doel:
|
||||
sloten = sloten_voor(gid)
|
||||
if dagdeel == "ochtend":
|
||||
kandidaten = [s for s in sloten if _is_ochtend(s)] + [s for s in sloten if not _is_ochtend(s)]
|
||||
elif dagdeel == "middag":
|
||||
kandidaten = [s for s in sloten if not _is_ochtend(s)] + [s for s in sloten if _is_ochtend(s)]
|
||||
else:
|
||||
kandidaten = sloten
|
||||
|
||||
geplaatst = 0
|
||||
vak_dagen = set()
|
||||
for pas in (1, 2): # pas 1 = spreiden (max 1 per dag), pas 2 = opvullen
|
||||
for s in kandidaten:
|
||||
geplaatst = 0
|
||||
vak_dagen = set()
|
||||
for pas in (1, 2):
|
||||
for s in kandidaten:
|
||||
if geplaatst >= aantal:
|
||||
break
|
||||
if s.id in groep_bezet[gid]:
|
||||
continue
|
||||
if pas == 1 and s.dag in vak_dagen:
|
||||
continue
|
||||
if max_per_dag is not None and per_dag.get((gid, s.dag), 0) >= max_per_dag:
|
||||
continue
|
||||
if beg and beg in beg_bezet.get(s.id, set()):
|
||||
continue
|
||||
if lok and lok in lok_bezet.get(s.id, set()):
|
||||
continue
|
||||
voorstel_lijst.append({
|
||||
"tijdslot": s.id,
|
||||
"tijdslot_label": slot_info[s.id]["label"],
|
||||
"dag": slot_info[s.id]["dag"],
|
||||
"dag_label": slot_info[s.id]["dag_label"],
|
||||
"dagdeel": "ochtend" if _is_ochtend(s) else "middag",
|
||||
"activiteit": act_id,
|
||||
"activiteit_naam": act_naam.get(act_id, "?"),
|
||||
"groep": gid,
|
||||
"groep_naam": groepen[gid].naam,
|
||||
"begeleider": beg,
|
||||
"begeleider_naam": pers_naam.get(beg) if beg else None,
|
||||
"lokaal": lok,
|
||||
"lokaal_naam": lok_naam.get(lok) if lok else None,
|
||||
})
|
||||
groep_bezet[gid].add(s.id)
|
||||
vak_dagen.add(s.dag)
|
||||
per_dag[(gid, s.dag)] = per_dag.get((gid, s.dag), 0) + 1
|
||||
if beg:
|
||||
beg_bezet.setdefault(s.id, set()).add(beg)
|
||||
if lok:
|
||||
lok_bezet.setdefault(s.id, set()).add(lok)
|
||||
geplaatst += 1
|
||||
if geplaatst >= aantal:
|
||||
break
|
||||
if s.id in groep_bezet:
|
||||
continue
|
||||
if pas == 1 and s.dag in vak_dagen:
|
||||
continue
|
||||
if max_per_dag is not None and per_dag.get(s.dag, 0) >= max_per_dag:
|
||||
continue
|
||||
if beg and beg in beg_bezet.get(s.id, set()):
|
||||
continue
|
||||
if lok and lok in lok_bezet.get(s.id, set()):
|
||||
continue
|
||||
voorstel_lijst.append({
|
||||
"tijdslot": s.id,
|
||||
"tijdslot_label": slot_info[s.id]["label"],
|
||||
"dag": slot_info[s.id]["dag"],
|
||||
"dag_label": slot_info[s.id]["dag_label"],
|
||||
"dagdeel": "ochtend" if _is_ochtend(s) else "middag",
|
||||
"activiteit": act_id,
|
||||
"activiteit_naam": act_naam.get(act_id, "?"),
|
||||
"groep": groep.id,
|
||||
"begeleider": beg,
|
||||
"begeleider_naam": pers_naam.get(beg) if beg else None,
|
||||
"lokaal": lok,
|
||||
"lokaal_naam": lok_naam.get(lok) if lok else None,
|
||||
})
|
||||
groep_bezet.add(s.id)
|
||||
vak_dagen.add(s.dag)
|
||||
per_dag[s.dag] = per_dag.get(s.dag, 0) + 1
|
||||
if beg:
|
||||
beg_bezet.setdefault(s.id, set()).add(beg)
|
||||
if lok:
|
||||
lok_bezet.setdefault(s.id, set()).add(lok)
|
||||
geplaatst += 1
|
||||
if geplaatst >= aantal:
|
||||
break
|
||||
if geplaatst < aantal:
|
||||
niet.append({"activiteit_naam": act_naam.get(act_id, "?"), "tekort": aantal - geplaatst})
|
||||
if geplaatst < aantal:
|
||||
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})
|
||||
|
|
|
|||
|
|
@ -3222,11 +3222,11 @@ export function AutoRoosterPage() {
|
|||
const [schooljaren, setSchooljaren] = useState([]);
|
||||
const [schooljaar, setSchooljaar] = useState("");
|
||||
const [groepen, setGroepen] = useState([]);
|
||||
const [groep, setGroep] = useState("");
|
||||
const [gekozenGroepen, setGekozenGroepen] = useState([]);
|
||||
const [activiteiten, setActiviteiten] = useState([]);
|
||||
const [medewerkers, setMedewerkers] = 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 [maxPerDag, setMaxPerDag] = useState("");
|
||||
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 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));
|
||||
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() {
|
||||
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);
|
||||
if (geldig.length === 0) { setError("Voeg minstens één wens toe (vak + aantal)."); return; }
|
||||
setBezig(true);
|
||||
try {
|
||||
const r = await createResource("autorooster/voorstel", {
|
||||
schooljaar: Number(schooljaar), groep: Number(groep),
|
||||
wensen: geldig.map((w) => ({ activiteit: Number(w.activiteit), aantal: Number(w.aantal), begeleider: w.begeleider || null, lokaal: w.lokaal || null, dagdeel: w.dagdeel || "" })),
|
||||
schooljaar: Number(schooljaar),
|
||||
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,
|
||||
max_per_dag: maxPerDag === "" ? null : Number(maxPerDag),
|
||||
});
|
||||
|
|
@ -3276,32 +3296,32 @@ export function AutoRoosterPage() {
|
|||
begeleiders: v.begeleider ? [v.begeleider] : [], lokaal: v.lokaal || null,
|
||||
});
|
||||
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 });
|
||||
setVoorstel(null);
|
||||
setBezig(false);
|
||||
}
|
||||
|
||||
const perDag = {};
|
||||
(voorstel || []).forEach((v) => { (perDag[v.dag] = perDag[v.dag] || []).push(v); });
|
||||
const groepenInVoorstel = voorstel ? [...new Set(voorstel.map((v) => v.groep_naam))].sort() : [];
|
||||
|
||||
return (
|
||||
<section className="autorooster">
|
||||
<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} />
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-row" style={{ alignItems: "center", flexWrap: "wrap" }}>
|
||||
{schooljaren.length > 1 && (
|
||||
<select value={schooljaar} onChange={(e) => setSchooljaar(e.target.value)}>
|
||||
{schooljaren.map((j) => <option key={j.id} value={j.id}>{j.naam}</option>)}
|
||||
</select>
|
||||
)}
|
||||
<select value={groep} onChange={(e) => setGroep(e.target.value)}>
|
||||
<option value="">— kies groep —</option>
|
||||
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam}</option>)}
|
||||
</select>
|
||||
<span className="muted small">Klassen:</span>
|
||||
{groepen.map((g) => (
|
||||
<button type="button" key={g.id} className={gekozenGroepen.includes(g.id) ? "chip on" : "chip"} onClick={() => toggleGroep(g.id)}>{g.naam}</button>
|
||||
))}
|
||||
{groepen.length === 0 && <span className="muted small">Nog geen klassen.</span>}
|
||||
</div>
|
||||
|
||||
<h2>Voorkeuren</h2>
|
||||
|
|
@ -3317,7 +3337,7 @@ export function AutoRoosterPage() {
|
|||
|
||||
<h2>Wensen</h2>
|
||||
<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>
|
||||
{wensen.map((w, i) => (
|
||||
<tr key={i}>
|
||||
|
|
@ -3327,7 +3347,15 @@ export function AutoRoosterPage() {
|
|||
{activiteiten.map((a) => <option key={a.id} value={a.id}>{a.naam}</option>)}
|
||||
</select>
|
||||
</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>
|
||||
<select value={w.dagdeel} onChange={(e) => setWens(i, "dagdeel", e.target.value)}>
|
||||
<option value="">geen</option>
|
||||
|
|
@ -3366,31 +3394,39 @@ export function AutoRoosterPage() {
|
|||
|
||||
{voorstel && (
|
||||
<div className="voorstel">
|
||||
<h2>Voorstel ({voorstel.length} blokken)</h2>
|
||||
<h2>Voorstel ({voorstel.length} blokken, {groepenInVoorstel.length} klas(sen))</h2>
|
||||
{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> : (
|
||||
<div className="ez-week">
|
||||
{WEEKDAGEN.map((d) => (
|
||||
<div className="ez-dag-kolom" key={d.value}>
|
||||
<div className="ez-dag-kop">{d.label}</div>
|
||||
{(perDag[d.value] || []).length === 0 && <div className="muted small">—</div>}
|
||||
{(perDag[d.value] || []).map((v, i) => {
|
||||
const kleur = (activiteiten.find((a) => a.id === v.activiteit) || {}).kleur || "#2563eb";
|
||||
return (
|
||||
<div className="ez-blok" key={i} style={{ borderColor: kleur, background: kleur + "18" }}>
|
||||
<div className="ez-tijd">{v.tijdslot_label.split(" ")[1]}</div>
|
||||
<div className="ez-vak" style={{ color: kleur }}>{v.activiteit_naam}</div>
|
||||
{v.begeleider_naam && <div className="muted small">{v.begeleider_naam}</div>}
|
||||
{v.lokaal_naam && <div className="muted small">{v.lokaal_naam}</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{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">
|
||||
{WEEKDAGEN.map((d) => (
|
||||
<div className="ez-dag-kolom" key={d.value}>
|
||||
<div className="ez-dag-kop">{d.label}</div>
|
||||
{(perDag[d.value] || []).length === 0 && <div className="muted small">—</div>}
|
||||
{(perDag[d.value] || []).map((v, i) => {
|
||||
const kleur = (activiteiten.find((a) => a.id === v.activiteit) || {}).kleur || "#2563eb";
|
||||
return (
|
||||
<div className="ez-blok" key={i} style={{ borderColor: kleur, background: kleur + "18" }}>
|
||||
<div className="ez-tijd">{v.tijdslot_label.split(" ")[1]}</div>
|
||||
<div className="ez-vak" style={{ color: kleur }}>{v.activiteit_naam}</div>
|
||||
{v.begeleider_naam && <div className="muted small">{v.begeleider_naam}</div>}
|
||||
{v.lokaal_naam && <div className="muted small">{v.lokaal_naam}</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<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" onClick={() => setVoorstel(null)}>Verwerpen</button>
|
||||
|
|
@ -3401,6 +3437,7 @@ export function AutoRoosterPage() {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
/* --------------------------- Wachtwoord opnieuw --------------------------- */
|
||||
export function WachtwoordResetPage({ uid, token }) {
|
||||
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; }
|
||||
|
||||
.chip.chip-plein { border-style: dashed; }
|
||||
|
||||
.voorstel-klas { margin-bottom: 18px; }
|
||||
.voorstel-klas h3 { margin: 8px 0 6px; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue