Rooster gesplitst in weergave + Roostermaker, printmodule afgebouwd (subtiele Afdrukken-knop met print-CSS)

This commit is contained in:
bes-r 2026-06-12 23:18:50 +02:00
parent 3318ae601e
commit e8e0022359
7 changed files with 89 additions and 30 deletions

View file

@ -18,6 +18,12 @@ rustiger is). De zijbalk groepeert items onder inklapbare submenu's (Personen,
Groepen) en heeft onderaan een subtiel **Instellingen**-blok met Gebruikers en
Modulebeheer.
Er zijn twee rooster-schermen: **Rooster** (alleen-lezen weergave, om te bekijken
en af te drukken) en **Roostermaker** (de aanpasser met drag-and-drop, tabs en
conflictdetectie). De module **Printen/Exporteren** voegt — als die aanstaat — een
subtiele **Afdrukken**-knop toe aan beide; die drukt het weekrooster
printvriendelijk af (of bewaart als PDF via de printdialoog).
Optionele module **Leerplein**: koppelt subgroepen aan groepen en bundelt
groepen, subgroepen én losse leerlingen onder een overkoepelend leerplein.
Optionele module **Stage**: interne en externe stages van leerlingen — beide met

View file

@ -48,6 +48,22 @@ volgt.
(de betreffende blokken vervallen voor die leerling).
- [ ] Stage instelbaar **per blok, per dagdeel of per dag**.
## Module: Urenoverzicht
- [ ] **Module die berekent hoeveel uur een groep per vak krijgt.**
- [ ] Optie om dit **per leerling** te tonen, **inclusief stage-uren**.
- [ ] **Geschiedenis** van dit overzicht **per leerling** tonen (verloop over de
tijd).
- [ ] **Vrije dagen tellen niet mee** in deze urenberekening.
## Roostermaker — vrije dagen & blok vervallen
- [ ] **Vrije/vakantie-/studiedagen kunnen niet gebruikt worden** in de
Roostermaker (geen blokken plannen op die dagen).
- [ ] De actie **"Deze dag laten vervallen"** subtieler wegwerken **en de tekst
corrigeren** — het is per *blok*, niet per dag.
- [ ] Mogelijkheid om **een hele dag op vervallen** te zetten (i.p.v. per blok).
## Later
- [ ] **Module: automatisch rooster genereren.** De gebruiker geeft vereisten/

View file

@ -27,8 +27,9 @@ class CoreConfig(AppConfig):
category="Kern",
core=True,
menu_items=(
# Hoofdniveau (Schooljaar zit nu als tab in het Rooster)
# Hoofdniveau: Rooster = weergave, Roostermaker = aanpasser.
MenuItem("Rooster", "/rooster", icon="calendar", order=10),
MenuItem("Roostermaker", "/roostermaker", icon="calendar", order=12),
# Submenu Personen
MenuItem("Personen", "/personen", icon="users", order=20, group="Personen"),
MenuItem("Functies", "/functies", icon="badge", order=25, group="Personen"),

View file

@ -15,15 +15,16 @@ class PrintingConfig(AppConfig):
key="printing",
name="Printen / Exporteren",
description=(
"Roosters afdrukken of als PDF exporteren, per groep, "
"leerling of leerkracht."
"Voegt een subtiele 'Afdrukken'-knop toe aan het Rooster en de "
"Roostermaker, die het weekrooster printvriendelijk afdrukt "
"(of als PDF bewaart via de printdialoog)."
),
version="0.1.0",
category="Uitvoer",
core=False,
default_enabled=True,
menu_items=(
MenuItem("Afdrukken", "/afdrukken", icon="printer", order=200),
),
# Geen eigen menu-item: de knop verschijnt in de rooster-schermen
# zodra deze module aanstaat.
menu_items=(),
)
)

View file

@ -3,7 +3,8 @@ import { getMe, login, logout, getModules, setModuleState } from "./api.js";
import Logo from "./Logo.jsx";
import {
GroepenPage, PersonenPage, SubgroepenPage, FunctiesPage,
ActiviteitenPage, LocatiesPage, TijdslotenPage, SchooljaarPage, RoosterPage,
ActiviteitenPage, LocatiesPage, TijdslotenPage, SchooljaarPage,
RoosterWeergavePage, RoostermakerPage,
AfwezigheidPage, LeerpleinPage, StagePage,
} from "./pages.jsx";
@ -16,7 +17,8 @@ const ICONS = {
// Koppel een menu-pad aan een echt scherm. Onbekende paden tonen een
// placeholder (worden in een latere fase ingevuld).
const PAGES = {
"/rooster": RoosterPage,
"/rooster": RoosterWeergavePage,
"/roostermaker": RoostermakerPage,
"/personen": PersonenPage,
"/functies": FunctiesPage,
"/groepen": GroepenPage,

View file

@ -4,6 +4,7 @@ import {
createResource,
updateResource,
deleteResource,
getModules,
} from "./api.js";
const ROLLEN = [
@ -1013,9 +1014,12 @@ function maandagVan(d) { const x = new Date(d); x.setHours(0, 0, 0, 0); const wd
function plusDagen(d, n) { const x = new Date(d); x.setDate(x.getDate() + n); return x; }
/* ---------------------------------- Rooster ------------------------------- */
// Weekweergave met effectief rooster (vrije dagen, afwezigheid, uitzonderingen)
// en drag-and-drop (muis + touch) om een blok naar een ander tijdslot te slepen.
export function RoosterPage() {
// Eén component met twee modi:
// - mode="view" -> Rooster (alleen-lezen weergave, om te bekijken/printen)
// - mode="edit" -> Roostermaker (volledige bewerking + drag-and-drop)
export function RoosterScherm({ mode }) {
const edit = mode === "edit";
const [printAan, setPrintAan] = useState(false);
const [schooljaren, setSchooljaren] = useState([]);
const [schooljaar, setSchooljaar] = useState("");
const [tijdsloten, setTijdsloten] = useState([]);
@ -1052,6 +1056,8 @@ export function RoosterPage() {
listResource("subgroepen").then(setSubgroepen).catch(() => {});
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
listResource("personen").then((ps) => setMedewerkers(ps.filter((p) => p.rol !== "leerling"))).catch(() => {});
// Afdrukken alleen tonen als de print-module aanstaat.
getModules().then((ms) => setPrintAan(ms.some((m) => m.key === "printing" && m.enabled))).catch(() => {});
}, []);
const weekDatums = [0, 1, 2, 3, 4].map((i) => plusDagen(maandag, i));
@ -1140,12 +1146,16 @@ export function RoosterPage() {
const persoonAfwezig = (id, iso) => afwezig.some((a) => a.persoon === id && a.start_datum <= iso && a.eind_datum >= iso);
return (
<section>
<h1>Rooster</h1>
<p className="muted">Sleep een blok naar een ander tijdslot om het te verplaatsen. Vrije dagen, afwezigheid en uitzonderingen worden meegenomen.</p>
<section className="rooster-scherm">
<h1>{edit ? "Roostermaker" : "Rooster"}</h1>
<p className="muted no-print">
{edit
? "Sleep een blok naar een ander tijdslot om het te verplaatsen. Vrije dagen, afwezigheid en uitzonderingen worden meegenomen."
: "Bekijk het weekrooster. Vrije dagen, afwezigheid en uitzonderingen zijn verwerkt."}
</p>
<Foutmelding msg={error} />
<div className="rooster-balk">
<div className="rooster-balk no-print">
<label className="veld">Schooljaar
<select value={schooljaar} onChange={(e) => setSchooljaar(e.target.value)}>
<option value=""> kies </option>
@ -1160,18 +1170,23 @@ export function RoosterPage() {
<span className="week-label">week van {fmtDM(maandag)}</span>
</div>
)}
{schooljaar && tab === "week" && <button className="btn primary" onClick={() => setToonForm((v) => !v)}>{toonForm ? "Sluiten" : "+ Blok"}</button>}
{edit && schooljaar && tab === "week" && <button className="btn primary" onClick={() => setToonForm((v) => !v)}>{toonForm ? "Sluiten" : "+ Blok"}</button>}
{printAan && schooljaar && (edit ? tab === "week" : true) && (
<button className="btn print-knop" title="Rooster afdrukken" onClick={() => window.print()}>🖨 Afdrukken</button>
)}
</div>
<div className="tabbar">
<button className={tab === "week" ? "tab on" : "tab"} onClick={() => setTab("week")}>Weekrooster</button>
<button className={tab === "conflicten" ? "tab on" : "tab"} onClick={() => setTab("conflicten")}>Conflicten</button>
<button className={tab === "schooljaar" ? "tab on" : "tab"} onClick={() => setTab("schooljaar")}>Schooljaar</button>
<button className={tab === "instellingen" ? "tab on" : "tab"} onClick={() => setTab("instellingen")}>Instellingen</button>
</div>
{edit && (
<div className="tabbar no-print">
<button className={tab === "week" ? "tab on" : "tab"} onClick={() => setTab("week")}>Weekrooster</button>
<button className={tab === "conflicten" ? "tab on" : "tab"} onClick={() => setTab("conflicten")}>Conflicten</button>
<button className={tab === "schooljaar" ? "tab on" : "tab"} onClick={() => setTab("schooljaar")}>Schooljaar</button>
<button className={tab === "instellingen" ? "tab on" : "tab"} onClick={() => setTab("instellingen")}>Instellingen</button>
</div>
)}
{schooljaar && tab === "week" && toonForm && (
<form className="form-card" onSubmit={opslaan}>
{edit && schooljaar && tab === "week" && toonForm && (
<form className="form-card no-print" onSubmit={opslaan}>
<div className="form-row">
<select value={form.tijdslot} required onChange={(e) => setForm({ ...form, tijdslot: e.target.value })}>
<option value=""> tijdslot </option>
@ -1244,9 +1259,9 @@ export function RoosterPage() {
style={{ borderLeftColor: b.activiteit_kleur }}
>
<div className="rb-top">
<span className="rb-greep" onPointerDown={(e) => startDrag(e, b)} title="Sleep naar ander tijdslot"></span>
{edit && <span className="rb-greep" onPointerDown={(e) => startDrag(e, b)} title="Sleep naar ander tijdslot"></span>}
<strong>{b.activiteit_naam}</strong>
<button className="link danger rb-x" onClick={() => verwijder(b.id)} title="Blok verwijderen">×</button>
{edit && <button className="link danger rb-x" onClick={() => verwijder(b.id)} title="Blok verwijderen">×</button>}
</div>
<div className="small">🎯 {b.doel_naam} <span className="muted">({b.doel_type})</span></div>
{b.locatie_naam && <div className="small">📍 {b.locatie_naam}</div>}
@ -1256,10 +1271,10 @@ export function RoosterPage() {
))}</div>
)}
{kal && <div className="small rb-status">Vervalt {kal.type_label}</div>}
{!kal && uit && uit.vervalt && <div className="small rb-status">Vervalt {uit.opmerking ? `(${uit.opmerking})` : ""} · <button className="link" onClick={() => herstel(uit.id)}>herstel</button></div>}
{!kal && uit && uit.vervalt && <div className="small rb-status">Vervalt {uit.opmerking ? `(${uit.opmerking})` : ""}{edit && <> · <button className="link" onClick={() => herstel(uit.id)}>herstel</button></>}</div>}
{!kal && uit && !uit.vervalt && uit.vervanger_naam && <div className="small rb-verv"> Vervanger: {uit.vervanger_naam}</div>}
{!kal && !uit && (
<button className="link danger small rb-verval" onClick={() => laatVervallen(b, iso)}>deze dag laten vervallen</button>
{edit && !kal && !uit && (
<button className="link danger small rb-verval no-print" onClick={() => laatVervallen(b, iso)}>deze dag laten vervallen</button>
)}
</div>
);
@ -1291,7 +1306,11 @@ export function RoosterPage() {
</div>
)}
{ghost && <div className="drag-ghost" style={{ left: ghost.x + 10, top: ghost.y + 10 }}>{ghost.label}</div>}
{edit && ghost && <div className="drag-ghost" style={{ left: ghost.x + 10, top: ghost.y + 10 }}>{ghost.label}</div>}
</section>
);
}
// Twee schermen op basis van dezelfde component.
export function RoosterWeergavePage() { return <RoosterScherm mode="view" />; }
export function RoostermakerPage() { return <RoosterScherm mode="edit" />; }

View file

@ -147,6 +147,20 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
.drag-ghost { position: fixed; z-index: 1000; pointer-events: none; background: var(--brand); color: #fff; padding: 6px 12px; border-radius: 8px; font-size: 13px; font-weight: 600; box-shadow: 0 6px 18px rgba(0,0,0,.25); }
/* --- Afdrukken --- */
.print-knop { margin-left: auto; background: #fff; }
@media print {
.sidebar, .no-print, .tabbar, .rooster-balk, .banner,
.rb-greep, .rb-x, .rb-verval, .print-knop, .drag-ghost { display: none !important; }
body { background: #fff; }
.app { display: block; }
.content { padding: 0; }
.rooster-scherm h1 { margin: 0 0 8px; }
.rooster-grid { grid-template-columns: repeat(5, 1fr); gap: 4px; }
.rooster-kolom { border-color: #999; }
.rooster-blok { break-inside: avoid; }
}
/* --- Onderkant zijbalk: subtiel Instellingen-blok --- */
.sidebar-foot { margin-top: auto; padding-top: 10px; border-top: 1px solid var(--line); }
.nav-group-head.subtiel { color: var(--muted); opacity: .8; }