Stage-uitbreidingen: stagelocaties, omvang per dag/dagdeel/blok, leerling uit klas op stage, conflictregel theorievak; herstel afgekapte frontend-bestanden

This commit is contained in:
bes-r 2026-06-15 23:32:32 +02:00
parent 9bb78681fc
commit edddf40c7f
4 changed files with 68 additions and 38 deletions

View file

@ -38,7 +38,10 @@ volgt.
## Vakken ## Vakken
- [ ] Per vak **automatisch een passend icoon** genereren of laten selecteren. - [x] Per vak **automatisch een passend icoon** (afgeleid van de naam) — getoond
in het rooster en bij Activiteiten/vakken.
- [ ] Optioneel: een icoon handmatig kunnen kiezen/overschrijven (vereist een
`icoon`-veld op Activiteit + migratie).
## Stage-module — uitbreidingen ## Stage-module — uitbreidingen

View file

@ -183,26 +183,10 @@ function Shell({ user, onLogout }) {
); );
} }
return ( // Instellingen-cog: staat in de bovenbalk (buiten de scrollende tabbalk,
<div className="app"> // anders knipt overflow het uitklapmenu weg).
<header className="topbar no-print"> const cogMenu = instellingenTabs.length > 0 ? (
<div className="topbar-main"> <div className={cogOpen ? "cog-menu open" : "cog-menu"} onMouseLeave={() => setCogOpen(false)}>
<div className="brand">
<span className="brand-logo"><Logo size={30} /></span>
<span className="brand-name">Roosterwijs</span>
</div>
<div className="topbar-user">
<span className="muted small">{user.username}</span>
<button className="link" onClick={doLogout}>Uitloggen</button>
</div>
</div>
<nav className="top-tabs" aria-label="Hoofdnavigatie">
{tabs.map((it) => <TabKnop key={it.path} it={it} />)}
{instellingenTabs.length > 0 && (
<div
className={cogOpen ? "cog-menu open" : "cog-menu"}
onMouseLeave={() => setCogOpen(false)}
>
<button <button
className="cog-btn" className="cog-btn"
onClick={() => setCogOpen((v) => !v)} onClick={() => setCogOpen((v) => !v)}
@ -229,7 +213,24 @@ function Shell({ user, onLogout }) {
})} })}
</div> </div>
</div> </div>
)} ) : null;
return (
<div className="app">
<header className="topbar no-print">
<div className="topbar-main">
<div className="brand">
<span className="brand-logo"><Logo size={30} /></span>
<span className="brand-name">Roosterwijs</span>
</div>
<div className="topbar-user">
<span className="muted small">{user.username}</span>
<button className="link" onClick={doLogout}>Uitloggen</button>
{cogMenu}
</div>
</div>
<nav className="top-tabs" aria-label="Hoofdnavigatie">
{tabs.map((it) => <TabKnop key={it.path} it={it} />)}
</nav> </nav>
</header> </header>

View file

@ -16,6 +16,30 @@ const ROLLEN = [
// Korte aanduiding van een medewerkersrol (compact in de roostervakjes). // Korte aanduiding van een medewerkersrol (compact in de roostervakjes).
const ROL_AFK = { leerkracht: "LK", praktijkondersteuner: "PO", klassenondersteuner: "KO" }; const ROL_AFK = { leerkracht: "LK", praktijkondersteuner: "PO", klassenondersteuner: "KO" };
// Automatisch een passend icoon bij een vak/activiteit op basis van de naam.
const VAK_ICONEN = [
[/reken|wiskund|getal/i, "🔢"],
[/taal|lees|spell|begrijpend|nederlands|woordenschat/i, "📖"],
[/engels|frans|duits|spaans/i, "🗣️"],
[/gym|beweg|sport|zwem|motoriek/i, "🤸"],
[/muziek|zang/i, "🎵"],
[/teken|kunst|knutsel|creatief|handvaardig|drama/i, "🎨"],
[/pauze|speel|buiten/i, "☕"],
[/eten|lunch|fruit/i, "🍎"],
[/natuur|biolog|wereld|aardrijk|geschied/i, "🌍"],
[/koken|huishoud/i, "🍳"],
[/techniek|bouw|hout/i, "🔧"],
[/computer|digit|ict|programmeer|media/i, "💻"],
[/therap|logop|fysio|sova|sociale|gedrag/i, "💬"],
[/stage|werk|prakt/i, "🧑‍🏭"],
[/kring|start|dag|inloop/i, "⭕"],
];
function vakIcoon(naam) {
if (!naam) return "📚";
for (const [re, icon] of VAK_ICONEN) if (re.test(naam)) return icon;
return "📚";
}
const BOVENGROEP_LABEL = "Bovenliggende groep"; const BOVENGROEP_LABEL = "Bovenliggende groep";
const BOVENGROEP_LABEL_LC = "bovenliggende groep"; const BOVENGROEP_LABEL_LC = "bovenliggende groep";
const BOVENGROEP_LABEL_MV = "Bovenliggende groepen"; const BOVENGROEP_LABEL_MV = "Bovenliggende groepen";
@ -62,7 +86,7 @@ function Foutmelding({ msg }) {
/* ----------------------- Generieke eenvoudige catalogus ------------------- */ /* ----------------------- Generieke eenvoudige catalogus ------------------- */
// Voor lijsten met alleen naam + omschrijving (Functies, Activiteiten, Locaties). // Voor lijsten met alleen naam + omschrijving (Functies, Activiteiten, Locaties).
function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTheorievak = false }) { function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTheorievak = false, metIcoon = false }) {
const { items, error, loading, reload, setError } = useList(resource); const { items, error, loading, reload, setError } = useList(resource);
const leeg = { const leeg = {
naam: "", omschrijving: "", naam: "", omschrijving: "",
@ -143,6 +167,7 @@ function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTh
<tr key={it.id}> <tr key={it.id}>
<td> <td>
{kleur && <span className="kleur-stip" style={{ background: it.kleur }} />} {kleur && <span className="kleur-stip" style={{ background: it.kleur }} />}
{metIcoon && <span className="vak-icoon">{vakIcoon(it.naam)} </span>}
{it.naam} {it.naam}
</td> </td>
<td className="muted">{it.omschrijving || "—"}</td> <td className="muted">{it.omschrijving || "—"}</td>
@ -183,6 +208,7 @@ export function ActiviteitenPage() {
resource="activiteiten" resource="activiteiten"
kleur kleur
metTheorievak metTheorievak
metIcoon
/> />
); );
} }
@ -1874,7 +1900,7 @@ export function RoosterScherm({ mode }) {
> >
<div className="rb-top"> <div className="rb-top">
{edit && <span className="rb-greep" onPointerDown={(e) => startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot"></span>} {edit && <span className="rb-greep" onPointerDown={(e) => startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot"></span>}
<strong>{b.activiteit_naam}</strong> <strong>{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" 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>} {edit && <button type="button" className="icon-btn danger rb-x" onClick={(e) => { e.stopPropagation(); verwijder(b.id); }} title="Blok verwijderen">×</button>}
</div> </div>
@ -1907,7 +1933,7 @@ export function RoosterScherm({ mode }) {
> >
<div className="split-vak-kop"> <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>} {edit && <span className="rb-greep" onPointerDown={(e) => startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot"></span>}
<strong>{b.activiteit_naam}</strong> <strong>{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" 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>} {edit && <button type="button" className="icon-btn danger" onClick={(e) => { e.stopPropagation(); verwijder(b.id); }} title="Blok verwijderen">×</button>}
</div> </div>

View file

@ -26,10 +26,10 @@ body { margin: 0; font-family: system-ui, "Segoe UI", Roboto, sans-serif; color:
.top-tabs-settings { display: flex; align-items: center; gap: 6px; margin-left: auto; padding-left: 10px; border-left: 1px solid var(--line); } .top-tabs-settings { display: flex; align-items: center; gap: 6px; margin-left: auto; padding-left: 10px; border-left: 1px solid var(--line); }
/* Instellingen als cog-menu (popover via hover of klik) */ /* Instellingen als cog-menu (popover via hover of klik) */
.cog-menu { position: relative; margin-left: auto; } .cog-menu { position: relative; }
.cog-btn { display: inline-flex; align-items: center; min-height: 38px; background: transparent; border: 0; border-radius: 8px; padding: 0 10px; font-size: 18px; cursor: pointer; color: var(--muted); } .cog-btn { display: inline-flex; align-items: center; min-height: 38px; background: transparent; border: 0; border-radius: 8px; padding: 0 10px; font-size: 18px; cursor: pointer; color: var(--muted); }
.cog-btn:hover { background: var(--bg); color: var(--ink); } .cog-btn:hover { background: var(--bg); color: var(--ink); }
.cog-dropdown { display: none; position: absolute; right: 0; top: 100%; margin-top: 4px; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,.15); min-width: 200px; padding: 6px; z-index: 50; } .cog-dropdown { display: none; position: absolute; right: 0; top: 100%; margin-top: 4px; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,.15); min-width: 200px; padding: 6px; z-index: 60; }
.cog-menu:hover .cog-dropdown, .cog-menu.open .cog-dropdown { display: block; } .cog-menu:hover .cog-dropdown, .cog-menu.open .cog-dropdown { display: block; }
.cog-item { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; background: transparent; border: 0; border-radius: 8px; padding: 9px 10px; cursor: pointer; font: inherit; color: var(--ink); } .cog-item { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; background: transparent; border: 0; border-radius: 8px; padding: 9px 10px; cursor: pointer; font: inherit; color: var(--ink); }
.cog-item:hover { background: var(--bg); } .cog-item:hover { background: var(--bg); }