Verbeter vak toevoegen en groepsindeling UI
This commit is contained in:
parent
18043bc147
commit
fa50438d35
5 changed files with 219 additions and 93 deletions
|
|
@ -32,7 +32,7 @@ class CoreConfig(AppConfig):
|
||||||
MenuItem("Roostermaker", "/roostermaker", icon="calendar", order=12),
|
MenuItem("Roostermaker", "/roostermaker", icon="calendar", order=12),
|
||||||
# Submenu Organisatie: wie hoort waar.
|
# Submenu Organisatie: wie hoort waar.
|
||||||
MenuItem("Personen", "/personen", icon="users", order=20, group="Organisatie"),
|
MenuItem("Personen", "/personen", icon="users", order=20, group="Organisatie"),
|
||||||
MenuItem("Structuur", "/structuur", icon="layers", order=24, group="Organisatie"),
|
MenuItem("Groepsindeling", "/structuur", icon="layers", order=24, group="Organisatie"),
|
||||||
MenuItem("Afwezigheid", "/afwezigheid", icon="away", order=28, group="Organisatie"),
|
MenuItem("Afwezigheid", "/afwezigheid", icon="away", order=28, group="Organisatie"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ class LeerpleinConfig(AppConfig):
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = "modules.leerplein"
|
name = "modules.leerplein"
|
||||||
label = "module_leerplein"
|
label = "module_leerplein"
|
||||||
verbose_name = "Module: Leerplein (groepsstructuur)"
|
verbose_name = "Module: Bovenliggende groepen"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from plugins.registry import ModuleSpec, registry
|
from plugins.registry import ModuleSpec, registry
|
||||||
|
|
@ -13,13 +13,13 @@ class LeerpleinConfig(AppConfig):
|
||||||
registry.register(
|
registry.register(
|
||||||
ModuleSpec(
|
ModuleSpec(
|
||||||
key="leerplein",
|
key="leerplein",
|
||||||
name="Leerplein (groepsstructuur)",
|
name="Bovenliggende groepen",
|
||||||
description=(
|
description=(
|
||||||
"Koppel subgroepen aan groepen en bundel groepen, subgroepen "
|
"Koppel subgroepen aan groepen en bundel groepen, subgroepen "
|
||||||
"en losse leerlingen onder een overkoepelend leerplein."
|
"en losse leerlingen onder een bovenliggende groep."
|
||||||
),
|
),
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
category="Structuur",
|
category="Groepsindeling",
|
||||||
core=False,
|
core=False,
|
||||||
default_enabled=False,
|
default_enabled=False,
|
||||||
menu_items=(),
|
menu_items=(),
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const ICONS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const STRUCTUUR_PADEN = new Set(["/structuur", "/leerplein", "/groepen", "/subgroepen"]);
|
const STRUCTUUR_PADEN = new Set(["/structuur", "/leerplein", "/groepen", "/subgroepen"]);
|
||||||
const STRUCTUUR_ITEM = { label: "Structuur", path: "/structuur", icon: "layers", order: 24, group: "Organisatie" };
|
const STRUCTUUR_ITEM = { label: "Groepsindeling", path: "/structuur", icon: "layers", order: 24, group: "Organisatie" };
|
||||||
const normaliseerPad = (path) => (STRUCTUUR_PADEN.has(path) ? "/structuur" : path);
|
const normaliseerPad = (path) => (STRUCTUUR_PADEN.has(path) ? "/structuur" : path);
|
||||||
|
|
||||||
// Koppel een menu-pad aan een echt scherm. Onbekende paden tonen een
|
// Koppel een menu-pad aan een echt scherm. Onbekende paden tonen een
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ 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" };
|
||||||
|
const BOVENGROEP_LABEL = "Bovenliggende groep";
|
||||||
|
const BOVENGROEP_LABEL_LC = "bovenliggende groep";
|
||||||
|
const BOVENGROEP_LABEL_MV = "Bovenliggende groepen";
|
||||||
|
|
||||||
const WEEKDAGEN = [
|
const WEEKDAGEN = [
|
||||||
{ value: 0, label: "Maandag" },
|
{ value: 0, label: "Maandag" },
|
||||||
|
|
@ -381,7 +384,7 @@ export function PersonenPage() {
|
||||||
</div>
|
</div>
|
||||||
{leerpleinen.length > 0 && (
|
{leerpleinen.length > 0 && (
|
||||||
<div className="leden-keuze">
|
<div className="leden-keuze">
|
||||||
<div className="muted small">Leerpleinen:</div>
|
<div className="muted small">{BOVENGROEP_LABEL_MV}:</div>
|
||||||
<div className="chips">
|
<div className="chips">
|
||||||
{leerpleinen.map((lp) => <button type="button" key={lp.id} className={form.leerpleinen.includes(lp.id) ? "chip on" : "chip"} onClick={() => toggle("leerpleinen", lp.id)}>{lp.naam}</button>)}
|
{leerpleinen.map((lp) => <button type="button" key={lp.id} className={form.leerpleinen.includes(lp.id) ? "chip on" : "chip"} onClick={() => toggle("leerpleinen", lp.id)}>{lp.naam}</button>)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -698,8 +701,8 @@ export function AfwezigheidPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------- Leerplein ------------------------------- */
|
/* ----------------------------- Groepsindeling ----------------------------- */
|
||||||
// Gezamenlijk structuurbeheer: leerplein -> groep -> subgroep.
|
// Gezamenlijk beheer: bovenliggende groep -> groep -> subgroep.
|
||||||
export function LeerpleinPage() {
|
export function LeerpleinPage() {
|
||||||
const [leerpleinen, setLeerpleinen] = useState([]);
|
const [leerpleinen, setLeerpleinen] = useState([]);
|
||||||
const [groepen, setGroepen] = useState([]);
|
const [groepen, setGroepen] = useState([]);
|
||||||
|
|
@ -745,7 +748,6 @@ export function LeerpleinPage() {
|
||||||
|
|
||||||
const toggleArray = (lijst, id) => lijst.includes(id) ? lijst.filter((x) => x !== id) : [...lijst, id];
|
const toggleArray = (lijst, id) => lijst.includes(id) ? lijst.filter((x) => x !== id) : [...lijst, id];
|
||||||
const groepNaam = (id) => groepen.find((g) => g.id === id)?.naam || "—";
|
const groepNaam = (id) => groepen.find((g) => g.id === id)?.naam || "—";
|
||||||
const leerpleinNaam = (id) => leerpleinen.find((lp) => lp.id === id)?.naam || "—";
|
|
||||||
const leerpleinVanGroep = (groepId) => leerpleinen.find((lp) => (lp.groepen || []).includes(Number(groepId)));
|
const leerpleinVanGroep = (groepId) => leerpleinen.find((lp) => (lp.groepen || []).includes(Number(groepId)));
|
||||||
const leerpleinVanSubgroep = (subgroepId) => leerpleinen.find((lp) => (lp.subgroepen || []).includes(Number(subgroepId)));
|
const leerpleinVanSubgroep = (subgroepId) => leerpleinen.find((lp) => (lp.subgroepen || []).includes(Number(subgroepId)));
|
||||||
const leerlingenVanGroep = (groepId) => leerlingen.filter((l) => l.groep === Number(groepId));
|
const leerlingenVanGroep = (groepId) => leerlingen.filter((l) => l.groep === Number(groepId));
|
||||||
|
|
@ -820,7 +822,7 @@ export function LeerpleinPage() {
|
||||||
}
|
}
|
||||||
async function opslaanGroep(e) {
|
async function opslaanGroep(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!groepForm.leerplein) { setError("Kies een bovenliggend leerplein voor deze groep."); return; }
|
if (!groepForm.leerplein) { setError(`Kies een ${BOVENGROEP_LABEL_LC} voor deze groep.`); return; }
|
||||||
try {
|
try {
|
||||||
const payload = { naam: groepForm.naam, omschrijving: groepForm.omschrijving };
|
const payload = { naam: groepForm.naam, omschrijving: groepForm.omschrijving };
|
||||||
const opgeslagen = editGroepId ? await updateResource("groepen", editGroepId, payload) : await createResource("groepen", payload);
|
const opgeslagen = editGroepId ? await updateResource("groepen", editGroepId, payload) : await createResource("groepen", payload);
|
||||||
|
|
@ -834,7 +836,7 @@ export function LeerpleinPage() {
|
||||||
async function opslaanSubgroep(e) {
|
async function opslaanSubgroep(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!subgroepForm.groep) { setError("Kies een groep waar deze subgroep onder valt."); return; }
|
if (!subgroepForm.groep) { setError("Kies een groep waar deze subgroep onder valt."); return; }
|
||||||
if (subgroepForm.ledenBron === "leerplein" && !subgroepForm.leerplein) { setError("Kies een leerplein voor leden uit het hele leerplein."); return; }
|
if (subgroepForm.ledenBron === "leerplein" && !subgroepForm.leerplein) { setError(`Kies een ${BOVENGROEP_LABEL_LC} voor leden uit de volledige ${BOVENGROEP_LABEL_LC}.`); return; }
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
naam: subgroepForm.naam,
|
naam: subgroepForm.naam,
|
||||||
|
|
@ -852,8 +854,8 @@ export function LeerpleinPage() {
|
||||||
}
|
}
|
||||||
async function verwijderLeerplein(id) {
|
async function verwijderLeerplein(id) {
|
||||||
const lp = leerpleinen.find((x) => x.id === id);
|
const lp = leerpleinen.find((x) => x.id === id);
|
||||||
if ((lp?.groepen || []).length > 0) { setError("Verplaats of verwijder eerst de groepen onder dit leerplein."); return; }
|
if ((lp?.groepen || []).length > 0) { setError(`Verplaats of verwijder eerst de groepen onder deze ${BOVENGROEP_LABEL_LC}.`); return; }
|
||||||
if (!confirm("Dit leerplein verwijderen?")) return;
|
if (!confirm(`Deze ${BOVENGROEP_LABEL_LC} verwijderen?`)) return;
|
||||||
try { await deleteResource("leerpleinen", id); await laadAlles(); } catch (err) { setError(err.message); }
|
try { await deleteResource("leerpleinen", id); await laadAlles(); } catch (err) { setError(err.message); }
|
||||||
}
|
}
|
||||||
async function verwijderGroep(id) {
|
async function verwijderGroep(id) {
|
||||||
|
|
@ -885,18 +887,18 @@ export function LeerpleinPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1>Groepenstructuur</h1>
|
<h1>Groepsindeling</h1>
|
||||||
<p className="muted">Beheer leerpleinen, groepen en subgroepen op één plek. Een groep hoort altijd onder een leerplein; een subgroep hoort altijd onder een groep.</p>
|
<p className="muted">Beheer bovenliggende groepen, groepen en subgroepen op één plek. Een groep hoort altijd onder een bovenliggende groep; een subgroep hoort altijd onder een groep.</p>
|
||||||
<Foutmelding msg={error} />
|
<Foutmelding msg={error} />
|
||||||
|
|
||||||
<form className="form-card structuur-form" onSubmit={opslaanLeerplein}>
|
<form className="form-card structuur-form" onSubmit={opslaanLeerplein}>
|
||||||
<h2>Leerplein</h2>
|
<h2>{BOVENGROEP_LABEL}</h2>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<input placeholder="Naam leerplein" value={leerpleinForm.naam} required onChange={(e) => setLeerpleinForm({ ...leerpleinForm, naam: e.target.value })} />
|
<input placeholder={`Naam ${BOVENGROEP_LABEL_LC}`} value={leerpleinForm.naam} required onChange={(e) => setLeerpleinForm({ ...leerpleinForm, naam: e.target.value })} />
|
||||||
<input placeholder="Omschrijving (optioneel)" value={leerpleinForm.omschrijving} onChange={(e) => setLeerpleinForm({ ...leerpleinForm, omschrijving: e.target.value })} />
|
<input placeholder="Omschrijving (optioneel)" value={leerpleinForm.omschrijving} onChange={(e) => setLeerpleinForm({ ...leerpleinForm, omschrijving: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
<div className="leden-keuze">
|
<div className="leden-keuze">
|
||||||
<div className="muted small">Losse leerlingen op leerpleinniveau:</div>
|
<div className="muted small">Losse leerlingen op niveau van de {BOVENGROEP_LABEL_LC}:</div>
|
||||||
<Vinkjes items={leerlingen} geselecteerd={leerpleinForm.leerlingen} toggle={(id) => setLeerpleinForm((f) => ({ ...f, leerlingen: toggleArray(f.leerlingen, id) }))} />
|
<Vinkjes items={leerlingen} geselecteerd={leerpleinForm.leerlingen} toggle={(id) => setLeerpleinForm((f) => ({ ...f, leerlingen: toggleArray(f.leerlingen, id) }))} />
|
||||||
</div>
|
</div>
|
||||||
<div className="leden-keuze">
|
<div className="leden-keuze">
|
||||||
|
|
@ -904,7 +906,7 @@ export function LeerpleinPage() {
|
||||||
<Vinkjes items={medewerkers} geselecteerd={leerpleinForm.medewerkers} toggle={(id) => setLeerpleinForm((f) => ({ ...f, medewerkers: toggleArray(f.medewerkers, id) }))} leeg="Geen medewerkers gevonden." />
|
<Vinkjes items={medewerkers} geselecteerd={leerpleinForm.medewerkers} toggle={(id) => setLeerpleinForm((f) => ({ ...f, medewerkers: toggleArray(f.medewerkers, id) }))} leeg="Geen medewerkers gevonden." />
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<button className="btn primary" type="submit">{editLeerpleinId ? "Leerplein opslaan" : "Leerplein toevoegen"}</button>
|
<button className="btn primary" type="submit">{editLeerpleinId ? `${BOVENGROEP_LABEL} opslaan` : `${BOVENGROEP_LABEL} toevoegen`}</button>
|
||||||
{editLeerpleinId && <button type="button" className="btn" onClick={() => { setEditLeerpleinId(null); setLeerpleinForm(leegLeerplein); }}>Annuleren</button>}
|
{editLeerpleinId && <button type="button" className="btn" onClick={() => { setEditLeerpleinId(null); setLeerpleinForm(leegLeerplein); }}>Annuleren</button>}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -913,7 +915,7 @@ export function LeerpleinPage() {
|
||||||
<h2>Groep</h2>
|
<h2>Groep</h2>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<select value={groepForm.leerplein} required onChange={(e) => setGroepForm({ ...groepForm, leerplein: e.target.value })}>
|
<select value={groepForm.leerplein} required onChange={(e) => setGroepForm({ ...groepForm, leerplein: e.target.value })}>
|
||||||
<option value="">— bovenliggend leerplein —</option>
|
<option value="">— {BOVENGROEP_LABEL_LC} —</option>
|
||||||
{leerpleinen.map((lp) => <option key={lp.id} value={lp.id}>{lp.naam}</option>)}
|
{leerpleinen.map((lp) => <option key={lp.id} value={lp.id}>{lp.naam}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<input placeholder="Naam groep" value={groepForm.naam} required onChange={(e) => setGroepForm({ ...groepForm, naam: e.target.value })} />
|
<input placeholder="Naam groep" value={groepForm.naam} required onChange={(e) => setGroepForm({ ...groepForm, naam: e.target.value })} />
|
||||||
|
|
@ -944,10 +946,10 @@ export function LeerpleinPage() {
|
||||||
setSubgroepForm({ ...subgroepForm, groep: groepId, leerplein: subgroepForm.leerplein || lp?.id || "" });
|
setSubgroepForm({ ...subgroepForm, groep: groepId, leerplein: subgroepForm.leerplein || lp?.id || "" });
|
||||||
}}>
|
}}>
|
||||||
<option value="">— groep —</option>
|
<option value="">— groep —</option>
|
||||||
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam} ({leerpleinVanGroep(g.id)?.naam || "geen leerplein"})</option>)}
|
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam} ({leerpleinVanGroep(g.id)?.naam || `geen ${BOVENGROEP_LABEL_LC}`})</option>)}
|
||||||
</select>
|
</select>
|
||||||
<select value={subgroepForm.leerplein} onChange={(e) => setSubgroepForm({ ...subgroepForm, leerplein: e.target.value })}>
|
<select value={subgroepForm.leerplein} onChange={(e) => setSubgroepForm({ ...subgroepForm, leerplein: e.target.value })}>
|
||||||
<option value="">— koppel ook aan leerplein —</option>
|
<option value="">— koppel ook aan {BOVENGROEP_LABEL_LC} —</option>
|
||||||
{leerpleinen.map((lp) => <option key={lp.id} value={lp.id}>{lp.naam}</option>)}
|
{leerpleinen.map((lp) => <option key={lp.id} value={lp.id}>{lp.naam}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<input placeholder="Naam subgroep" value={subgroepForm.naam} required onChange={(e) => setSubgroepForm({ ...subgroepForm, naam: e.target.value })} />
|
<input placeholder="Naam subgroep" value={subgroepForm.naam} required onChange={(e) => setSubgroepForm({ ...subgroepForm, naam: e.target.value })} />
|
||||||
|
|
@ -955,7 +957,7 @@ export function LeerpleinPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label className="veld"><input type="radio" checked={subgroepForm.ledenBron === "groep"} onChange={() => setSubgroepForm({ ...subgroepForm, ledenBron: "groep" })} /> leerlingen uit groep</label>
|
<label className="veld"><input type="radio" checked={subgroepForm.ledenBron === "groep"} onChange={() => setSubgroepForm({ ...subgroepForm, ledenBron: "groep" })} /> leerlingen uit groep</label>
|
||||||
<label className="veld"><input type="radio" checked={subgroepForm.ledenBron === "leerplein"} onChange={() => setSubgroepForm({ ...subgroepForm, ledenBron: "leerplein" })} /> leerlingen uit heel leerplein</label>
|
<label className="veld"><input type="radio" checked={subgroepForm.ledenBron === "leerplein"} onChange={() => setSubgroepForm({ ...subgroepForm, ledenBron: "leerplein" })} /> leerlingen uit volledige {BOVENGROEP_LABEL_LC}</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="leden-keuze">
|
<div className="leden-keuze">
|
||||||
<div className="lijst-kop">
|
<div className="lijst-kop">
|
||||||
|
|
@ -978,50 +980,67 @@ export function LeerpleinPage() {
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{loading ? <p>Laden…</p> : (
|
{loading ? <p>Laden…</p> : (
|
||||||
<div className="structuur-overzicht">
|
<div className="structuur-tree">
|
||||||
{leerpleinen.map((lp) => {
|
{leerpleinen.map((lp) => {
|
||||||
const lpGroepen = groepen.filter((g) => (lp.groepen || []).includes(g.id));
|
const lpGroepen = groepen.filter((g) => (lp.groepen || []).includes(g.id));
|
||||||
const lpSubgroepen = subgroepen.filter((s) => (lp.subgroepen || []).includes(s.id) || lpGroepen.some((g) => g.id === s.groep));
|
const lpSubgroepen = subgroepen.filter((s) => (lp.subgroepen || []).includes(s.id) || lpGroepen.some((g) => g.id === s.groep));
|
||||||
return (
|
return (
|
||||||
<div className="structuur-card" key={lp.id}>
|
<div className="tree-root" key={lp.id}>
|
||||||
<div className="structuur-head">
|
<div className="tree-node tree-parent">
|
||||||
<div><strong>{lp.naam}</strong>{lp.omschrijving && <div className="muted small">{lp.omschrijving}</div>}</div>
|
<div className="tree-node-main">
|
||||||
|
<span className="tree-dot parent" />
|
||||||
|
<div>
|
||||||
|
<strong>{lp.naam}</strong>
|
||||||
|
<div className="muted small">{lpGroepen.length} groep(en){lp.omschrijving ? ` · ${lp.omschrijving}` : ""}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="row-actions"><button className="link" onClick={() => bewerkLeerplein(lp)}>bewerken</button><button className="link danger" onClick={() => verwijderLeerplein(lp.id)}>verwijderen</button></div>
|
<div className="row-actions"><button className="link" onClick={() => bewerkLeerplein(lp)}>bewerken</button><button className="link danger" onClick={() => verwijderLeerplein(lp.id)}>verwijderen</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="structuur-grid">
|
<div className="tree-children">
|
||||||
{lpGroepen.map((g) => {
|
{lpGroepen.map((g) => {
|
||||||
const groepLeerlingen = leerlingenVanGroep(g.id);
|
const groepLeerlingen = leerlingenVanGroep(g.id);
|
||||||
const groepSubs = lpSubgroepen.filter((s) => s.groep === g.id);
|
const groepSubs = lpSubgroepen.filter((s) => s.groep === g.id);
|
||||||
return (
|
return (
|
||||||
<div className="structuur-groep" key={g.id}>
|
<div className="tree-branch" key={g.id}>
|
||||||
<div className="structuur-head compact">
|
<div className="tree-node tree-group">
|
||||||
<div><strong>{g.naam}</strong><div className="muted small">{groepLeerlingen.length} leerling(en)</div></div>
|
<div className="tree-node-main">
|
||||||
|
<span className="tree-dot group" />
|
||||||
|
<div>
|
||||||
|
<strong>{g.naam}</strong>
|
||||||
|
<div className="muted small">{groepLeerlingen.length} leerling(en)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="row-actions"><button className="link" onClick={() => bewerkGroep(g)}>bewerken</button><button className="link danger" onClick={() => verwijderGroep(g.id)}>verwijderen</button></div>
|
<div className="row-actions"><button className="link" onClick={() => bewerkGroep(g)}>bewerken</button><button className="link danger" onClick={() => verwijderGroep(g.id)}>verwijderen</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="muted small">{groepLeerlingen.map((l) => l.volledige_naam).join(", ") || "Geen leerlingen"}</div>
|
<div className="tree-meta muted small">{groepLeerlingen.map((l) => l.volledige_naam).join(", ") || "Geen leerlingen"}</div>
|
||||||
<div className="structuur-subgroepen">
|
<div className="tree-children tree-subchildren">
|
||||||
{groepSubs.map((s) => (
|
{groepSubs.map((s) => (
|
||||||
<div className="structuur-subgroep" key={s.id}>
|
<div className="tree-node tree-subgroup" key={s.id}>
|
||||||
<div className="structuur-head compact">
|
<div className="tree-node-main">
|
||||||
<strong>{s.naam}</strong>
|
<span className="tree-dot sub" />
|
||||||
|
<div>
|
||||||
|
<strong>{s.naam}</strong>
|
||||||
|
<div className="muted small">{s.aantal_leden} leerling(en): {s.leden_namen.map((x) => x.naam).join(", ") || "geen"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<div className="row-actions"><button className="link" onClick={() => bewerkSubgroep(s)}>bewerken</button><button className="link danger" onClick={() => verwijderSubgroep(s.id)}>verwijderen</button></div>
|
<div className="row-actions"><button className="link" onClick={() => bewerkSubgroep(s)}>bewerken</button><button className="link danger" onClick={() => verwijderSubgroep(s.id)}>verwijderen</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="muted small">{s.aantal_leden} leerling(en): {s.leden_namen.map((x) => x.naam).join(", ") || "geen"}</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{groepSubs.length === 0 && <div className="muted small">Geen subgroepen.</div>}
|
{groepSubs.length === 0 && <div className="tree-empty muted small">Geen subgroepen.</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{lpGroepen.length === 0 && <div className="muted">Nog geen groepen onder dit leerplein.</div>}
|
{lpGroepen.length === 0 && <div className="tree-empty muted">Nog geen groepen onder deze {BOVENGROEP_LABEL_LC}.</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{leerpleinen.length === 0 && <div className="muted">Nog geen leerpleinen.</div>}
|
{leerpleinen.length === 0 && <div className="muted">Nog geen bovenliggende groepen.</div>}
|
||||||
{groepen.filter((g) => !leerpleinVanGroep(g.id)).length > 0 && (
|
{groepen.filter((g) => !leerpleinVanGroep(g.id)).length > 0 && (
|
||||||
<div className="banner error">Er zijn groepen zonder leerplein. Bewerk ze hieronder en kies een bovenliggend leerplein: {groepen.filter((g) => !leerpleinVanGroep(g.id)).map((g) => g.naam).join(", ")}</div>
|
<div className="banner error">Er zijn groepen zonder {BOVENGROEP_LABEL_LC}. Bewerk ze hieronder en kies een {BOVENGROEP_LABEL_LC}: {groepen.filter((g) => !leerpleinVanGroep(g.id)).map((g) => g.naam).join(", ")}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1277,7 +1296,7 @@ export function leerlingenVanBlok(blok, ctx) {
|
||||||
if (blok.leerling) return [blok.leerling];
|
if (blok.leerling) return [blok.leerling];
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
// Welke leerlingen vallen binnen een gekozen scope (klas/leerplein/leerling)?
|
// Welke leerlingen vallen binnen een gekozen scope (klas/bovenliggende groep/leerling)?
|
||||||
export function scopeLeerlingen(scope, ctx) {
|
export function scopeLeerlingen(scope, ctx) {
|
||||||
if (scope.type === "leerling") return [scope.id];
|
if (scope.type === "leerling") return [scope.id];
|
||||||
if (scope.type === "klas") return ctx.leerlingen.filter((l) => l.groep === scope.id).map((l) => l.id);
|
if (scope.type === "klas") return ctx.leerlingen.filter((l) => l.groep === scope.id).map((l) => l.id);
|
||||||
|
|
@ -1504,7 +1523,7 @@ function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------- Groepsindeling --------------------------- */
|
/* ------------------------------- Groepsindeling --------------------------- */
|
||||||
// Verdeel een hoofdgroep (of leerplein) kolomsgewijs in subgroepen: per kolom
|
// Verdeel een hoofdgroep (of bovenliggende groep) kolomsgewijs in subgroepen: per kolom
|
||||||
// de subgroep met haar verantwoordelijke(n) en de vakken die er gegeven worden.
|
// de subgroep met haar verantwoordelijke(n) en de vakken die er gegeven worden.
|
||||||
function Groepsindeling({ groepen, leerpleinen, subgroepen, medewerkers, blokken, onWijzig }) {
|
function Groepsindeling({ groepen, leerpleinen, subgroepen, medewerkers, blokken, onWijzig }) {
|
||||||
const [type, setType] = useState("groep");
|
const [type, setType] = useState("groep");
|
||||||
|
|
@ -1544,13 +1563,13 @@ function Groepsindeling({ groepen, leerpleinen, subgroepen, medewerkers, blokken
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className="muted">Kies een hoofdgroep of leerplein en verdeel die in subgroepen. Per subgroep zie je de vakken en wie verantwoordelijk is.</p>
|
<p className="muted">Kies een hoofdgroep of bovenliggende groep en verdeel die in subgroepen. Per subgroep zie je de vakken en wie verantwoordelijk is.</p>
|
||||||
<Foutmelding msg={error} />
|
<Foutmelding msg={error} />
|
||||||
<div className="rooster-balk">
|
<div className="rooster-balk">
|
||||||
<label className="veld">Type
|
<label className="veld">Type
|
||||||
<select value={type} onChange={(e) => { setType(e.target.value); setHoofdId(""); setNieuw({ naam: "", groep: "", begeleiders: [] }); }}>
|
<select value={type} onChange={(e) => { setType(e.target.value); setHoofdId(""); setNieuw({ naam: "", groep: "", begeleiders: [] }); }}>
|
||||||
<option value="groep">Groep</option>
|
<option value="groep">Groep</option>
|
||||||
<option value="leerplein">Leerplein</option>
|
<option value="leerplein">{BOVENGROEP_LABEL}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<select value={hoofdId} onChange={(e) => setHoofdId(e.target.value)}>
|
<select value={hoofdId} onChange={(e) => setHoofdId(e.target.value)}>
|
||||||
|
|
@ -1599,7 +1618,7 @@ function Groepsindeling({ groepen, leerpleinen, subgroepen, medewerkers, blokken
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form className="form-card" onSubmit={voegToe}>
|
<form className="form-card" onSubmit={voegToe}>
|
||||||
<div className="muted small">Subgroep toevoegen onder {type === "groep" ? hoofd.naam : "een groep binnen dit leerplein"}:</div>
|
<div className="muted small">Subgroep toevoegen onder {type === "groep" ? hoofd.naam : `een groep binnen deze ${BOVENGROEP_LABEL_LC}`}:</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
{type === "leerplein" && (
|
{type === "leerplein" && (
|
||||||
<select value={nieuw.groep || ""} required onChange={(e) => setNieuw({ ...nieuw, groep: e.target.value })}>
|
<select value={nieuw.groep || ""} required onChange={(e) => setNieuw({ ...nieuw, groep: e.target.value })}>
|
||||||
|
|
@ -1648,7 +1667,7 @@ export function RoosterScherm({ mode }) {
|
||||||
const [toonForm, setToonForm] = useState(false);
|
const [toonForm, setToonForm] = useState(false);
|
||||||
const [tab, setTab] = useState("week");
|
const [tab, setTab] = useState("week");
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
// Scope-filter (ook voor gericht afdrukken): per klas/leerplein/leerling/personeel.
|
// Scope-filter (ook voor gericht afdrukken): per klas/bovenliggende groep/leerling/personeel.
|
||||||
const [scopeType, setScopeType] = useState("alles");
|
const [scopeType, setScopeType] = useState("alles");
|
||||||
const [scopeId, setScopeId] = useState("");
|
const [scopeId, setScopeId] = useState("");
|
||||||
// Blok bewerken/kopiëren (alleen in de Roostermaker).
|
// Blok bewerken/kopiëren (alleen in de Roostermaker).
|
||||||
|
|
@ -1656,6 +1675,8 @@ export function RoosterScherm({ mode }) {
|
||||||
|
|
||||||
const leeg = { tijdslot: "", activiteit: "", locatie: "", doel_type: "groep", doel: "", begeleiders: [], opmerkingen: "" };
|
const leeg = { tijdslot: "", activiteit: "", locatie: "", doel_type: "groep", doel: "", begeleiders: [], opmerkingen: "" };
|
||||||
const [form, setForm] = useState(leeg);
|
const [form, setForm] = useState(leeg);
|
||||||
|
const [splitNieuwBlok, setSplitNieuwBlok] = useState(false);
|
||||||
|
const [splitNieuwDoelen, setSplitNieuwDoelen] = useState([]);
|
||||||
|
|
||||||
// Drag-and-drop via pointer events (werkt op muis én touch).
|
// Drag-and-drop via pointer events (werkt op muis én touch).
|
||||||
const dragRef = useRef(null);
|
const dragRef = useRef(null);
|
||||||
|
|
@ -1748,9 +1769,9 @@ export function RoosterScherm({ mode }) {
|
||||||
function onKey(e) {
|
function onKey(e) {
|
||||||
const t = e.target;
|
const t = e.target;
|
||||||
const typing = t && (t.tagName === "INPUT" || t.tagName === "SELECT" || t.tagName === "TEXTAREA" || t.isContentEditable);
|
const typing = t && (t.tagName === "INPUT" || t.tagName === "SELECT" || t.tagName === "TEXTAREA" || t.isContentEditable);
|
||||||
if (e.key === "Escape") { setBewerkBlok(null); setToonForm(false); return; }
|
if (e.key === "Escape") { setBewerkBlok(null); sluitNieuwBlok(); return; }
|
||||||
if (typing) return;
|
if (typing) return;
|
||||||
if (e.key === "n" || e.key === "N") { setTab("week"); setToonForm((v) => !v); }
|
if (e.key === "n" || e.key === "N") { setTab("week"); setBewerkBlok(null); setToonForm((v) => { if (v) resetNieuweSplit(); return !v; }); }
|
||||||
else if (e.key === "ArrowLeft") { setMaandag((m) => plusDagen(m, -7)); }
|
else if (e.key === "ArrowLeft") { setMaandag((m) => plusDagen(m, -7)); }
|
||||||
else if (e.key === "ArrowRight") { setMaandag((m) => plusDagen(m, 7)); }
|
else if (e.key === "ArrowRight") { setMaandag((m) => plusDagen(m, 7)); }
|
||||||
else if (e.key === "t" || e.key === "T") { setMaandag(maandagVan(new Date())); }
|
else if (e.key === "t" || e.key === "T") { setMaandag(maandagVan(new Date())); }
|
||||||
|
|
@ -1766,14 +1787,50 @@ export function RoosterScherm({ mode }) {
|
||||||
setHoverSlot(blok.tijdslot);
|
setHoverSlot(blok.tijdslot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetNieuweSplit() {
|
||||||
|
setSplitNieuwBlok(false);
|
||||||
|
setSplitNieuwDoelen([]);
|
||||||
|
}
|
||||||
|
function openNieuwBlok() {
|
||||||
|
setBewerkBlok(null);
|
||||||
|
setToonForm(true);
|
||||||
|
}
|
||||||
|
function sluitNieuwBlok() {
|
||||||
|
setToonForm(false);
|
||||||
|
resetNieuweSplit();
|
||||||
|
}
|
||||||
|
function toggleNieuwSplitDoel(id) {
|
||||||
|
setSplitNieuwDoelen((doelen) => doelen.includes(id) ? doelen.filter((x) => x !== id) : [...doelen, id]);
|
||||||
|
}
|
||||||
|
|
||||||
const doelOpties = form.doel_type === "groep" ? groepen : form.doel_type === "subgroep" ? subgroepen : leerlingen;
|
const doelOpties = form.doel_type === "groep" ? groepen : form.doel_type === "subgroep" ? subgroepen : leerlingen;
|
||||||
|
const splitNieuweOpties = form.doel_type === "groep" && form.doel
|
||||||
|
? subgroepen.filter((s) => s.groep === Number(form.doel))
|
||||||
|
: [];
|
||||||
|
const splitNieuweGroep = groepen.find((g) => g.id === Number(form.doel));
|
||||||
function toggleBegeleider(id) {
|
function toggleBegeleider(id) {
|
||||||
setForm((f) => f.begeleiders.includes(id) ? { ...f, begeleiders: f.begeleiders.filter((x) => x !== id) } : { ...f, begeleiders: [...f.begeleiders, id] });
|
setForm((f) => f.begeleiders.includes(id) ? { ...f, begeleiders: f.begeleiders.filter((x) => x !== id) } : { ...f, begeleiders: [...f.begeleiders, id] });
|
||||||
}
|
}
|
||||||
async function opslaan(e) {
|
async function opslaan(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const payload = { schooljaar, tijdslot: form.tijdslot, activiteit: form.activiteit, locatie: form.locatie || null, groep: form.doel_type === "groep" ? form.doel : null, subgroep: form.doel_type === "subgroep" ? form.doel : null, leerling: form.doel_type === "leerling" ? form.doel : null, begeleiders: form.begeleiders, opmerkingen: form.opmerkingen };
|
const basis = { schooljaar, tijdslot: form.tijdslot, activiteit: form.activiteit, locatie: form.locatie || null, begeleiders: form.begeleiders, opmerkingen: form.opmerkingen };
|
||||||
try { await createResource("roosterblokken", payload); setForm({ ...leeg, doel_type: form.doel_type }); setToonForm(false); await laadBlokken(); } catch (err) { setError(err.message); }
|
try {
|
||||||
|
if (form.doel_type === "groep" && splitNieuwBlok && splitNieuwDoelen.length > 0) {
|
||||||
|
for (const subgroep of splitNieuwDoelen) {
|
||||||
|
await createResource("roosterblokken", { ...basis, groep: null, subgroep, leerling: null });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await createResource("roosterblokken", {
|
||||||
|
...basis,
|
||||||
|
groep: form.doel_type === "groep" ? form.doel : null,
|
||||||
|
subgroep: form.doel_type === "subgroep" ? form.doel : null,
|
||||||
|
leerling: form.doel_type === "leerling" ? form.doel : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setForm({ ...leeg, doel_type: form.doel_type });
|
||||||
|
sluitNieuwBlok();
|
||||||
|
await herlaadNaBlokWijziging();
|
||||||
|
} catch (err) { setError(err.message); }
|
||||||
}
|
}
|
||||||
async function verwijder(id) {
|
async function verwijder(id) {
|
||||||
if (!confirm("Dit roosterblok (alle weken) verwijderen?")) return;
|
if (!confirm("Dit roosterblok (alle weken) verwijderen?")) return;
|
||||||
|
|
@ -1880,8 +1937,8 @@ export function RoosterScherm({ mode }) {
|
||||||
<div className="split-header">
|
<div className="split-header">
|
||||||
<span className="split-icon">⤴</span>
|
<span className="split-icon">⤴</span>
|
||||||
<div className="split-title">
|
<div className="split-title">
|
||||||
<strong>{leerplein ? leerplein.naam : "Leerplein"}</strong>
|
<strong>{leerplein ? leerplein.naam : BOVENGROEP_LABEL}</strong>
|
||||||
<span>Leerplein → groepen</span>
|
<span>{BOVENGROEP_LABEL} → groepen</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="split-count">{kolommen.length}</span>
|
<span className="split-count">{kolommen.length}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1969,7 +2026,7 @@ export function RoosterScherm({ mode }) {
|
||||||
<span className="week-label">week van {fmtDM(maandag)}</span>
|
<span className="week-label">week van {fmtDM(maandag)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{edit && schooljaar && tab === "week" && <button className="btn primary icon-label" title="Blok toevoegen" onClick={() => setToonForm((v) => !v)}>{toonForm ? "×" : "+"} <span>{toonForm ? "Sluiten" : "Blok"}</span></button>}
|
{edit && schooljaar && tab === "week" && <button className="btn primary icon-label" title="Vak toevoegen" onClick={toonForm ? sluitNieuwBlok : openNieuwBlok}>{toonForm ? "×" : "+"} <span>{toonForm ? "Sluiten" : "Vak"}</span></button>}
|
||||||
{printAan && schooljaar && (edit ? tab === "week" : true) && (
|
{printAan && schooljaar && (edit ? tab === "week" : true) && (
|
||||||
<button className="btn print-knop icon-label" title="Rooster afdrukken" onClick={() => window.print()}>🖨 <span>Print</span></button>
|
<button className="btn print-knop icon-label" title="Rooster afdrukken" onClick={() => window.print()}>🖨 <span>Print</span></button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1981,7 +2038,7 @@ export function RoosterScherm({ mode }) {
|
||||||
<select value={scopeType} onChange={(e) => { setScopeType(e.target.value); setScopeId(""); }}>
|
<select value={scopeType} onChange={(e) => { setScopeType(e.target.value); setScopeId(""); }}>
|
||||||
<option value="alles">Alles</option>
|
<option value="alles">Alles</option>
|
||||||
<option value="klas">Per klas</option>
|
<option value="klas">Per klas</option>
|
||||||
<option value="leerplein">Per leerplein</option>
|
<option value="leerplein">Per {BOVENGROEP_LABEL_LC}</option>
|
||||||
<option value="leerling">Per leerling</option>
|
<option value="leerling">Per leerling</option>
|
||||||
<option value="personeel">Per personeel</option>
|
<option value="personeel">Per personeel</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -2024,44 +2081,92 @@ export function RoosterScherm({ mode }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{edit && schooljaar && tab === "week" && toonForm && (
|
{edit && schooljaar && tab === "week" && toonForm && (
|
||||||
<form className="form-card no-print" onSubmit={opslaan}>
|
<div className="modal-overlay no-print" onClick={sluitNieuwBlok}>
|
||||||
<div className="form-row">
|
<form className="modal rooster-modal" role="dialog" aria-modal="true" aria-label="Vak toevoegen" onSubmit={opslaan} onClick={(e) => e.stopPropagation()}>
|
||||||
<select value={form.tijdslot} required onChange={(e) => setForm({ ...form, tijdslot: e.target.value })}>
|
<div className="modal-kop">
|
||||||
<option value="">— tijdslot —</option>
|
<div>
|
||||||
{tijdsloten.map((t) => <option key={t.id} value={t.id}>{t.dag_label} {t.start_tijd?.slice(0, 5)} {t.naam}</option>)}
|
<h2>Vak toevoegen</h2>
|
||||||
</select>
|
<div className="muted small">Maak één blok of splits direct over subgroepen.</div>
|
||||||
<select value={form.activiteit} required onChange={(e) => setForm({ ...form, activiteit: e.target.value })}>
|
</div>
|
||||||
<option value="">— activiteit —</option>
|
<button type="button" className="icon-btn" title="Sluiten" onClick={sluitNieuwBlok}>×</button>
|
||||||
{activiteiten.map((a) => <option key={a.id} value={a.id}>{a.naam}</option>)}
|
|
||||||
</select>
|
|
||||||
<select value={form.locatie} onChange={(e) => setForm({ ...form, locatie: e.target.value })}>
|
|
||||||
<option value="">— locatie (optioneel) —</option>
|
|
||||||
{locaties.map((l) => <option key={l.id} value={l.id}>{l.naam}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<select value={form.doel_type} onChange={(e) => setForm({ ...form, doel_type: e.target.value, doel: "" })}>
|
|
||||||
<option value="groep">Groep</option>
|
|
||||||
<option value="subgroep">Subgroep</option>
|
|
||||||
<option value="leerling">Individuele leerling</option>
|
|
||||||
</select>
|
|
||||||
<select value={form.doel} required onChange={(e) => setForm({ ...form, doel: e.target.value })}>
|
|
||||||
<option value="">— kies {form.doel_type} —</option>
|
|
||||||
{doelOpties.map((o) => <option key={o.id} value={o.id}>{o.naam || o.volledige_naam}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="leden-keuze">
|
|
||||||
<div className="muted small">Begeleiders:</div>
|
|
||||||
<div className="chips">
|
|
||||||
{medewerkers.map((m) => <button type="button" key={m.id} className={form.begeleiders.includes(m.id) ? "chip on" : "chip"} onClick={() => toggleBegeleider(m.id)}>{m.volledige_naam}{m.rol_label ? ` · ${m.rol_label}` : ""}</button>)}
|
|
||||||
{medewerkers.length === 0 && <span className="muted small">Nog geen medewerkers.</span>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="blok-editor modal-editor">
|
||||||
<div className="form-row">
|
<div className="blok-editor-kop">
|
||||||
<input placeholder="Opmerking (optioneel)" value={form.opmerkingen} onChange={(e) => setForm({ ...form, opmerkingen: e.target.value })} />
|
<strong>Basis</strong>
|
||||||
<button className="btn primary" type="submit">Blok toevoegen</button>
|
<span className="muted small">Tijd, vak en doel</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<div className="editor-grid">
|
||||||
|
<select value={form.tijdslot} required onChange={(e) => setForm({ ...form, tijdslot: e.target.value })}>
|
||||||
|
<option value="">— tijdslot —</option>
|
||||||
|
{tijdsloten.map((t) => <option key={t.id} value={t.id}>{t.dag_label} {t.start_tijd?.slice(0, 5)} {t.naam}</option>)}
|
||||||
|
</select>
|
||||||
|
<select value={form.activiteit} required onChange={(e) => setForm({ ...form, activiteit: e.target.value })}>
|
||||||
|
<option value="">— activiteit —</option>
|
||||||
|
{activiteiten.map((a) => <option key={a.id} value={a.id}>{a.naam}</option>)}
|
||||||
|
</select>
|
||||||
|
<select value={form.locatie} onChange={(e) => setForm({ ...form, locatie: e.target.value })}>
|
||||||
|
<option value="">— locatie (optioneel) —</option>
|
||||||
|
{locaties.map((l) => <option key={l.id} value={l.id}>{l.naam}</option>)}
|
||||||
|
</select>
|
||||||
|
<input placeholder="Opmerking (optioneel)" value={form.opmerkingen} onChange={(e) => setForm({ ...form, opmerkingen: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details className="editor-detail" open>
|
||||||
|
<summary>Doelgroep</summary>
|
||||||
|
<div className="editor-grid">
|
||||||
|
<select value={form.doel_type} onChange={(e) => { setForm({ ...form, doel_type: e.target.value, doel: "" }); resetNieuweSplit(); }}>
|
||||||
|
<option value="groep">Groep</option>
|
||||||
|
<option value="subgroep">Subgroep</option>
|
||||||
|
<option value="leerling">Individuele leerling</option>
|
||||||
|
</select>
|
||||||
|
<select value={form.doel} required onChange={(e) => { setForm({ ...form, doel: e.target.value }); resetNieuweSplit(); }}>
|
||||||
|
<option value="">— kies {form.doel_type} —</option>
|
||||||
|
{doelOpties.map((o) => <option key={o.id} value={o.id}>{o.naam || o.volledige_naam}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
{form.doel_type === "groep" && form.doel && (
|
||||||
|
<details className="editor-detail split-editor" open>
|
||||||
|
<summary>Groep direct splitsen</summary>
|
||||||
|
<label className="veld editor-check">
|
||||||
|
<input type="checkbox" checked={splitNieuwBlok} onChange={(e) => { setSplitNieuwBlok(e.target.checked); if (!e.target.checked) setSplitNieuwDoelen([]); }} />
|
||||||
|
blok onder subgroepen plaatsen
|
||||||
|
</label>
|
||||||
|
{splitNieuwBlok && (
|
||||||
|
<>
|
||||||
|
<div className="chips compact">
|
||||||
|
{splitNieuweOpties.map((s) => <button type="button" key={s.id} className={splitNieuwDoelen.includes(s.id) ? "chip on" : "chip"} onClick={() => toggleNieuwSplitDoel(s.id)}>{s.naam}</button>)}
|
||||||
|
{splitNieuweOpties.length === 0 && <span className="muted small">Nog geen subgroepen onder deze groep.</span>}
|
||||||
|
</div>
|
||||||
|
{splitNieuwDoelen.length > 0 && (
|
||||||
|
<div className="split-preview">
|
||||||
|
<div className="split-preview-root">{splitNieuweGroep?.naam || "Groep"}</div>
|
||||||
|
<div className="split-preview-children">
|
||||||
|
{splitNieuwDoelen.map((sid) => <span key={sid}>{splitNieuweOpties.find((s) => s.id === sid)?.naam || "Subgroep"}</span>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<details className="editor-detail" open>
|
||||||
|
<summary>Begeleiders</summary>
|
||||||
|
<div className="chips compact">
|
||||||
|
{medewerkers.map((m) => <button type="button" key={m.id} className={form.begeleiders.includes(m.id) ? "chip on" : "chip"} onClick={() => toggleBegeleider(m.id)}>{m.volledige_naam}{m.rol_label ? ` · ${m.rol_label}` : ""}</button>)}
|
||||||
|
{medewerkers.length === 0 && <span className="muted small">Nog geen medewerkers.</span>}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div className="editor-actions">
|
||||||
|
<button className="btn primary icon-label" type="submit" disabled={splitNieuwBlok && splitNieuwDoelen.length === 0}>+ <span>{splitNieuwBlok ? `${splitNieuwDoelen.length} blok(ken)` : "Vak toevoegen"}</span></button>
|
||||||
|
<button type="button" className="btn" onClick={sluitNieuwBlok}>Annuleren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{schooljaar && tab === "week" && (
|
{schooljaar && tab === "week" && (
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,24 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
|
||||||
.check-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.check-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.lijst-kop { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
|
.lijst-kop { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
|
||||||
.structuur-overzicht { display: grid; gap: 14px; margin-top: 18px; }
|
.structuur-overzicht { display: grid; gap: 14px; margin-top: 18px; }
|
||||||
|
.structuur-tree { display: grid; gap: 14px; margin-top: 18px; }
|
||||||
|
.tree-root { border: 1px solid #bfdbfe; border-radius: 8px; background: #f8fbff; padding: 12px; }
|
||||||
|
.tree-node { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: 10px 12px; }
|
||||||
|
.tree-node-main { display: flex; align-items: flex-start; gap: 10px; min-width: 0; }
|
||||||
|
.tree-node-main > div { min-width: 0; }
|
||||||
|
.tree-node-main strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.tree-parent { border-color: #bfdbfe; background: #fff; }
|
||||||
|
.tree-group { border-left: 4px solid var(--brand); }
|
||||||
|
.tree-subgroup { border-left: 4px solid #93c5fd; background: #f9fafb; }
|
||||||
|
.tree-dot { width: 12px; height: 12px; border-radius: 999px; margin-top: 4px; flex: none; background: var(--line); }
|
||||||
|
.tree-dot.parent { background: var(--brand); }
|
||||||
|
.tree-dot.group { background: #38bdf8; }
|
||||||
|
.tree-dot.sub { background: #93c5fd; }
|
||||||
|
.tree-children { position: relative; display: grid; gap: 10px; margin: 10px 0 0 22px; padding-left: 16px; border-left: 2px solid #bfdbfe; }
|
||||||
|
.tree-branch { display: grid; gap: 7px; }
|
||||||
|
.tree-subchildren { margin-top: 0; border-left-color: #dbeafe; }
|
||||||
|
.tree-meta { padding-left: 16px; }
|
||||||
|
.tree-empty { padding: 8px 0 8px 12px; }
|
||||||
.structuur-card { border: 1px solid #bfdbfe; border-radius: 8px; background: #f8fbff; padding: 12px; }
|
.structuur-card { border: 1px solid #bfdbfe; border-radius: 8px; background: #f8fbff; padding: 12px; }
|
||||||
.structuur-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
|
.structuur-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
|
||||||
.structuur-head.compact { align-items: center; margin-bottom: 6px; }
|
.structuur-head.compact { align-items: center; margin-bottom: 6px; }
|
||||||
|
|
@ -305,6 +323,9 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
|
||||||
.top-tabs-settings { margin-left: 0; border-left: 0; padding-left: 0; }
|
.top-tabs-settings { margin-left: 0; border-left: 0; padding-left: 0; }
|
||||||
.split-kolommen { grid-template-columns: 1fr; }
|
.split-kolommen { grid-template-columns: 1fr; }
|
||||||
.split-kolom { border-left: 0; padding-left: 0; }
|
.split-kolom { border-left: 0; padding-left: 0; }
|
||||||
|
.tree-node { flex-direction: column; align-items: stretch; }
|
||||||
|
.tree-children { margin-left: 10px; padding-left: 12px; }
|
||||||
|
.tree-meta { padding-left: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Login --- */
|
/* --- Login --- */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue