Pictogram per vak (upload) + media-infra; grote leerling- en ouderweergave met pictogram en vakkleur (migratie core 0009)
This commit is contained in:
parent
6afe007b22
commit
2548b7acd9
16 changed files with 263 additions and 7 deletions
|
|
@ -109,6 +109,15 @@ volgt.
|
|||
- [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).
|
||||
|
||||
## Pictogrammen & leerlingweergave (ZML)
|
||||
|
||||
- [x] **Pictogram per vak uploaden** (afbeelding) en tonen in het rooster.
|
||||
- [x] **Grote leerlingweergave** (één dag, groot pictogram + vakkleur) en
|
||||
**ouderweergave** (weekoverzicht), per leerling of groep.
|
||||
- [x] Vakkleur duidelijk gebruikt in de weergaven.
|
||||
- [ ] Later: pictogrammen ook per losse activiteit/stap binnen een dag; dagdeel-
|
||||
indeling met picto's.
|
||||
|
||||
## Later
|
||||
|
||||
- [ ] **Module: automatisch rooster genereren.** De gebruiker ge
|
||||
|
|
@ -24,7 +24,7 @@ RUN chmod +x /app/entrypoint.sh
|
|||
# Draai niet als root: een eventueel lek in de app geeft dan geen
|
||||
# root-rechten in de container.
|
||||
RUN useradd --create-home --shell /usr/sbin/nologin appuser \
|
||||
&& mkdir -p /app/staticfiles \
|
||||
&& mkdir -p /app/staticfiles /app/media \
|
||||
&& chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
|
||||
|
|
|
|||
|
|
@ -150,6 +150,10 @@ USE_TZ = True
|
|||
# (o.a. de CSS van de Django-admin).
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = os.environ.get("DJANGO_STATIC_ROOT", str(BASE_DIR / "staticfiles"))
|
||||
|
||||
# Geüploade bestanden (o.a. pictogrammen bij vakken).
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = os.environ.get("DJANGO_MEDIA_ROOT", str(BASE_DIR / "media"))
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
# --- API ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"""Hoofd-URL-configuratie."""
|
||||
from django.contrib import admin
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
|
|
@ -14,3 +16,6 @@ urlpatterns = [
|
|||
path("api/", include("modules.uren.urls")),
|
||||
path("api/", include("modules.accounts.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ class CoreConfig(AppConfig):
|
|||
# Hoofdniveau: Rooster = weergave, Roostermaker = aanpasser.
|
||||
MenuItem("Rooster", "/rooster", icon="calendar", order=10),
|
||||
MenuItem("Roostermaker", "/roostermaker", icon="calendar", order=12),
|
||||
MenuItem("Leerlingrooster", "/leerlingrooster", icon="book", order=14),
|
||||
MenuItem("Ouderrooster", "/ouderrooster", icon="badge", order=16),
|
||||
# Submenu Organisatie: wie hoort waar.
|
||||
MenuItem("Personen", "/personen", icon="users", order=20, group="Organisatie"),
|
||||
MenuItem("Groepsindeling", "/groepsindeling", icon="layers", order=24, group="Organisatie"),
|
||||
|
|
|
|||
18
backend/core/migrations/0009_activiteit_pictogram.py
Normal file
18
backend/core/migrations/0009_activiteit_pictogram.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.2.15 on 2026-06-16 21:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0008_roosterblok_geldig_tot_roosterblok_geldig_van'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='activiteit',
|
||||
name='pictogram',
|
||||
field=models.ImageField(blank=True, help_text='Optioneel pictogram (afbeelding) ter ondersteuning van leerlingen.', null=True, upload_to='pictogrammen/', verbose_name='pictogram'),
|
||||
),
|
||||
]
|
||||
|
|
@ -286,6 +286,10 @@ class Activiteit(models.Model):
|
|||
naam = models.CharField("naam", max_length=80, unique=True)
|
||||
kleur = models.CharField("kleur", max_length=7, default="#2563eb", help_text="hex, bv. #2563eb")
|
||||
icoon = models.CharField("icoon", max_length=8, blank=True, help_text="Optioneel emoji; leeg = automatisch gekozen op naam.")
|
||||
pictogram = models.ImageField(
|
||||
"pictogram", upload_to="pictogrammen/", null=True, blank=True,
|
||||
help_text="Optioneel pictogram (afbeelding) ter ondersteuning van leerlingen.",
|
||||
)
|
||||
omschrijving = models.CharField("omschrijving", max_length=255, blank=True)
|
||||
theorievak = models.BooleanField(
|
||||
"theorievak", default=False,
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ class ActiviteitSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Activiteit
|
||||
fields = ["id", "naam", "kleur", "icoon", "omschrijving", "theorievak", "actief"]
|
||||
fields = ["id", "naam", "kleur", "icoon", "pictogram", "omschrijving", "theorievak", "actief"]
|
||||
|
||||
|
||||
class LocatieSerializer(serializers.ModelSerializer):
|
||||
|
|
@ -208,6 +208,7 @@ class RoosterblokSerializer(serializers.ModelSerializer):
|
|||
activiteit_naam = serializers.CharField(source="activiteit.naam", read_only=True)
|
||||
activiteit_kleur = serializers.CharField(source="activiteit.kleur", read_only=True)
|
||||
activiteit_icoon = serializers.CharField(source="activiteit.icoon", read_only=True, default="")
|
||||
activiteit_pictogram = serializers.ImageField(source="activiteit.pictogram", read_only=True)
|
||||
tijdslot_label = serializers.CharField(source="tijdslot.__str__", read_only=True)
|
||||
locatie_naam = serializers.CharField(source="locatie.naam", read_only=True, default=None)
|
||||
lokaal_naam = serializers.CharField(source="lokaal.naam", read_only=True, default=None)
|
||||
|
|
@ -219,7 +220,7 @@ class RoosterblokSerializer(serializers.ModelSerializer):
|
|||
model = Roosterblok
|
||||
fields = [
|
||||
"id", "schooljaar", "tijdslot", "tijdslot_label",
|
||||
"activiteit", "activiteit_naam", "activiteit_kleur", "activiteit_icoon",
|
||||
"activiteit", "activiteit_naam", "activiteit_kleur", "activiteit_icoon", "activiteit_pictogram",
|
||||
"locatie", "locatie_naam", "lokaal", "lokaal_naam",
|
||||
"groep", "subgroep", "leerling", "doel_type", "doel_naam",
|
||||
"geldig_van", "geldig_tot",
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@ django-cors-headers>=4.4
|
|||
# --- Productie / test-server ---------------------------------------------
|
||||
gunicorn>=22.0 # WSGI-server die Django draait
|
||||
psycopg[binary]>=3.2 # PostgreSQL-driver
|
||||
Pillow>=10.0 # beeldverwerking voor pictogram-upload
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ services:
|
|||
volumes:
|
||||
# Verzamelde statics delen met nginx.
|
||||
- static_volume:/app/staticfiles
|
||||
# Geüploade media (pictogrammen) delen met nginx.
|
||||
- media_volume:/app/media
|
||||
|
||||
nginx:
|
||||
build: ./frontend
|
||||
|
|
@ -52,7 +54,9 @@ services:
|
|||
volumes:
|
||||
# Lees de door Django verzamelde statics (alleen-lezen).
|
||||
- static_volume:/static:ro
|
||||
- media_volume:/media:ro
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
static_volume:
|
||||
media_volume:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,13 @@ server {
|
|||
expires 30d;
|
||||
}
|
||||
|
||||
# Geüploade media (pictogrammen), uit het gedeelde volume.
|
||||
location /media/ {
|
||||
alias /media/;
|
||||
access_log off;
|
||||
expires 7d;
|
||||
}
|
||||
|
||||
# API-verzoeken naar de Django-backend (gunicorn).
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
ActiviteitenPage, LocatiesPage, LokalenPage, TijdslotenPage, SchooljaarPage,
|
||||
RoosterWeergavePage, RoostermakerPage,
|
||||
AfwezigheidPage, GroepsindelingPage, StagePage, GebruikersPage, UrenPage,
|
||||
LeerlingRoosterPage, OuderRoosterPage,
|
||||
} from "./pages.jsx";
|
||||
|
||||
const ICONS = {
|
||||
|
|
@ -35,6 +36,8 @@ const PAGES = {
|
|||
"/stage": StagePage,
|
||||
"/gebruikers": GebruikersPage,
|
||||
"/uren": UrenPage,
|
||||
"/leerlingrooster": LeerlingRoosterPage,
|
||||
"/ouderrooster": OuderRoosterPage,
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
|
|
|
|||
|
|
@ -61,3 +61,24 @@ export const deleteResource = (resource, id) =>
|
|||
// --- Lokaal-instelling (singleton; dubbele-boekingbeleid) ------------------
|
||||
export const getLokaalinstelling = () => request("GET", "/lokaalinstelling/");
|
||||
export const setLokaalinstelling = (data) => request("PUT", "/lokaalinstelling/", data);
|
||||
|
||||
// --- Bestand uploaden (multipart, bv. pictogram bij een vak) ---------------
|
||||
export async function uploadBestand(resource, id, veld, file) {
|
||||
const fd = new FormData();
|
||||
fd.append(veld, file);
|
||||
const token = getCookie("csrftoken");
|
||||
const res = await fetch(`${BASE}/${resource}/${id}/`, {
|
||||
method: "PATCH",
|
||||
headers: token ? { "X-CSRFToken": token } : {},
|
||||
credentials: "same-origin",
|
||||
body: fd,
|
||||
});
|
||||
if (!res.ok) {
|
||||
let data = null;
|
||||
try { data = await res.json(); } catch { data = null; }
|
||||
const err = new Error((data && JSON.stringify(data)) || "Upload mislukt");
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
getModules,
|
||||
getLokaalinstelling,
|
||||
setLokaalinstelling,
|
||||
uploadBestand,
|
||||
} from "./api.js";
|
||||
|
||||
const ROLLEN = [
|
||||
|
|
@ -88,7 +89,7 @@ function Foutmelding({ msg }) {
|
|||
|
||||
/* ----------------------- Generieke eenvoudige catalogus ------------------- */
|
||||
// Voor lijsten met alleen naam + omschrijving (Functies, Activiteiten, Locaties).
|
||||
function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTheorievak = false, metIcoon = false }) {
|
||||
function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTheorievak = false, metIcoon = false, metPictogram = false }) {
|
||||
const { items, error, loading, reload, setError } = useList(resource);
|
||||
const leeg = {
|
||||
naam: "", omschrijving: "",
|
||||
|
|
@ -98,7 +99,7 @@ function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTh
|
|||
};
|
||||
const [form, setForm] = useState(leeg);
|
||||
const [editId, setEditId] = useState(null);
|
||||
const kolommen = 3 + (telKolom ? 1 : 0) + (metTheorievak ? 1 : 0);
|
||||
const kolommen = 3 + (telKolom ? 1 : 0) + (metTheorievak ? 1 : 0) + (metPictogram ? 1 : 0);
|
||||
|
||||
async function opslaan(e) {
|
||||
e.preventDefault();
|
||||
|
|
@ -121,6 +122,14 @@ function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTh
|
|||
setError(err.message);
|
||||
}
|
||||
}
|
||||
async function uploadPicto(id, file) {
|
||||
try { await uploadBestand(resource, id, "pictogram", file); await reload(); }
|
||||
catch (err) { setError(err.message); }
|
||||
}
|
||||
async function verwijderPicto(id) {
|
||||
try { await updateResource(resource, id, { pictogram: null }); await reload(); }
|
||||
catch (err) { setError(err.message); }
|
||||
}
|
||||
function bewerk(it) {
|
||||
setEditId(it.id);
|
||||
setForm({
|
||||
|
|
@ -165,6 +174,7 @@ function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTh
|
|||
<th>Naam</th>
|
||||
<th>Omschrijving</th>
|
||||
{metTheorievak && <th>Theorievak</th>}
|
||||
{metPictogram && <th>Pictogram</th>}
|
||||
{telKolom && <th>{telKolom}</th>}
|
||||
<th></th>
|
||||
</tr>
|
||||
|
|
@ -179,6 +189,13 @@ function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTh
|
|||
</td>
|
||||
<td className="muted">{it.omschrijving || "—"}</td>
|
||||
{metTheorievak && <td>{it.theorievak ? "✓" : "—"}</td>}
|
||||
{metPictogram && (
|
||||
<td className="picto-cel">
|
||||
{it.pictogram && <img src={it.pictogram} alt="" className="picto-thumb" />}
|
||||
<label className="link small">{it.pictogram ? "vervang" : "upload"}<input type="file" accept="image/*" style={{ display: "none" }} onChange={(e) => { const f = e.target.files[0]; if (f) uploadPicto(it.id, f); e.target.value = ""; }} /></label>
|
||||
{it.pictogram && <button type="button" className="link small danger" onClick={() => verwijderPicto(it.id)}>verwijder</button>}
|
||||
</td>
|
||||
)}
|
||||
{telKolom && <td>{it.aantal_personen ?? "—"}</td>}
|
||||
<td className="row-actions">
|
||||
<button className="link" onClick={() => bewerk(it)}>bewerken</button>
|
||||
|
|
@ -216,6 +233,7 @@ export function ActiviteitenPage() {
|
|||
kleur
|
||||
metTheorievak
|
||||
metIcoon
|
||||
metPictogram
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -2162,7 +2180,7 @@ export function RoosterScherm({ mode }) {
|
|||
>
|
||||
<div className="rb-top">
|
||||
{edit && <span className="rb-greep" onPointerDown={(e) => startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot">⠿</span>}
|
||||
<strong>{b.activiteit_icoon || vakIcoon(b.activiteit_naam)} {b.activiteit_naam}</strong>
|
||||
{b.activiteit_pictogram && <img src={b.activiteit_pictogram} alt="" className="rb-picto" />}<strong>{b.activiteit_icoon || vakIcoon(b.activiteit_naam)} {b.activiteit_naam}</strong>
|
||||
{edit && <button type="button" className="icon-btn" onClick={(e) => { e.stopPropagation(); toggleOpenBlok(b); }} title="Bewerken">✎</button>}
|
||||
{edit && <button type="button" className="icon-btn danger rb-x" onClick={(e) => { e.stopPropagation(); verwijder(b.id); }} title="Blok verwijderen">×</button>}
|
||||
</div>
|
||||
|
|
@ -2197,7 +2215,7 @@ export function RoosterScherm({ mode }) {
|
|||
>
|
||||
<div className="split-vak-kop">
|
||||
{edit && <span className="rb-greep" onPointerDown={(e) => startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot">⠿</span>}
|
||||
<strong>{b.activiteit_icoon || vakIcoon(b.activiteit_naam)} {b.activiteit_naam}</strong>
|
||||
{b.activiteit_pictogram && <img src={b.activiteit_pictogram} alt="" className="rb-picto" />}<strong>{b.activiteit_icoon || vakIcoon(b.activiteit_naam)} {b.activiteit_naam}</strong>
|
||||
{edit && <button type="button" className="icon-btn" onClick={(e) => { e.stopPropagation(); toggleOpenBlok(b); }} title="Bewerken">✎</button>}
|
||||
{edit && <button type="button" className="icon-btn danger" onClick={(e) => { e.stopPropagation(); verwijder(b.id); }} title="Blok verwijderen">×</button>}
|
||||
</div>
|
||||
|
|
@ -2687,3 +2705,130 @@ export function UrenPage() {
|
|||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- Eenvoudige weergave (ZML-leerling / ouder) -------------- */
|
||||
export function EenvoudigRooster({ mode }) {
|
||||
const ouder = mode === "ouder";
|
||||
const [schooljaren, setSchooljaren] = useState([]);
|
||||
const [schooljaar, setSchooljaar] = useState("");
|
||||
const [tijdsloten, setTijdsloten] = useState([]);
|
||||
const [blokken, setBlokken] = useState([]);
|
||||
const [kalender, setKalender] = useState([]);
|
||||
const [groepen, setGroepen] = useState([]);
|
||||
const [subgroepen, setSubgroepen] = useState([]);
|
||||
const [leerpleinen, setLeerpleinen] = useState([]);
|
||||
const [leerlingen, setLeerlingen] = useState([]);
|
||||
const [scopeType, setScopeType] = useState("leerling");
|
||||
const [scopeId, setScopeId] = useState("");
|
||||
const [maandag, setMaandag] = useState(() => maandagVan(new Date()));
|
||||
const [dagIdx, setDagIdx] = useState(() => Math.min((new Date().getDay() + 6) % 7, 4));
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
listResource("schooljaren").then((j) => { setSchooljaren(j); if (j.length) setSchooljaar(String(j[0].id)); }).catch((e) => setError(e.message));
|
||||
listResource("tijdsloten").then(setTijdsloten).catch(() => {});
|
||||
listResource("groepen").then(setGroepen).catch(() => {});
|
||||
listResource("subgroepen").then(setSubgroepen).catch(() => {});
|
||||
listResource("leerpleinen").then(setLeerpleinen).catch(() => setLeerpleinen([]));
|
||||
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!schooljaar) return;
|
||||
listResource("roosterblokken", `?schooljaar=${schooljaar}`).then(setBlokken).catch((e) => setError(e.message));
|
||||
listResource("kalenderdagen", `?schooljaar=${schooljaar}`).then(setKalender).catch(() => {});
|
||||
}, [schooljaar]);
|
||||
|
||||
const ctx = { leerlingen, subgroepen, leerpleinen };
|
||||
const scope = { type: scopeType, id: scopeId ? Number(scopeId) : "" };
|
||||
const scopeOpties = scopeType === "klas" ? groepen : leerlingen;
|
||||
const weekDatums = [0, 1, 2, 3, 4].map((i) => plusDagen(maandag, i));
|
||||
const kalenderOp = (iso) => kalender.find((k) => k.datum === iso);
|
||||
const blokGeldigOp = (b, iso) => !((b.geldig_van && iso < b.geldig_van) || (b.geldig_tot && iso > b.geldig_tot));
|
||||
|
||||
function dagBlokken(datum) {
|
||||
const iso = fmtISO(datum);
|
||||
const vrij = kalenderOp(iso);
|
||||
if (vrij) return { vrij, items: [] };
|
||||
const wd = (datum.getDay() + 6) % 7;
|
||||
const slots = tijdsloten.filter((t) => t.dag === wd).sort((a, b) => a.volgorde - b.volgorde);
|
||||
const items = [];
|
||||
slots.forEach((t) => {
|
||||
blokken
|
||||
.filter((b) => b.tijdslot === t.id && blokGeldigOp(b, iso) && (scopeId ? blokInScope(b, scope, ctx) : false))
|
||||
.forEach((b) => items.push({ t, b }));
|
||||
});
|
||||
return { vrij: null, items };
|
||||
}
|
||||
|
||||
function blokKaart({ t, b }, groot) {
|
||||
const kleur = b.activiteit_kleur || "#2563eb";
|
||||
return (
|
||||
<div className={groot ? "ez-blok groot" : "ez-blok"} key={b.id + "-" + t.id} style={{ borderColor: kleur, background: kleur + "18" }}>
|
||||
<div className="ez-tijd">{t.start_tijd?.slice(0, 5)}</div>
|
||||
{b.activiteit_pictogram
|
||||
? <img className="ez-picto" src={b.activiteit_pictogram} alt="" />
|
||||
: <div className="ez-emoji">{b.activiteit_icoon || vakIcoon(b.activiteit_naam)}</div>}
|
||||
<div className="ez-vak" style={{ color: kleur }}>{b.activiteit_naam}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="ez">
|
||||
<h1>{ouder ? "Weekrooster" : "Mijn rooster"}</h1>
|
||||
<Foutmelding msg={error} />
|
||||
<div className="form-row ez-bar no-print">
|
||||
<select value={scopeType} onChange={(e) => { setScopeType(e.target.value); setScopeId(""); }}>
|
||||
<option value="leerling">Per leerling</option>
|
||||
<option value="klas">Per groep</option>
|
||||
</select>
|
||||
<select value={scopeId} onChange={(e) => setScopeId(e.target.value)}>
|
||||
<option value="">— kies {scopeType === "klas" ? "groep" : "leerling"} —</option>
|
||||
{scopeOpties.map((o) => <option key={o.id} value={o.id}>{o.naam || o.volledige_naam}</option>)}
|
||||
</select>
|
||||
{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>
|
||||
)}
|
||||
<button type="button" className="btn" onClick={() => setMaandag((m) => plusDagen(m, -7))}>◀ vorige week</button>
|
||||
<button type="button" className="btn" onClick={() => setMaandag(maandagVan(new Date()))}>deze week</button>
|
||||
<button type="button" className="btn" onClick={() => setMaandag((m) => plusDagen(m, 7))}>volgende week ▶</button>
|
||||
</div>
|
||||
{!scopeId && <p className="muted">Kies hierboven een {scopeType === "klas" ? "groep" : "leerling"} om het rooster te tonen.</p>}
|
||||
{scopeId && ouder && (
|
||||
<div className="ez-week">
|
||||
{weekDatums.map((datum, i) => {
|
||||
const { vrij, items } = dagBlokken(datum);
|
||||
return (
|
||||
<div className="ez-dag-kolom" key={i}>
|
||||
<div className="ez-dag-kop">{WEEKDAGEN[i].label}<span className="muted"> {fmtDM(datum)}</span></div>
|
||||
{vrij && <div className="ez-vrij">{vrij.type_label}</div>}
|
||||
{!vrij && items.length === 0 && <div className="muted small">—</div>}
|
||||
{!vrij && items.map((x) => blokKaart(x, false))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{scopeId && !ouder && (() => {
|
||||
const datum = plusDagen(maandag, dagIdx);
|
||||
const { vrij, items } = dagBlokken(datum);
|
||||
return (
|
||||
<div className="ez-dag-groot">
|
||||
<div className="ez-dag-nav no-print">
|
||||
<button type="button" className="btn groot" onClick={() => setDagIdx((d) => Math.max(0, d - 1))} disabled={dagIdx === 0}>◀</button>
|
||||
<div className="ez-dag-titel">{WEEKDAGEN[dagIdx].label} <span className="muted">{fmtDM(datum)}</span></div>
|
||||
<button type="button" className="btn groot" onClick={() => setDagIdx((d) => Math.min(4, d + 1))} disabled={dagIdx === 4}>▶</button>
|
||||
</div>
|
||||
{vrij && <div className="ez-vrij groot">{vrij.type_label}{vrij.omschrijving ? `: ${vrij.omschrijving}` : ""}</div>}
|
||||
{!vrij && items.length === 0 && <p className="muted">Geen lessen op deze dag.</p>}
|
||||
{!vrij && <div className="ez-dag-blokken">{items.map((x) => blokKaart(x, true))}</div>}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
export function LeerlingRoosterPage() { return <EenvoudigRooster mode="leerling" />; }
|
||||
export function OuderRoosterPage() { return <EenvoudigRooster mode="ouder" />; }
|
||||
|
|
|
|||
|
|
@ -429,3 +429,34 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
|
|||
.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; }
|
||||
|
||||
/* Pictogrammen bij vak + in het rooster */
|
||||
.picto-cel { white-space: nowrap; }
|
||||
.picto-thumb { height: 28px; width: 28px; object-fit: contain; vertical-align: middle; margin-right: 6px; border-radius: 4px; }
|
||||
.rb-picto { height: 22px; width: 22px; object-fit: contain; vertical-align: middle; margin-right: 4px; }
|
||||
|
||||
/* Eenvoudige ZML-/ouderweergave */
|
||||
.ez h1 { font-size: 28px; }
|
||||
.ez-bar { flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||
.ez-week { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; align-items: start; }
|
||||
.ez-dag-kolom { display: flex; flex-direction: column; gap: 10px; }
|
||||
.ez-dag-kop { font-size: 16px; font-weight: 700; text-align: center; padding-bottom: 4px; border-bottom: 2px solid var(--line); }
|
||||
.ez-blok { display: flex; flex-direction: column; align-items: center; gap: 4px; border: 3px solid; border-radius: 14px; padding: 10px 8px; text-align: center; }
|
||||
.ez-tijd { font-size: 14px; font-weight: 700; color: var(--muted); }
|
||||
.ez-picto { width: 64px; height: 64px; object-fit: contain; }
|
||||
.ez-emoji { font-size: 48px; line-height: 1; }
|
||||
.ez-vak { font-size: 16px; font-weight: 800; }
|
||||
.ez-vrij { text-align: center; font-weight: 700; color: var(--muted); padding: 10px; background: var(--bg); border-radius: 10px; }
|
||||
|
||||
/* Leerlingmodus: groot, één dag */
|
||||
.ez-dag-groot { max-width: 760px; margin: 0 auto; }
|
||||
.ez-dag-nav { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
|
||||
.ez-dag-titel { font-size: 24px; font-weight: 800; }
|
||||
.btn.groot { font-size: 24px; padding: 8px 20px; }
|
||||
.ez-dag-blokken { display: flex; flex-direction: column; gap: 14px; }
|
||||
.ez-dag-blokken .ez-blok.groot { flex-direction: row; justify-content: flex-start; gap: 18px; padding: 16px 20px; border-width: 4px; }
|
||||
.ez-blok.groot .ez-tijd { font-size: 22px; min-width: 70px; }
|
||||
.ez-blok.groot .ez-picto { width: 84px; height: 84px; }
|
||||
.ez-blok.groot .ez-emoji { font-size: 64px; }
|
||||
.ez-blok.groot .ez-vak { font-size: 26px; }
|
||||
.ez-vrij.groot { font-size: 22px; padding: 30px; }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export default defineConfig({
|
|||
port: 5173,
|
||||
proxy: {
|
||||
"/api": "http://localhost:8000",
|
||||
"/media": "http://localhost:8000",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue