Split-subgroep fix; vrije dagen niet bruikbaar in roostermaker; uren-geschiedenis (momentopnames, migratie module_uren 0001)
This commit is contained in:
parent
fc651c2f9d
commit
6afe007b22
9 changed files with 171 additions and 29 deletions
|
|
@ -90,8 +90,8 @@ volgt.
|
||||||
- [x] **Module die berekent hoeveel uur een groep per vak krijgt.** (module `uren`, default uit; endpoint /api/urenoverzicht/)
|
- [x] **Module die berekent hoeveel uur een groep per vak krijgt.** (module `uren`, default uit; endpoint /api/urenoverzicht/)
|
||||||
- [x] Optie om dit **per leerling** te tonen, **inclusief stage-uren**.
|
- [x] Optie om dit **per leerling** te tonen, **inclusief stage-uren**.
|
||||||
- [x] **Filters op de uren-tabellen** (per groep, per vak, zoek op leerling).
|
- [x] **Filters op de uren-tabellen** (per groep, per vak, zoek op leerling).
|
||||||
- [ ] **Geschiedenis** van dit overzicht **per leerling** tonen (verloop over de
|
- [x] **Geschiedenis** per leerling: momentopnames opslaan (model
|
||||||
tijd). _(nog te doen: vereist opslag van momentopnames)_
|
UrenMomentopname) en een tabel leerling × datum met totaal-uren.
|
||||||
- [x] **Vrije dagen tellen niet mee** in deze urenberekening.
|
- [x] **Vrije dagen tellen niet mee** in deze urenberekening.
|
||||||
|
|
||||||
## Roostermaker — vrije dagen & blok vervallen
|
## Roostermaker — vrije dagen & blok vervallen
|
||||||
|
|
@ -102,8 +102,9 @@ volgt.
|
||||||
dag laten vervallen, blijft lesdag) en ➕ (eenmalig blok op die dag), zonder
|
dag laten vervallen, blijft lesdag) en ➕ (eenmalig blok op die dag), zonder
|
||||||
gevolgen voor andere dagen.
|
gevolgen voor andere dagen.
|
||||||
|
|
||||||
- [ ] **Vrije/vakantie-/studiedagen kunnen niet gebruikt worden** in de
|
- [x] **Vrije/vakantie-/studiedagen niet bruikbaar** in de Roostermaker: een
|
||||||
Roostermaker (geen blokken plannen op die dagen).
|
blok slepen naar een vrije dag wordt geweigerd; vrije dagkolommen zijn
|
||||||
|
gearceerd; dag-acties (leegmaken/eenmalig) verschijnen niet op vrije dagen.
|
||||||
- [x] De actie heet nu **"dit vak laten vervallen"** (per blok, juiste tekst).
|
- [x] De actie heet nu **"dit vak laten vervallen"** (per blok, juiste tekst).
|
||||||
- [x] Een **hele dag laten vervallen** kan direct in het weekrooster (knop in de
|
- [x] Een **hele dag laten vervallen** kan direct in het weekrooster (knop in de
|
||||||
dagkop; maakt een vrije dag aan, met "weer lesdag" om te herstellen).
|
dagkop; maakt een vrije dag aan, met "weer lesdag" om te herstellen).
|
||||||
|
|
|
||||||
9
backend/modules/uren/admin.py
Normal file
9
backend/modules/uren/admin.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import UrenMomentopname
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UrenMomentopname)
|
||||||
|
class UrenMomentopnameAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("schooljaar", "gemaakt_op")
|
||||||
|
list_filter = ("schooljaar",)
|
||||||
28
backend/modules/uren/migrations/0001_initial.py
Normal file
28
backend/modules/uren/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UrenMomentopname",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
("gemaakt_op", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("payload", models.JSONField(default=dict)),
|
||||||
|
("schooljaar", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="uren_momentopnames", to="core.schooljaar")),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "uren-momentopname",
|
||||||
|
"verbose_name_plural": "uren-momentopnames",
|
||||||
|
"ordering": ["-gemaakt_op"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
backend/modules/uren/migrations/__init__.py
Normal file
0
backend/modules/uren/migrations/__init__.py
Normal file
19
backend/modules/uren/models.py
Normal file
19
backend/modules/uren/models.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class UrenMomentopname(models.Model):
|
||||||
|
"""Een opgeslagen momentopname van het urenoverzicht (voor geschiedenis)."""
|
||||||
|
|
||||||
|
schooljaar = models.ForeignKey(
|
||||||
|
"core.Schooljaar", on_delete=models.CASCADE, related_name="uren_momentopnames"
|
||||||
|
)
|
||||||
|
gemaakt_op = models.DateTimeField(auto_now_add=True)
|
||||||
|
payload = models.JSONField(default=dict)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "uren-momentopname"
|
||||||
|
verbose_name_plural = "uren-momentopnames"
|
||||||
|
ordering = ["-gemaakt_op"]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Urenoverzicht {self.schooljaar} @ {self.gemaakt_op:%Y-%m-%d %H:%M}"
|
||||||
|
|
@ -4,4 +4,5 @@ from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("urenoverzicht/", views.urenoverzicht, name="urenoverzicht"),
|
path("urenoverzicht/", views.urenoverzicht, name="urenoverzicht"),
|
||||||
|
path("uren-momentopnames/", views.momentopnames, name="uren-momentopnames"),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
Roosterblokken zijn wekelijks: per schooljaar telt een blok zo vaak mee als er
|
Roosterblokken zijn wekelijks: per schooljaar telt een blok zo vaak mee als er
|
||||||
lesdagen op die weekdag zijn (vrije dagen/vakanties/studiedagen tellen niet mee),
|
lesdagen op die weekdag zijn (vrije dagen/vakanties/studiedagen tellen niet mee),
|
||||||
minus de keren dat het blok via een uitzondering vervalt.
|
minus de keren dat het blok via een uitzondering vervalt. Een blok met een
|
||||||
|
geldigheidsperiode (geldig_van/geldig_tot) telt alleen binnen dat bereik mee.
|
||||||
"""
|
"""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
@ -11,23 +12,16 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
from core.models import KalenderDag, Persoon, Roosterblok, Rol, Schooljaar, Subgroep, Tijdslot
|
from core.models import KalenderDag, Persoon, Roosterblok, Rol, Schooljaar, Subgroep, Tijdslot
|
||||||
|
|
||||||
|
from .models import UrenMomentopname
|
||||||
|
|
||||||
|
|
||||||
def _duur_uren(ts):
|
def _duur_uren(ts):
|
||||||
s, e = ts.start_tijd, ts.eind_tijd
|
s, e = ts.start_tijd, ts.eind_tijd
|
||||||
return ((e.hour * 60 + e.minute) - (s.hour * 60 + s.minute)) / 60.0
|
return ((e.hour * 60 + e.minute) - (s.hour * 60 + s.minute)) / 60.0
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
def bereken_uren(sj):
|
||||||
def urenoverzicht(request):
|
"""Bereken het volledige urenoverzicht voor een schooljaar (dict)."""
|
||||||
sjid = request.query_params.get("schooljaar")
|
|
||||||
if not sjid:
|
|
||||||
return Response({"detail": "Geef een schooljaar op."}, status=400)
|
|
||||||
try:
|
|
||||||
sj = Schooljaar.objects.get(pk=sjid)
|
|
||||||
except Schooljaar.DoesNotExist:
|
|
||||||
return Response({"detail": "Schooljaar niet gevonden."}, status=404)
|
|
||||||
|
|
||||||
# Lesdagen per weekdag (ma–vr), vrije dagen uitgezonderd.
|
|
||||||
vrije = set(KalenderDag.objects.filter(schooljaar=sj).values_list("datum", flat=True))
|
vrije = set(KalenderDag.objects.filter(schooljaar=sj).values_list("datum", flat=True))
|
||||||
lesdagen = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0}
|
lesdagen = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0}
|
||||||
d = sj.start_datum
|
d = sj.start_datum
|
||||||
|
|
@ -48,16 +42,15 @@ def urenoverzicht(request):
|
||||||
|
|
||||||
def _tel_weekdag(van, tot, wd):
|
def _tel_weekdag(van, tot, wd):
|
||||||
n = 0
|
n = 0
|
||||||
d = van
|
d2 = van
|
||||||
while d <= tot:
|
while d2 <= tot:
|
||||||
if d.weekday() == wd and d not in vrije:
|
if d2.weekday() == wd and d2 not in vrije:
|
||||||
n += 1
|
n += 1
|
||||||
d += timedelta(days=1)
|
d2 += timedelta(days=1)
|
||||||
return n
|
return n
|
||||||
|
|
||||||
def occurrences(blok):
|
def occurrences(blok):
|
||||||
wd = blok.tijdslot.dag
|
wd = blok.tijdslot.dag
|
||||||
# Geldigheidsperiode binnen het schooljaar clampen.
|
|
||||||
van = max(blok.geldig_van or sj.start_datum, sj.start_datum)
|
van = max(blok.geldig_van or sj.start_datum, sj.start_datum)
|
||||||
tot = min(blok.geldig_tot or sj.eind_datum, sj.eind_datum)
|
tot = min(blok.geldig_tot or sj.eind_datum, sj.eind_datum)
|
||||||
if van > tot:
|
if van > tot:
|
||||||
|
|
@ -102,7 +95,6 @@ def urenoverzicht(request):
|
||||||
})
|
})
|
||||||
per_groep.sort(key=lambda x: x["groep"])
|
per_groep.sort(key=lambda x: x["groep"])
|
||||||
|
|
||||||
# Stage-uren (module optioneel).
|
|
||||||
stages = []
|
stages = []
|
||||||
alle_ts = []
|
alle_ts = []
|
||||||
try:
|
try:
|
||||||
|
|
@ -142,9 +134,49 @@ def urenoverzicht(request):
|
||||||
})
|
})
|
||||||
per_leerling.sort(key=lambda x: x["leerling"])
|
per_leerling.sort(key=lambda x: x["leerling"])
|
||||||
|
|
||||||
return Response({
|
return {
|
||||||
"schooljaar": sj.naam,
|
"schooljaar": sj.naam,
|
||||||
"lesdagen": lesdagen,
|
"lesdagen": lesdagen,
|
||||||
"per_groep": per_groep,
|
"per_groep": per_groep,
|
||||||
"per_leerling": per_leerling,
|
"per_leerling": per_leerling,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["GET"])
|
||||||
|
def urenoverzicht(request):
|
||||||
|
sjid = request.query_params.get("schooljaar")
|
||||||
|
if not sjid:
|
||||||
|
return Response({"detail": "Geef een schooljaar op."}, status=400)
|
||||||
|
try:
|
||||||
|
sj = Schooljaar.objects.get(pk=sjid)
|
||||||
|
except Schooljaar.DoesNotExist:
|
||||||
|
return Response({"detail": "Schooljaar niet gevonden."}, status=404)
|
||||||
|
return Response(bereken_uren(sj))
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["GET", "POST"])
|
||||||
|
def momentopnames(request):
|
||||||
|
"""GET: lijst van momentopnames (samenvatting). POST: bewaar huidige stand."""
|
||||||
|
sjid = request.query_params.get("schooljaar") or request.data.get("schooljaar")
|
||||||
|
if not sjid:
|
||||||
|
return Response({"detail": "Geef een schooljaar op."}, status=400)
|
||||||
|
try:
|
||||||
|
sj = Schooljaar.objects.get(pk=sjid)
|
||||||
|
except Schooljaar.DoesNotExist:
|
||||||
|
return Response({"detail": "Schooljaar niet gevonden."}, status=404)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
payload = bereken_uren(sj)
|
||||||
|
opname = UrenMomentopname.objects.create(schooljaar=sj, payload=payload)
|
||||||
|
return Response({"id": opname.id, "gemaakt_op": opname.gemaakt_op}, status=201)
|
||||||
|
|
||||||
|
opnames = UrenMomentopname.objects.filter(schooljaar=sj)
|
||||||
|
# Bouw een leerling x momentopname-tabel met totaal-uren.
|
||||||
|
kolommen = []
|
||||||
|
leerling_rijen = {}
|
||||||
|
for o in opnames:
|
||||||
|
kolommen.append({"id": o.id, "gemaakt_op": o.gemaakt_op})
|
||||||
|
for l in (o.payload or {}).get("per_leerling", []):
|
||||||
|
leerling_rijen.setdefault(l["leerling"], {})[o.id] = l["totaal"]
|
||||||
|
rijen = [{"leerling": naam, "totalen": tot} for naam, tot in sorted(leerling_rijen.items())]
|
||||||
|
return Response({"schooljaar": sj.naam, "momentopnames": kolommen, "leerlingen": rijen})
|
||||||
|
|
|
||||||
|
|
@ -1560,6 +1560,10 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
|
||||||
const [subgroepGroepFilter, setSubgroepGroepFilter] = useState(() => (
|
const [subgroepGroepFilter, setSubgroepGroepFilter] = useState(() => (
|
||||||
blok.subgroep ? groepVanSubgroep(subgroepen, blok.subgroep) || "" : ""
|
blok.subgroep ? groepVanSubgroep(subgroepen, blok.subgroep) || "" : ""
|
||||||
));
|
));
|
||||||
|
const [splitGroepId, setSplitGroepId] = useState(() => String(
|
||||||
|
blok.groep || groepVanSubgroep(subgroepen, blok.subgroep)
|
||||||
|
|| (blok.leerling ? ((leerlingen.find((l) => l.id === blok.leerling) || {}).groep || "") : "") || ""
|
||||||
|
));
|
||||||
const gekozenGroep = f.doel_type === "groep"
|
const gekozenGroep = f.doel_type === "groep"
|
||||||
? (f.doel ? Number(f.doel) : null)
|
? (f.doel ? Number(f.doel) : null)
|
||||||
: f.doel_type === "subgroep"
|
: f.doel_type === "subgroep"
|
||||||
|
|
@ -1567,14 +1571,16 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
|
||||||
: bovenliggendeGroep(blok, subs);
|
: bovenliggendeGroep(blok, subs);
|
||||||
const subgroepOpties = gekozenGroep ? subs.filter((s) => s.groep === gekozenGroep) : subs;
|
const subgroepOpties = gekozenGroep ? subs.filter((s) => s.groep === gekozenGroep) : subs;
|
||||||
const doelOpties = f.doel_type === "groep" ? groepen : f.doel_type === "subgroep" ? subgroepOpties : leerlingen;
|
const doelOpties = f.doel_type === "groep" ? groepen : f.doel_type === "subgroep" ? subgroepOpties : leerlingen;
|
||||||
const splitGroep = gekozenGroep || blok.groep || groepVanSubgroep(subs, blok.subgroep) || null;
|
const splitGroepNum = splitGroepId ? Number(splitGroepId) : null;
|
||||||
const splitOpties = splitGroep ? subs.filter((s) => s.groep === splitGroep) : subs;
|
const splitOpties = splitGroepNum ? subs.filter((s) => s.groep === splitGroepNum) : [];
|
||||||
const splitGroepNaam = splitGroep ? (groepen.find((g) => g.id === splitGroep)?.naam || "groep") : "groep";
|
const splitGroepNaam = splitGroepNum ? (groepen.find((g) => g.id === splitGroepNum)?.naam || "groep") : "(kies groep)";
|
||||||
|
|
||||||
async function maakSubgroep() {
|
async function maakSubgroep() {
|
||||||
if (!nieuweSub.trim()) return;
|
if (!nieuweSub.trim()) return;
|
||||||
|
const g = subgroepGroepFilter || gekozenGroep;
|
||||||
|
if (!g) { setError("Kies eerst een groep voor de nieuwe subgroep."); return; }
|
||||||
try {
|
try {
|
||||||
const s = await createResource("subgroepen", { naam: nieuweSub.trim(), groep: gekozenGroep || null });
|
const s = await createResource("subgroepen", { naam: nieuweSub.trim(), groep: Number(g) });
|
||||||
setSubs([...subs, s]);
|
setSubs([...subs, s]);
|
||||||
setSubgroepGroepFilter(s.groep || "");
|
setSubgroepGroepFilter(s.groep || "");
|
||||||
setF({ ...f, doel_type: "subgroep", doel: s.id });
|
setF({ ...f, doel_type: "subgroep", doel: s.id });
|
||||||
|
|
@ -1584,8 +1590,9 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
|
||||||
const toggleSplit = (id) => setSplitSubs((d) => d.includes(id) ? d.filter((x) => x !== id) : [...d, id]);
|
const toggleSplit = (id) => setSplitSubs((d) => d.includes(id) ? d.filter((x) => x !== id) : [...d, id]);
|
||||||
async function maakSplitSub() {
|
async function maakSplitSub() {
|
||||||
if (!splitNieuw.trim()) return;
|
if (!splitNieuw.trim()) return;
|
||||||
|
if (!splitGroepNum) { setError("Kies eerst een groep om onder te splitsen."); return; }
|
||||||
try {
|
try {
|
||||||
const s = await createResource("subgroepen", { naam: splitNieuw.trim(), groep: splitGroep });
|
const s = await createResource("subgroepen", { naam: splitNieuw.trim(), groep: splitGroepNum });
|
||||||
setSubs([...subs, s]);
|
setSubs([...subs, s]);
|
||||||
setSplitSubs([...splitSubs, s.id]);
|
setSplitSubs([...splitSubs, s.id]);
|
||||||
setSplitNieuw("");
|
setSplitNieuw("");
|
||||||
|
|
@ -1698,6 +1705,12 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
|
||||||
|
|
||||||
<details className="editor-detail split-editor" open>
|
<details className="editor-detail split-editor" open>
|
||||||
<summary>Splitsen onder {splitGroepNaam}</summary>
|
<summary>Splitsen onder {splitGroepNaam}</summary>
|
||||||
|
<div className="editor-grid">
|
||||||
|
<select value={splitGroepId} onChange={(e) => { setSplitGroepId(e.target.value); setSplitSubs([]); }} title="Onder welke groep splitsen">
|
||||||
|
<option value="">— kies groep —</option>
|
||||||
|
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div className="chips compact">
|
<div className="chips compact">
|
||||||
{splitOpties.map((s) => <button type="button" key={s.id} className={splitSubs.includes(s.id) ? "chip on" : "chip"} onClick={() => toggleSplit(s.id)}>{s.naam}</button>)}
|
{splitOpties.map((s) => <button type="button" key={s.id} className={splitSubs.includes(s.id) ? "chip on" : "chip"} onClick={() => toggleSplit(s.id)}>{s.naam}</button>)}
|
||||||
{splitOpties.length === 0 && <span className="muted small">Nog geen subgroepen onder deze groep.</span>}
|
{splitOpties.length === 0 && <span className="muted small">Nog geen subgroepen onder deze groep.</span>}
|
||||||
|
|
@ -1969,6 +1982,7 @@ export function RoosterScherm({ mode }) {
|
||||||
dragRef.current = null;
|
dragRef.current = null;
|
||||||
setGhost(null);
|
setGhost(null);
|
||||||
setHoverSlot(null);
|
setHoverSlot(null);
|
||||||
|
if (cell && cell.dataset.vrij) { setError("Op een vrije/vakantiedag kun je geen blok plaatsen."); return; }
|
||||||
if (doel && doel !== blok.tijdslot) {
|
if (doel && doel !== blok.tijdslot) {
|
||||||
try { await updateResource("roosterblokken", blok.id, { tijdslot: doel }); await laadRef.current(); }
|
try { await updateResource("roosterblokken", blok.id, { tijdslot: doel }); await laadRef.current(); }
|
||||||
catch (err) { setError(err.message); }
|
catch (err) { setError(err.message); }
|
||||||
|
|
@ -2485,6 +2499,7 @@ export function RoosterScherm({ mode }) {
|
||||||
<div
|
<div
|
||||||
className={hoverSlot === t.id ? "slot-cel hover" : "slot-cel"}
|
className={hoverSlot === t.id ? "slot-cel hover" : "slot-cel"}
|
||||||
data-tijdslot={t.id}
|
data-tijdslot={t.id}
|
||||||
|
data-vrij={kal ? "1" : undefined}
|
||||||
key={t.id}
|
key={t.id}
|
||||||
>
|
>
|
||||||
<div className="slot-tijd">{t.start_tijd?.slice(0, 5)} {t.naam}</div>
|
<div className="slot-tijd">{t.start_tijd?.slice(0, 5)} {t.naam}</div>
|
||||||
|
|
@ -2544,6 +2559,8 @@ export function UrenPage() {
|
||||||
const [groepFilter, setGroepFilter] = useState("");
|
const [groepFilter, setGroepFilter] = useState("");
|
||||||
const [vakFilter, setVakFilter] = useState("");
|
const [vakFilter, setVakFilter] = useState("");
|
||||||
const [leerlingFilter, setLeerlingFilter] = useState("");
|
const [leerlingFilter, setLeerlingFilter] = useState("");
|
||||||
|
const [historie, setHistorie] = useState(null);
|
||||||
|
const [bewaarBusy, setBewaarBusy] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listResource("schooljaren")
|
listResource("schooljaren")
|
||||||
|
|
@ -2558,6 +2575,18 @@ export function UrenPage() {
|
||||||
.catch((e) => setError(e.message))
|
.catch((e) => setError(e.message))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [schooljaar]);
|
}, [schooljaar]);
|
||||||
|
function laadHistorie(sjid) {
|
||||||
|
if (!sjid) { setHistorie(null); return; }
|
||||||
|
listResource("uren-momentopnames", `?schooljaar=${sjid}`).then(setHistorie).catch(() => setHistorie(null));
|
||||||
|
}
|
||||||
|
useEffect(() => { laadHistorie(schooljaar); }, [schooljaar]);
|
||||||
|
async function bewaarMomentopname() {
|
||||||
|
if (!schooljaar) return;
|
||||||
|
setBewaarBusy(true);
|
||||||
|
try { await createResource("uren-momentopnames", { schooljaar: Number(schooljaar) }); laadHistorie(schooljaar); }
|
||||||
|
catch (e) { setError(e.message); }
|
||||||
|
finally { setBewaarBusy(false); }
|
||||||
|
}
|
||||||
|
|
||||||
const r1 = (n) => Math.round(n * 10) / 10;
|
const r1 = (n) => Math.round(n * 10) / 10;
|
||||||
const groepNamen = data ? [...new Set(data.per_groep.map((g) => g.groep))].sort() : [];
|
const groepNamen = data ? [...new Set(data.per_groep.map((g) => g.groep))].sort() : [];
|
||||||
|
|
@ -2634,6 +2663,25 @@ export function UrenPage() {
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div className="historie-kop">
|
||||||
|
<h2>Geschiedenis</h2>
|
||||||
|
<button type="button" className="btn klein" disabled={bewaarBusy} onClick={bewaarMomentopname}>{bewaarBusy ? "Bezig…" : "Bewaar huidige stand"}</button>
|
||||||
|
</div>
|
||||||
|
{historie && historie.momentopnames.length > 0 ? (
|
||||||
|
<table className="grid">
|
||||||
|
<thead><tr><th>Leerling</th>{historie.momentopnames.map((m) => <th key={m.id}>{new Date(m.gemaakt_op).toLocaleDateString("nl-NL")}</th>)}</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{historie.leerlingen
|
||||||
|
.filter((r) => !leerlingFilter || r.leerling.toLowerCase().includes(leerlingFilter.toLowerCase()))
|
||||||
|
.map((r) => (
|
||||||
|
<tr key={r.leerling}>
|
||||||
|
<td>{r.leerling}</td>
|
||||||
|
{historie.momentopnames.map((m) => <td key={m.id}>{r.totalen[m.id] != null ? `${r.totalen[m.id]} u` : "—"}</td>)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
) : <p className="muted">Nog geen momentopnames. Klik op "Bewaar huidige stand" om het verloop in de tijd te volgen.</p>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -425,3 +425,7 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
|
||||||
.uren-filters { flex-wrap: wrap; align-items: center; gap: 8px; }
|
.uren-filters { flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||||
.periode-row { align-items: flex-end; gap: 12px; flex-wrap: wrap; }
|
.periode-row { align-items: flex-end; gap: 12px; flex-wrap: wrap; }
|
||||||
.periode-hint { color: #b45309; font-weight: 600; }
|
.periode-hint { color: #b45309; font-weight: 600; }
|
||||||
|
.rooster-kolom.vrij .slot-cel { opacity: .5; }
|
||||||
|
.slot-cel[data-vrij] { background: repeating-linear-gradient(45deg, transparent, transparent 6px, rgba(0,0,0,.03) 6px, rgba(0,0,0,.03) 12px); }
|
||||||
|
.historie-kop { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 8px; }
|
||||||
|
.historie-kop h2 { margin: 0; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue