diff --git a/frontend/src/pages.jsx b/frontend/src/pages.jsx index 5fe9e9d..9f0d98a 100644 --- a/frontend/src/pages.jsx +++ b/frontend/src/pages.jsx @@ -1333,23 +1333,29 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) { try { await deleteResource("roosterblokken", blok.id); await onKlaar(); } catch (e) { setError(e.message); } } return ( -
-
e.stopPropagation()}> -

Blok bewerken

- -
- - - -
-
+
e.stopPropagation()}> +
+ Bewerken + +
+ +
+ + + + setF({ ...f, opmerkingen: e.target.value })} /> +
+ +
+ Doelgroep en begeleiding +
+ {f.doel_type === "subgroep" && ( + <> + + setNieuweSub(e.target.value)} /> + + + )}
- {f.doel_type === "subgroep" && ( -
- - setNieuweSub(e.target.value)} /> - -
- )} -
-
Begeleiders:
-
- {medewerkers.map((m) => )} -
+
+ {medewerkers.map((m) => )}
-
setF({ ...f, opmerkingen: e.target.value })} />
-
- - -
-
-
Groep → subgroep: deel dit vak op onder {splitGroepNaam} (zelfde tijd):
-
+
+ +
+ + +
+ +
+ Splitsen onder {splitGroepNaam} +
{splitOpties.map((s) => )} {splitOpties.length === 0 && Nog geen subgroepen onder deze groep.}
-
+ {splitSubs.length > 0 && ( +
+
{splitGroepNaam}
+
+ {splitSubs.map((sid) => { + const sub = splitOpties.find((s) => s.id === sid) || subs.find((s) => s.id === sid); + return {sub ? sub.naam : "Subgroep"}; + })} +
+
+ )} +
setSplitNieuw(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); maakSplitSub(); } }} /> - -
- -
- +
+ + +
-
-
Kopiëren naar andere tijden/dagen:
-
+
+ Kopiëren +
{tijdsloten.map((t) => )}
-
- -
-
+ +
); } @@ -1706,21 +1719,33 @@ export function RoosterScherm({ mode }) { // Op id vergelijken, niet op naam: twee personen kunnen dezelfde naam hebben. const persoonAfwezig = (id, iso) => afwezig.some((a) => a.persoon === id && a.start_datum <= iso && a.eind_datum >= iso); const rolAfk = (id) => { const m = medewerkers.find((x) => x.id === id); return m ? (ROL_AFK[m.rol] || "") : ""; }; + const editorCtx = { activiteiten, locaties, groepen, subgroepen, leerlingen, medewerkers, tijdsloten }; + const isOpenBlok = (b) => edit && bewerkBlok && bewerkBlok.id === b.id; + const toggleOpenBlok = (b) => setBewerkBlok((huidig) => (huidig && huidig.id === b.id ? null : b)); + const inlineBewerker = (b) => isOpenBlok(b) && ( + setBewerkBlok(null)} + onKlaar={async () => { setBewerkBlok(null); await laadBlokken(); }} + /> + ); function renderRoosterBlok(b, iso, kal) { const uit = uitzOp(b.id, iso); const vervallen = !!kal || (uit && uit.vervalt); return (
setBewerkBlok(b) : undefined} + onClick={edit ? () => toggleOpenBlok(b) : undefined} title={edit ? "Klik om te bewerken" : undefined} >
{edit && startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot">⠿} {b.activiteit_naam} - {edit && } + {edit && } + {edit && }
Doel: {b.doel_naam} ({b.doel_type})
{b.locatie_naam &&
Locatie: {b.locatie_naam}
} @@ -1730,11 +1755,38 @@ export function RoosterScherm({ mode }) { ))}
)} {kal &&
Vervalt - {kal.type_label}
} - {!kal && uit && uit.vervalt &&
Vervalt {uit.opmerking ? `(${uit.opmerking})` : ""}{edit && <> · }
} + {!kal && uit && uit.vervalt &&
Vervalt {uit.opmerking ? `(${uit.opmerking})` : ""}{edit && }
} {!kal && uit && !uit.vervalt && uit.vervanger_naam &&
Vervanger: {uit.vervanger_naam}
} {edit && !kal && !uit && ( - + )} + {inlineBewerker(b)} +
+ ); + } + function renderCompactBlok(b, iso, kal) { + const uit = uitzOp(b.id, iso); + const vervallen = !!kal || (uit && uit.vervalt); + return ( +
toggleOpenBlok(b) : undefined} + title={edit ? "Klik om te bewerken" : undefined} + > +
+ {edit && startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot">⠿} + {b.activiteit_naam} + {edit && } + {edit && } +
+
+ {b.locatie_naam && {b.locatie_naam}} + {b.begeleiders_namen.length > 0 && {b.begeleiders_namen.map((x) => x.naam).join(", ")}} + {vervallen && Vervalt} +
+ {inlineBewerker(b)}
); } @@ -1750,16 +1802,24 @@ export function RoosterScherm({ mode }) { const out = losse.map((b) => renderRoosterBlok(b, iso, kal)); if (scopeType === "leerplein" && scopeId && perGroep.size > 0) { const leerplein = leerpleinen.find((l) => l.id === Number(scopeId)); + const kolommen = [...perGroep.entries()]; out.push(
-
{leerplein ? leerplein.naam : "Leerplein"}
+
+ +
+ {leerplein ? leerplein.naam : "Leerplein"} + Leerplein → groepen +
+ {kolommen.length} +
- {[...perGroep.entries()].map(([gid, groepBlokken]) => { + {kolommen.map(([gid, groepBlokken]) => { const groep = groepen.find((g) => g.id === gid); return (
{groep ? groep.naam : "Groep"}
- {groepBlokken.map((b) => renderRoosterBlok(b, iso, kal))} + {groepBlokken.map((b) => renderCompactBlok(b, iso, kal))}
); })} @@ -1777,14 +1837,22 @@ export function RoosterScherm({ mode }) { const groep = groepen.find((g) => g.id === gid); const heleGroep = groepBlokken.filter((b) => b.groep === gid); const subIds = [...new Set(groepBlokken.filter((b) => b.subgroep).map((b) => b.subgroep))]; + const kolomAantal = subIds.length + (heleGroep.length > 0 ? 1 : 0); out.push(
-
{groep ? groep.naam : "Groep"}
+
+ +
+ {groep ? groep.naam : "Groep"} + Groep → subgroepen +
+ {kolomAantal} +
{heleGroep.length > 0 && (
Hele groep
- {heleGroep.map((b) => renderRoosterBlok(b, iso, kal))} + {heleGroep.map((b) => renderCompactBlok(b, iso, kal))}
)} {subIds.map((sid) => { @@ -1793,7 +1861,7 @@ export function RoosterScherm({ mode }) { return (
{sub ? sub.naam : "Subgroep"}
- {blocks.map((b) => renderRoosterBlok(b, iso, kal))} + {blocks.map((b) => renderCompactBlok(b, iso, kal))}
); })} @@ -1829,9 +1897,9 @@ export function RoosterScherm({ mode }) { week van {fmtDM(maandag)}
)} - {edit && schooljaar && tab === "week" && } + {edit && schooljaar && tab === "week" && } {printAan && schooljaar && (edit ? tab === "week" : true) && ( - + )}
@@ -1853,16 +1921,31 @@ export function RoosterScherm({ mode }) { )} {scopeType !== "alles" && scopeId && {zichtbareBlokken.length} blok(ken)} + {edit && groepen.length > 0 && ( +
+ + {groepen.map((g) => ( + + ))} +
+ )}
)} {edit && (
- - - - - + + + + +
)} {edit && tab === "week" && ( @@ -1923,8 +2006,8 @@ export function RoosterScherm({ mode }) { {d.label} {fmtDM(datum)} {kal &&
{kal.type_label}{kal.omschrijving ? `: ${kal.omschrijving}` : ""}
} {edit && (kal - ? - : + ? + : )}
{slots.length === 0 &&
} @@ -1979,15 +2062,6 @@ export function RoosterScherm({ mode }) {
)} - {edit && bewerkBlok && ( - setBewerkBlok(null)} - onKlaar={async () => { setBewerkBlok(null); await laadBlokken(); }} - /> - )} - {edit && ghost &&
{ghost.label}
} ); diff --git a/frontend/src/styles.css b/frontend/src/styles.css index c74ee07..29d2360 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -114,11 +114,13 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px; .tag.dag-vrije_dag { background: #fee2e2; color: #b91c1c; } .tag.dag-vakantie { background: #dcfce7; color: #166534; } -.rooster-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-top: 20px; } +.rooster-grid { display: grid; grid-template-columns: repeat(5, minmax(220px, 1fr)); gap: 12px; margin-top: 20px; } .rooster-kolom { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 8px; min-height: 80px; } -.rooster-dag { font-weight: 600; font-size: 13px; text-align: center; padding: 6px 0; border-bottom: 1px solid var(--line); margin-bottom: 8px; } +.rooster-dag { position: relative; font-weight: 600; font-size: 13px; text-align: center; padding: 6px 32px 6px 0; border-bottom: 1px solid var(--line); margin-bottom: 8px; min-height: 36px; } .rooster-blok { border: 1px solid var(--line); border-left: 4px solid var(--brand); border-radius: 8px; padding: 8px; margin-bottom: 8px; background: #fff; } +.rooster-blok.open { box-shadow: 0 0 0 2px var(--brand-soft); } .rb-top { display: flex; justify-content: space-between; align-items: center; gap: 6px; } +.rb-top strong { min-width: 0; flex: 1; } .link.small { font-size: 16px; line-height: 1; padding: 0 4px; } @media (max-width: 900px) { .rooster-grid { grid-template-columns: 1fr; } } @@ -140,6 +142,10 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px; .rb-greep { cursor: grab; color: var(--muted); font-size: 16px; padding: 0 4px; touch-action: none; user-select: none; } .rb-greep:active { cursor: grabbing; } .rb-x { margin-left: auto; } +.icon-btn { width: 28px; height: 28px; display: inline-grid; place-items: center; border: 1px solid transparent; border-radius: 6px; background: transparent; color: var(--muted); cursor: pointer; font: inherit; line-height: 1; flex: none; } +.icon-btn:hover { background: var(--bg); color: var(--ink); border-color: var(--line); } +.icon-btn.danger { color: #b91c1c; } +.icon-label { display: inline-flex; align-items: center; gap: 6px; } .rb-status { color: #b91c1c; font-weight: 600; } .rb-verv { color: #166534; font-weight: 600; } .rb-verval { display: inline-block; margin-top: 4px; } @@ -147,7 +153,7 @@ 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); } -.dag-actie { display: block; margin-top: 4px; font-size: 11px; } +.dag-actie { position: absolute; right: 0; top: 3px; margin: 0; } .sneltoets-hint { margin: -4px 0 12px; } /* --- Groepsindeling --- */ @@ -161,14 +167,46 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px; .indeling-kop { font-weight: 600; margin-bottom: 2px; } @media (max-width: 700px) { .indeling-kolommen { grid-template-columns: 1fr; } } -.blok-splitsing { border: 1px solid var(--line); border-radius: 8px; background: #fafbff; padding: 6px; margin-bottom: 8px; } -.split-groep { font-size: 12px; font-weight: 700; color: var(--brand); margin: 0 2px 6px; } -.split-kolommen { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 6px; align-items: stretch; } -.split-kolom { min-width: 0; } +.blok-splitsing { position: relative; border: 1px solid #bfdbfe; border-radius: 8px; background: linear-gradient(180deg, #eff6ff 0, #f8fbff 42px, #fff 100%); padding: 8px; margin-bottom: 8px; } +.split-header { display: flex; align-items: center; gap: 8px; border: 1px solid #bfdbfe; border-radius: 7px; background: #fff; padding: 6px 8px; margin-bottom: 12px; } +.split-icon { width: 24px; height: 24px; display: inline-grid; place-items: center; border-radius: 6px; background: var(--brand); color: #fff; font-weight: 800; flex: none; } +.split-title { display: flex; flex-direction: column; gap: 1px; min-width: 0; line-height: 1.15; flex: 1; } +.split-title strong { font-size: 13px; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.split-title span { font-size: 11px; color: var(--muted); } +.split-count { min-width: 22px; height: 22px; display: inline-grid; place-items: center; border-radius: 999px; background: var(--brand-soft); color: var(--brand); font-size: 12px; font-weight: 800; } +.split-groep { font-size: 12px; font-weight: 800; color: var(--brand); margin: 0 2px 7px; } +.split-kolommen { position: relative; display: grid; grid-template-columns: repeat(auto-fit, minmax(135px, 1fr)); gap: 7px; align-items: start; padding-top: 12px; } +.split-kolommen::before { content: ""; position: absolute; top: 0; left: 10%; right: 10%; border-top: 2px solid #bfdbfe; } +.split-kolom { position: relative; min-width: 0; border-left: 1px solid var(--line); padding-left: 7px; } +.split-kolom::before { content: ""; position: absolute; top: -12px; left: 50%; width: 2px; height: 12px; background: #bfdbfe; } +.split-kolom:first-child { border-left: 0; padding-left: 0; } .split-kop { font-size: 11px; font-weight: 700; color: var(--muted); margin: 0 0 4px; } -.split-kolom .rooster-blok { margin-bottom: 0; } +.split-vak { position: relative; border: 1px solid var(--line); border-left: 4px solid var(--brand); border-radius: 6px; background: #fff; padding: 6px 6px 5px; margin-bottom: 6px; cursor: pointer; box-shadow: 0 1px 0 rgba(31,41,55,.03); } +.split-vak.open { box-shadow: 0 0 0 2px var(--brand-soft); } +.split-vak.vervallen { opacity: .55; } +.split-vak-kop { display: flex; align-items: center; gap: 5px; } +.split-vak-kop strong { min-width: 0; flex: 1; font-size: 12px; } +.split-vak-meta { display: flex; flex-direction: column; gap: 1px; color: var(--muted); font-size: 11px; margin-top: 2px; } +.klas-strip { display: flex; flex-wrap: wrap; gap: 6px; flex-basis: 100%; } -/* --- Blok-bewerker (modal) --- */ +/* --- Blok-bewerker --- */ +.blok-editor { margin-top: 8px; padding: 8px; border: 1px solid var(--line); border-radius: 8px; background: #f9fafb; cursor: default; } +.blok-editor-kop { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; } +.editor-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 7px; margin: 7px 0; } +.editor-grid input, .editor-grid select { width: 100%; min-width: 0; font-size: 12px; padding: 7px 8px; } +.editor-detail { border-top: 1px dashed var(--line); padding-top: 7px; margin-top: 7px; } +.editor-detail summary { cursor: pointer; color: var(--brand); font-size: 12px; font-weight: 700; } +.editor-actions { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 8px; } +.editor-check { margin: 6px 0; } +.chips.compact { gap: 5px; margin-top: 6px; } +.chips.compact .chip { font-size: 12px; padding: 5px 8px; } +.split-editor { background: #f8fbff; border: 1px solid #dbeafe; border-radius: 7px; padding: 7px; } +.split-preview { margin: 8px 0; padding: 8px; border-radius: 7px; background: #fff; border: 1px solid #dbeafe; } +.split-preview-root { display: inline-block; padding: 5px 9px; border-radius: 6px; background: var(--brand); color: #fff; font-size: 12px; font-weight: 800; } +.split-preview-children { position: relative; display: flex; flex-wrap: wrap; gap: 6px; margin-top: 13px; padding-top: 13px; } +.split-preview-children::before { content: ""; position: absolute; top: 0; left: 12px; right: 12px; border-top: 2px solid #bfdbfe; } +.split-preview-children span { position: relative; padding: 5px 8px; border-radius: 6px; background: var(--brand-soft); color: var(--brand); font-size: 12px; font-weight: 700; } +.split-preview-children span::before { content: ""; position: absolute; top: -13px; left: 50%; width: 2px; height: 13px; background: #bfdbfe; } .modal-overlay { position: fixed; inset: 0; background: rgba(15,23,42,.45); display: flex; align-items: flex-start; justify-content: center; padding: 40px 16px; z-index: 900; overflow: auto; } .modal { background: var(--panel); border-radius: 12px; padding: 18px 20px; width: min(560px, 100%); box-shadow: 0 20px 50px rgba(0,0,0,.3); } .modal-kop { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } @@ -182,7 +220,7 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px; .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; } + .rb-greep, .rb-x, .rb-verval, .icon-btn, .print-knop, .drag-ghost { display: none !important; } body { background: #fff; } .app { display: block; } .content { padding: 0; }