printopties-dialog: bereik, indeling, kleur, details, papierformaat & orientatie

This commit is contained in:
bes-r 2026-06-19 23:45:06 +02:00
parent 0ab8ecb798
commit 1b559dd2c6
3 changed files with 119 additions and 8 deletions

View file

@ -40,6 +40,12 @@ volgt.
## Fase 6 — Weergave & beheer ## Fase 6 — Weergave & beheer
- [x] **Printopties-dialog** (🖨 in Rooster/Roostermaker): kies wát (huidige
selectie / alle klassen) en hóé je afdrukt — indeling (kolommen / dagen
onder elkaar), vakken in kleur, begeleiding/lokaal tonen, optionele
koptekst — plus **papierformaat** (A4/A3/A5/Letter) en oriëntatie
(liggend/staand). Toegepast via body-classes + dynamische `@page`.
- [x] **Vakkleur als vlak** (toggle 🎨, onthouden per browser): vak volledig in de - [x] **Vakkleur als vlak** (toggle 🎨, onthouden per browser): vak volledig in de
activiteitskleur i.p.v. een streepje, met automatisch leesbare tekstkleur activiteitskleur i.p.v. een streepje, met automatisch leesbare tekstkleur
(zwart/wit op basis van helderheid). Werkt in Rooster/Roostermaker en (zwart/wit op basis van helderheid). Werkt in Rooster/Roostermaker en

View file

@ -2072,6 +2072,12 @@ export function RoosterScherm({ mode }) {
const setWeergave = (v) => { setWeergaveState(v); try { localStorage.setItem("rw_weergave", v); } catch { /* niet beschikbaar */ } }; const setWeergave = (v) => { setWeergaveState(v); try { localStorage.setItem("rw_weergave", v); } catch { /* niet beschikbaar */ } };
const [vakkleurVlak, setVakkleurState] = useState(() => { try { return localStorage.getItem("rw_vakkleur") === "vlak"; } catch { return false; } }); const [vakkleurVlak, setVakkleurState] = useState(() => { try { return localStorage.getItem("rw_vakkleur") === "vlak"; } catch { return false; } });
const setVakkleur = (v) => { setVakkleurState(v); try { localStorage.setItem("rw_vakkleur", v ? "vlak" : "streep"); } catch { /* niet beschikbaar */ } }; const setVakkleur = (v) => { setVakkleurState(v); try { localStorage.setItem("rw_vakkleur", v ? "vlak" : "streep"); } catch { /* niet beschikbaar */ } };
// Printopties
const [printOpen, setPrintOpen] = useState(false);
const [printPending, setPrintPending] = useState(false);
const [printAllesActief, setPrintAllesActief] = useState(false);
const [printKop, setPrintKop] = useState("");
const [printOpts, setPrintOpts] = useState({ bereik: "huidig", indeling: "kolom", kleur: false, begeleiding: true, lokaal: true, papier: "A4", orientatie: "landscape" });
useEffect(() => { useEffect(() => {
listResource("schooljaren").then((j) => { setSchooljaren(j); if (j.length) setSchooljaar((s) => s || String(j[0].id)); }).catch((e) => setError(e.message)); listResource("schooljaren").then((j) => { setSchooljaren(j); if (j.length) setSchooljaar((s) => s || String(j[0].id)); }).catch((e) => setError(e.message));
@ -2096,7 +2102,9 @@ export function RoosterScherm({ mode }) {
// Scope-context + gefilterde blokken (voor weergave/afdrukken). // Scope-context + gefilterde blokken (voor weergave/afdrukken).
const scopeCtx = { leerlingen, subgroepen, leerpleinen }; const scopeCtx = { leerlingen, subgroepen, leerpleinen };
const scope = { type: scopeType, id: scopeId ? Number(scopeId) : "" }; const scope = { type: scopeType, id: scopeId ? Number(scopeId) : "" };
const zichtbareBlokken = blokken.filter((b) => blokInScope(b, scope, scopeCtx)); // Bij printen "alle klassen" tijdelijk de scope negeren.
const effScope = printAllesActief ? { type: "alles", id: "" } : scope;
const zichtbareBlokken = blokken.filter((b) => blokInScope(b, effScope, scopeCtx));
// Conflicten over álle blokken (niet alleen de zichtbare scope), zodat de // Conflicten over álle blokken (niet alleen de zichtbare scope), zodat de
// melding compleet is en de betrokken blokken in het rooster omlijnd worden. // melding compleet is en de betrokken blokken in het rooster omlijnd worden.
const conflictInfo = berekenRoosterConflicten(blokken, scopeCtx, tijdsloten); const conflictInfo = berekenRoosterConflicten(blokken, scopeCtx, tijdsloten);
@ -2194,6 +2202,34 @@ export function RoosterScherm({ mode }) {
setBewerkBlok(null); setBewerkBlok(null);
setToonForm(true); setToonForm(true);
} }
// Print: pas de gekozen opties toe via body-classes + @page, dan afdrukken.
function zetPaginaStijl(papier, orientatie) {
let el = document.getElementById("rw-print-page");
if (!el) { el = document.createElement("style"); el.id = "rw-print-page"; document.head.appendChild(el); }
el.textContent = `@page { size: ${papier} ${orientatie}; margin: 10mm; }`;
}
function startPrint() {
const b = document.body.classList;
b.toggle("print-lijst", printOpts.indeling === "lijst");
b.toggle("print-kolom", printOpts.indeling === "kolom");
b.toggle("print-kleur", !!printOpts.kleur);
b.toggle("print-geen-begeleiding", !printOpts.begeleiding);
b.toggle("print-geen-lokaal", !printOpts.lokaal);
zetPaginaStijl(printOpts.papier, printOpts.orientatie);
setPrintKop(printOpts.kop || "");
if (printOpts.bereik === "alles") setPrintAllesActief(true);
setPrintOpen(false);
setPrintPending(true);
}
useEffect(() => {
if (!printPending) return;
const id = setTimeout(() => {
window.print();
setPrintPending(false);
setPrintAllesActief(false);
}, 120);
return () => clearTimeout(id);
}, [printPending]);
// Direct vanuit een tijdvak in het rooster een vak toevoegen: tijdslot vast, // Direct vanuit een tijdvak in het rooster een vak toevoegen: tijdslot vast,
// en het doel alvast op de actieve scope (klas/leerling) zetten. // en het doel alvast op de actieve scope (klas/leerling) zetten.
function openNieuwBlokVoorSlot(tijdslotId) { function openNieuwBlokVoorSlot(tijdslotId) {
@ -2338,7 +2374,7 @@ export function RoosterScherm({ mode }) {
<div <div
className={"vak-kaart" + (vervallen ? " vervallen" : "") + (edit ? " klikbaar" : "") + (isOpenBlok(b) ? " open" : "") + (conflictInfo.blokIds.has(b.id) ? " in-conflict" : "") + (vakkleurVlak ? " kleur-vlak" : "")} className={"vak-kaart" + (vervallen ? " vervallen" : "") + (edit ? " klikbaar" : "") + (isOpenBlok(b) ? " open" : "") + (conflictInfo.blokIds.has(b.id) ? " in-conflict" : "") + (vakkleurVlak ? " kleur-vlak" : "")}
key={b.id} key={b.id}
style={vakkleurVlak ? { borderLeftColor: b.activiteit_kleur, background: b.activiteit_kleur, color: tekstOpKleur(b.activiteit_kleur) } : { borderLeftColor: b.activiteit_kleur }} style={{ borderLeftColor: b.activiteit_kleur, "--vk-kleur": b.activiteit_kleur, "--vk-tekst": tekstOpKleur(b.activiteit_kleur) }}
onClick={edit ? () => toggleOpenBlok(b) : undefined} onClick={edit ? () => toggleOpenBlok(b) : undefined}
title={edit ? "Klik om te bewerken" : undefined} title={edit ? "Klik om te bewerken" : undefined}
> >
@ -2351,10 +2387,10 @@ export function RoosterScherm({ mode }) {
</div> </div>
{(b.locatie_naam || b.lokaal_naam || b.begeleiders_namen.length > 0) && ( {(b.locatie_naam || b.lokaal_naam || b.begeleiders_namen.length > 0) && (
<div className="vk-meta"> <div className="vk-meta">
{b.locatie_naam && <span className="vk-tag">📍 {b.locatie_naam}</span>} {b.locatie_naam && <span className="vk-tag t-loc">📍 {b.locatie_naam}</span>}
{b.lokaal_naam && <span className="vk-tag">🚪 {b.lokaal_naam}</span>} {b.lokaal_naam && <span className="vk-tag t-lok">🚪 {b.lokaal_naam}</span>}
{b.begeleiders_namen.length > 0 && ( {b.begeleiders_namen.length > 0 && (
<span className="vk-tag">👤 {b.begeleiders_namen.map((x, idx) => ( <span className="vk-tag t-beg">👤 {b.begeleiders_namen.map((x, idx) => (
<span key={x.id} className={persoonAfwezig(x.id, iso) ? "afwezig" : ""}>{idx > 0 ? ", " : ""}{x.naam}{rolAfk(x.id) ? ` (${rolAfk(x.id)})` : ""}</span> <span key={x.id} className={persoonAfwezig(x.id, iso) ? "afwezig" : ""}>{idx > 0 ? ", " : ""}{x.naam}{rolAfk(x.id) ? ` (${rolAfk(x.id)})` : ""}</span>
))}</span> ))}</span>
)} )}
@ -2474,6 +2510,7 @@ export function RoosterScherm({ mode }) {
return ( return (
<section className="rooster-scherm"> <section className="rooster-scherm">
{printKop && <div className="print-only print-kop">{printKop}</div>}
<h1>{edit ? "Roostermaker" : "Rooster"}</h1> <h1>{edit ? "Roostermaker" : "Rooster"}</h1>
<p className="muted no-print"> <p className="muted no-print">
{edit {edit
@ -2501,7 +2538,7 @@ export function RoosterScherm({ mode }) {
<div className="weergave-opties no-print"> <div className="weergave-opties no-print">
<button type="button" className={weergave === "lijst" ? "icon-toggle on" : "icon-toggle"} aria-pressed={weergave === "lijst"} title={weergave === "kolom" ? "Dagen onder elkaar tonen" : "Dagen als kolommen tonen"} onClick={() => setWeergave(weergave === "kolom" ? "lijst" : "kolom")}>{weergave === "kolom" ? "☰" : "▦"}</button> <button type="button" className={weergave === "lijst" ? "icon-toggle on" : "icon-toggle"} aria-pressed={weergave === "lijst"} title={weergave === "kolom" ? "Dagen onder elkaar tonen" : "Dagen als kolommen tonen"} onClick={() => setWeergave(weergave === "kolom" ? "lijst" : "kolom")}>{weergave === "kolom" ? "☰" : "▦"}</button>
<button type="button" className={vakkleurVlak ? "icon-toggle on" : "icon-toggle"} aria-pressed={vakkleurVlak} title={vakkleurVlak ? "Vakkleur als streepje tonen" : "Vak volledig in vakkleur tonen"} onClick={() => setVakkleur(!vakkleurVlak)}>🎨</button> <button type="button" className={vakkleurVlak ? "icon-toggle on" : "icon-toggle"} aria-pressed={vakkleurVlak} title={vakkleurVlak ? "Vakkleur als streepje tonen" : "Vak volledig in vakkleur tonen"} onClick={() => setVakkleur(!vakkleurVlak)}>🎨</button>
{printAan && <button type="button" className="icon-toggle" title="Rooster afdrukken" onClick={() => window.print()}>🖨</button>} {printAan && <button type="button" className="icon-toggle" title="Afdrukken (met opties)" onClick={() => setPrintOpen(true)}>🖨</button>}
</div> </div>
)} )}
</div> </div>
@ -2772,6 +2809,55 @@ export function RoosterScherm({ mode }) {
/> />
)} )}
{edit && 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>}
{printOpen && (
<div className="modal-overlay no-print" onClick={() => setPrintOpen(false)}>
<div className="modal" role="dialog" aria-modal="true" aria-label="Printopties" onClick={(e) => e.stopPropagation()}>
<div className="modal-kop">
<div><h2>Afdrukken</h2><div className="muted small">Kies wat en hoe je afdrukt.</div></div>
<button type="button" className="icon-btn" title="Sluiten" onClick={() => setPrintOpen(false)}>×</button>
</div>
<div className="editor-grid">
<label className="veld-kol"><span className="veld-kop">Wat afdrukken</span>
<select value={printOpts.bereik} onChange={(e) => setPrintOpts({ ...printOpts, bereik: e.target.value })}>
<option value="huidig">Huidige selectie</option>
<option value="alles">Alle klassen</option>
</select>
</label>
<label className="veld-kol"><span className="veld-kop">Indeling</span>
<select value={printOpts.indeling} onChange={(e) => setPrintOpts({ ...printOpts, indeling: e.target.value })}>
<option value="kolom">Dagen als kolommen</option>
<option value="lijst">Dagen onder elkaar</option>
</select>
</label>
<label className="veld-kol"><span className="veld-kop">Papierformaat</span>
<select value={printOpts.papier} onChange={(e) => setPrintOpts({ ...printOpts, papier: e.target.value })}>
<option value="A4">A4</option>
<option value="A3">A3</option>
<option value="A5">A5</option>
<option value="Letter">Letter</option>
</select>
</label>
<label className="veld-kol"><span className="veld-kop">Oriëntatie</span>
<select value={printOpts.orientatie} onChange={(e) => setPrintOpts({ ...printOpts, orientatie: e.target.value })}>
<option value="landscape">Liggend</option>
<option value="portrait">Staand</option>
</select>
</label>
</div>
<label className="veld-kol"><span className="veld-kop">Koptekst (optioneel)</span><input placeholder="bv. Weekrooster groep A1" value={printOpts.kop || ""} onChange={(e) => setPrintOpts({ ...printOpts, kop: e.target.value })} /></label>
<div className="check-grid">
<label className="check-item"><input type="checkbox" checked={printOpts.kleur} onChange={(e) => setPrintOpts({ ...printOpts, kleur: e.target.checked })} /><span>Vakken in kleur</span></label>
<label className="check-item"><input type="checkbox" checked={printOpts.begeleiding} onChange={(e) => setPrintOpts({ ...printOpts, begeleiding: e.target.checked })} /><span>Begeleiding tonen</span></label>
<label className="check-item"><input type="checkbox" checked={printOpts.lokaal} onChange={(e) => setPrintOpts({ ...printOpts, lokaal: e.target.checked })} /><span>Lokaal/locatie tonen</span></label>
</div>
<div className="editor-actions">
<button type="button" className="btn primary" onClick={startPrint}>Afdrukken</button>
<button type="button" className="btn" onClick={() => setPrintOpen(false)}>Annuleren</button>
</div>
</div>
</div>
)}
</section> </section>
); );
} }

View file

@ -326,10 +326,26 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
body { background: #fff; } body { background: #fff; }
.app { display: block; } .app { display: block; }
.content { padding: 0; } .content { padding: 0; }
.print-only { display: block; }
.rooster-scherm h1 { margin: 0 0 8px; } .rooster-scherm h1 { margin: 0 0 8px; }
.rooster-grid { grid-template-columns: repeat(5, 1fr); gap: 4px; } .rooster-grid { grid-template-columns: repeat(5, 1fr); gap: 4px; }
.rooster-kolom { border-color: #999; } .rooster-kolom { border-color: #999; }
.rooster-blok { break-inside: avoid; } .rooster-blok, .vak-kaart, .blok-splitsing { break-inside: avoid; }
/* Indeling-keuze uit de printdialog */
body.print-kolom .rooster-grid { display: grid !important; grid-template-columns: repeat(5, 1fr) !important; }
body.print-lijst .rooster-grid { display: flex !important; flex-direction: column !important; }
body.print-lijst .rooster-kolom { display: flex; flex-wrap: wrap; gap: 6px; }
body.print-lijst .rooster-dag { flex: 0 0 100%; }
body.print-lijst .slot-cel { flex: 1 1 200px; max-width: 320px; }
/* Details verbergen volgens keuze */
body.print-geen-begeleiding .vk-tag.t-beg { display: none; }
body.print-geen-lokaal .vk-tag.t-lok, body.print-geen-lokaal .vk-tag.t-loc { display: none; }
/* Vakken in kleur afdrukken */
body.print-kleur .vak-kaart { background: var(--vk-kleur) !important; color: var(--vk-tekst) !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
body.print-kleur .vak-kaart .vk-naam, body.print-kleur .vak-kaart .muted { color: inherit !important; }
} }
/* --- Onderkant zijbalk: subtiel Instellingen-blok --- */ /* --- Onderkant zijbalk: subtiel Instellingen-blok --- */
@ -560,8 +576,11 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
.wm-add { width: 100%; border: 1px dashed var(--line); background: transparent; color: var(--muted); border-radius: 7px; padding: 4px; cursor: pointer; font: inherit; font-size: 13px; opacity: .6; } .wm-add { width: 100%; border: 1px dashed var(--line); background: transparent; color: var(--muted); border-radius: 7px; padding: 4px; cursor: pointer; font: inherit; font-size: 13px; opacity: .6; }
.wm-cel:hover .wm-add { opacity: 1; } .wm-cel:hover .wm-add { opacity: 1; }
.wm-add:hover { border-color: var(--brand); color: var(--brand); background: var(--brand-soft); } .wm-add:hover { border-color: var(--brand); color: var(--brand); background: var(--brand-soft); }
/* Vak volledig in de vakkleur (tekstkleur wordt inline op contrast gezet) */ /* Vak volledig in de vakkleur (kleur + leesbare tekstkleur via CSS-variabelen) */
.vak-kaart.kleur-vlak { background: var(--vk-kleur); color: var(--vk-tekst); }
.vak-kaart.kleur-vlak .vk-naam { color: inherit; } .vak-kaart.kleur-vlak .vk-naam { color: inherit; }
.print-only { display: none; }
.print-kop { font-size: 16px; font-weight: 700; margin: 0 0 8px; }
.vak-kaart.kleur-vlak .rb-greep, .vak-kaart.kleur-vlak .icon-btn { color: inherit; opacity: .85; } .vak-kaart.kleur-vlak .rb-greep, .vak-kaart.kleur-vlak .icon-btn { color: inherit; opacity: .85; }
.vak-kaart.kleur-vlak .rb-status, .vak-kaart.kleur-vlak .rb-verv { color: inherit; } .vak-kaart.kleur-vlak .rb-status, .vak-kaart.kleur-vlak .rb-verv { color: inherit; }
.wm-vak.kleur-vlak .wm-vak-naam { color: inherit; } .wm-vak.kleur-vlak .wm-vak-naam { color: inherit; }