2242 lines
110 KiB
JavaScript
2242 lines
110 KiB
JavaScript
import { useEffect, useRef, useState } from "react";
|
||
import {
|
||
listResource,
|
||
createResource,
|
||
updateResource,
|
||
deleteResource,
|
||
getModules,
|
||
} from "./api.js";
|
||
|
||
const ROLLEN = [
|
||
{ value: "leerling", label: "Leerling" },
|
||
{ value: "leerkracht", label: "Leerkracht" },
|
||
{ value: "praktijkondersteuner", label: "Praktijkondersteuner" },
|
||
{ value: "klassenondersteuner", label: "Klassenondersteuner" },
|
||
];
|
||
|
||
// Korte aanduiding van een medewerkersrol (compact in de roostervakjes).
|
||
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 = [
|
||
{ value: 0, label: "Maandag" },
|
||
{ value: 1, label: "Dinsdag" },
|
||
{ value: 2, label: "Woensdag" },
|
||
{ value: 3, label: "Donderdag" },
|
||
{ value: 4, label: "Vrijdag" },
|
||
];
|
||
|
||
const DAGTYPES = [
|
||
{ value: "studiedag", label: "Studiedag" },
|
||
{ value: "vrije_dag", label: "Vrije dag" },
|
||
{ value: "vakantie", label: "Vakantie" },
|
||
];
|
||
|
||
function useList(resource, query = "") {
|
||
const [items, setItems] = useState([]);
|
||
const [error, setError] = useState(null);
|
||
const [loading, setLoading] = useState(true);
|
||
async function reload() {
|
||
setLoading(true);
|
||
try {
|
||
setItems(await listResource(resource, query));
|
||
setError(null);
|
||
} catch (e) {
|
||
setError(e.message);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
useEffect(() => {
|
||
reload();
|
||
}, [resource, query]);
|
||
return { items, error, loading, reload, setError };
|
||
}
|
||
|
||
function Foutmelding({ msg }) {
|
||
if (!msg) return null;
|
||
return <div className="banner error">⚠ {msg}</div>;
|
||
}
|
||
|
||
/* ----------------------- Generieke eenvoudige catalogus ------------------- */
|
||
// Voor lijsten met alleen naam + omschrijving (Functies, Activiteiten, Locaties).
|
||
function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTheorievak = false }) {
|
||
const { items, error, loading, reload, setError } = useList(resource);
|
||
const leeg = {
|
||
naam: "", omschrijving: "",
|
||
...(kleur ? { kleur: "#2563eb" } : {}),
|
||
...(metTheorievak ? { theorievak: false } : {}),
|
||
};
|
||
const [form, setForm] = useState(leeg);
|
||
const [editId, setEditId] = useState(null);
|
||
const kolommen = 3 + (telKolom ? 1 : 0) + (metTheorievak ? 1 : 0);
|
||
|
||
async function opslaan(e) {
|
||
e.preventDefault();
|
||
try {
|
||
if (editId) await updateResource(resource, editId, form);
|
||
else await createResource(resource, form);
|
||
setForm(leeg);
|
||
setEditId(null);
|
||
await reload();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
async function verwijder(id) {
|
||
if (!confirm("Verwijderen?")) return;
|
||
try {
|
||
await deleteResource(resource, id);
|
||
await reload();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
function bewerk(it) {
|
||
setEditId(it.id);
|
||
setForm({
|
||
naam: it.naam, omschrijving: it.omschrijving,
|
||
...(kleur ? { kleur: it.kleur } : {}),
|
||
...(metTheorievak ? { theorievak: it.theorievak } : {}),
|
||
});
|
||
}
|
||
|
||
return (
|
||
<section>
|
||
<h1>{titel}</h1>
|
||
{uitleg && <p className="muted">{uitleg}</p>}
|
||
<Foutmelding msg={error} />
|
||
<form className="form-row" onSubmit={opslaan}>
|
||
<input placeholder="Naam" value={form.naam} required onChange={(e) => setForm({ ...form, naam: e.target.value })} />
|
||
<input placeholder="Omschrijving (optioneel)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} />
|
||
{kleur && (
|
||
<input type="color" value={form.kleur} title="Kleur" onChange={(e) => setForm({ ...form, kleur: e.target.value })} />
|
||
)}
|
||
{metTheorievak && (
|
||
<label className="veld">
|
||
<input type="checkbox" checked={!!form.theorievak} onChange={(e) => setForm({ ...form, theorievak: e.target.checked })} />
|
||
theorievak
|
||
</label>
|
||
)}
|
||
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
|
||
{editId && (
|
||
<button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>
|
||
)}
|
||
</form>
|
||
{loading ? (
|
||
<p>Laden…</p>
|
||
) : (
|
||
<table className="grid">
|
||
<thead>
|
||
<tr>
|
||
<th>Naam</th>
|
||
<th>Omschrijving</th>
|
||
{metTheorievak && <th>Theorievak</th>}
|
||
{telKolom && <th>{telKolom}</th>}
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{items.map((it) => (
|
||
<tr key={it.id}>
|
||
<td>
|
||
{kleur && <span className="kleur-stip" style={{ background: it.kleur }} />}
|
||
{it.naam}
|
||
</td>
|
||
<td className="muted">{it.omschrijving || "—"}</td>
|
||
{metTheorievak && <td>{it.theorievak ? "✓" : "—"}</td>}
|
||
{telKolom && <td>{it.aantal_personen ?? "—"}</td>}
|
||
<td className="row-actions">
|
||
<button className="link" onClick={() => bewerk(it)}>bewerken</button>
|
||
<button className="link danger" onClick={() => verwijder(it.id)}>verwijderen</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
{items.length === 0 && (
|
||
<tr><td colSpan={kolommen} className="muted">Nog niets.</td></tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
export function FunctiesPage() {
|
||
return (
|
||
<CatalogusPage
|
||
titel="Functies"
|
||
uitleg="Beheer de functies die medewerkers kunnen hebben (bv. Directie, Logopedist). Medewerkers kunnen meerdere functies hebben."
|
||
resource="functies"
|
||
telKolom="Medewerkers"
|
||
/>
|
||
);
|
||
}
|
||
|
||
export function ActiviteitenPage() {
|
||
return (
|
||
<CatalogusPage
|
||
titel="Activiteiten / vakken"
|
||
uitleg="Wat er gedaan wordt: rekenen, gym, therapie, pauze … Elke activiteit heeft een kleur. Markeer theorievakken; die koppelen automatisch aan nieuwe leerkrachten."
|
||
resource="activiteiten"
|
||
kleur
|
||
metTheorievak
|
||
/>
|
||
);
|
||
}
|
||
|
||
export function LocatiesPage() {
|
||
return (
|
||
<CatalogusPage
|
||
titel="Locaties"
|
||
uitleg="Lokalen en ruimtes. Worden gebruikt voor conflictdetectie."
|
||
resource="locaties"
|
||
/>
|
||
);
|
||
}
|
||
|
||
/* --------------------------------- Groepen -------------------------------- */
|
||
export function GroepenPage() {
|
||
return <GroepsindelingPage />;
|
||
}
|
||
|
||
/* -------------------------------- Personen -------------------------------- */
|
||
const STAFF_ROLLEN = ROLLEN.filter((r) => r.value !== "leerling");
|
||
|
||
export function PersonenPage() {
|
||
const { items, error, loading, reload, setError } = useList("personen");
|
||
const [groepen, setGroepen] = useState([]);
|
||
const [functies, setFuncties] = useState([]);
|
||
const [vakken, setVakken] = useState([]);
|
||
const [subgroepen, setSubgroepen] = useState([]);
|
||
const [leerpleinen, setLeerpleinen] = useState([]);
|
||
// Twee losse flows: medewerker en leerling.
|
||
const [modus, setModus] = useState("leerling");
|
||
const leegMedewerker = { voornaam: "", achternaam: "", rol: "leerkracht", stamgroep: "", functies: [], vakken: [], begeleide_subgroepen: [], leerpleinen: [] };
|
||
const leegLeerling = { voornaam: "", achternaam: "", groep: "", geboortedatum: "" };
|
||
const [form, setForm] = useState(leegLeerling);
|
||
const [editId, setEditId] = useState(null);
|
||
const [filter, setFilter] = useState("");
|
||
const [nieuweFunctie, setNieuweFunctie] = useState("");
|
||
|
||
async function maakFunctie() {
|
||
const naam = nieuweFunctie.trim();
|
||
if (!naam) return;
|
||
try {
|
||
const f = await createResource("functies", { naam });
|
||
setFuncties((lijst) => [...lijst, f]);
|
||
setForm((prev) => ({ ...prev, functies: [...prev.functies, f.id] }));
|
||
setNieuweFunctie("");
|
||
} catch (e) { setError(e.message); }
|
||
}
|
||
|
||
function laadHulplijsten() {
|
||
listResource("groepen").then(setGroepen).catch(() => {});
|
||
listResource("functies").then(setFuncties).catch(() => {});
|
||
listResource("activiteiten").then(setVakken).catch(() => {});
|
||
listResource("subgroepen").then(setSubgroepen).catch(() => {});
|
||
listResource("leerpleinen").then(setLeerpleinen).catch(() => setLeerpleinen([]));
|
||
}
|
||
useEffect(() => { laadHulplijsten(); }, []);
|
||
|
||
// De bovenliggende-groep-koppeling loopt via de bestaande API.
|
||
async function syncLeerpleinen(persoonId, gekozenIds) {
|
||
const huidige = await listResource("leerpleinen").catch(() => []);
|
||
for (const lp of huidige) {
|
||
const inLijst = (lp.medewerkers || []).includes(persoonId);
|
||
const moet = gekozenIds.includes(lp.id);
|
||
if (moet && !inLijst) await updateResource("leerpleinen", lp.id, { medewerkers: [...lp.medewerkers, persoonId] });
|
||
else if (!moet && inLijst) await updateResource("leerpleinen", lp.id, { medewerkers: lp.medewerkers.filter((x) => x !== persoonId) });
|
||
}
|
||
listResource("leerpleinen").then(setLeerpleinen).catch(() => {});
|
||
}
|
||
|
||
function kiesModus(m) {
|
||
setModus(m);
|
||
setEditId(null);
|
||
setForm(m === "medewerker" ? leegMedewerker : leegLeerling);
|
||
}
|
||
function toggle(veld, id) {
|
||
setForm((f) => f[veld].includes(id) ? { ...f, [veld]: f[veld].filter((x) => x !== id) } : { ...f, [veld]: [...f[veld], id] });
|
||
}
|
||
|
||
async function opslaan(e) {
|
||
e.preventDefault();
|
||
const payload = modus === "medewerker"
|
||
? {
|
||
voornaam: form.voornaam, achternaam: form.achternaam, rol: form.rol,
|
||
stamgroep: form.stamgroep || null, functies: form.functies, vakken: form.vakken,
|
||
begeleide_subgroepen: form.begeleide_subgroepen,
|
||
}
|
||
: {
|
||
voornaam: form.voornaam, achternaam: form.achternaam, rol: "leerling",
|
||
groep: form.groep || null, geboortedatum: form.geboortedatum || null,
|
||
};
|
||
try {
|
||
const opgeslagen = editId
|
||
? await updateResource("personen", editId, payload)
|
||
: await createResource("personen", payload);
|
||
if (modus === "medewerker" && opgeslagen && opgeslagen.id) {
|
||
await syncLeerpleinen(opgeslagen.id, form.leerpleinen || []);
|
||
}
|
||
setForm(modus === "medewerker" ? leegMedewerker : leegLeerling);
|
||
setEditId(null);
|
||
await reload();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
async function verwijder(id) {
|
||
if (!confirm("Deze persoon verwijderen?")) return;
|
||
try { await deleteResource("personen", id); await reload(); } catch (err) { setError(err.message); }
|
||
}
|
||
function bewerk(p) {
|
||
setEditId(p.id);
|
||
if (p.rol === "leerling") {
|
||
setModus("leerling");
|
||
setForm({ voornaam: p.voornaam, achternaam: p.achternaam, groep: p.groep || "", geboortedatum: p.geboortedatum || "" });
|
||
} else {
|
||
setModus("medewerker");
|
||
setForm({
|
||
voornaam: p.voornaam, achternaam: p.achternaam, rol: p.rol,
|
||
stamgroep: p.stamgroep || "",
|
||
functies: p.functies_namen.map((x) => x.id),
|
||
vakken: p.vakken_namen.map((x) => x.id),
|
||
begeleide_subgroepen: (p.begeleide_subgroepen_namen || []).map((x) => x.id),
|
||
leerpleinen: leerpleinen.filter((lp) => (lp.medewerkers || []).includes(p.id)).map((lp) => lp.id),
|
||
});
|
||
}
|
||
}
|
||
|
||
const zichtbaar = filter === "leerling" ? items.filter((p) => p.rol === "leerling")
|
||
: filter === "medewerker" ? items.filter((p) => p.rol !== "leerling")
|
||
: items;
|
||
|
||
return (
|
||
<section>
|
||
<h1>Personen</h1>
|
||
<p className="muted">Personeel en leerlingen staan los van elkaar. Kies wat je wilt toevoegen.</p>
|
||
<Foutmelding msg={error} />
|
||
|
||
<div className="tabbar">
|
||
<button className={modus === "leerling" ? "tab on" : "tab"} onClick={() => kiesModus("leerling")}>Leerling toevoegen</button>
|
||
<button className={modus === "medewerker" ? "tab on" : "tab"} onClick={() => kiesModus("medewerker")}>Medewerker toevoegen</button>
|
||
</div>
|
||
|
||
<form className="form-card" onSubmit={opslaan}>
|
||
<div className="form-row">
|
||
<input placeholder="Voornaam" value={form.voornaam} required onChange={(e) => setForm({ ...form, voornaam: e.target.value })} />
|
||
<input placeholder="Achternaam" value={form.achternaam} required onChange={(e) => setForm({ ...form, achternaam: e.target.value })} />
|
||
{modus === "leerling" ? (
|
||
<>
|
||
<select value={form.groep || ""} onChange={(e) => setForm({ ...form, groep: e.target.value })}>
|
||
<option value="">— groep —</option>
|
||
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam}</option>)}
|
||
</select>
|
||
<label className="veld">Geb. <input type="date" value={form.geboortedatum || ""} onChange={(e) => setForm({ ...form, geboortedatum: e.target.value })} /></label>
|
||
</>
|
||
) : (
|
||
<>
|
||
<select value={form.rol} onChange={(e) => setForm({ ...form, rol: e.target.value })}>
|
||
{STAFF_ROLLEN.map((r) => <option key={r.value} value={r.value}>{r.label}</option>)}
|
||
</select>
|
||
<select value={form.stamgroep || ""} onChange={(e) => setForm({ ...form, stamgroep: e.target.value })}>
|
||
<option value="">— stamgroep/plein —</option>
|
||
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam}</option>)}
|
||
</select>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{modus === "medewerker" && (
|
||
<>
|
||
<div className="leden-keuze">
|
||
<div className="muted small">Functies (klik om te kiezen, of typ een nieuwe):</div>
|
||
<div className="chips">
|
||
{functies.map((f) => <button type="button" key={f.id} className={form.functies.includes(f.id) ? "chip on" : "chip"} onClick={() => toggle("functies", f.id)}>{f.naam}</button>)}
|
||
{functies.length === 0 && <span className="muted small">Nog geen functies.</span>}
|
||
</div>
|
||
<div className="form-row" style={{ marginTop: 6 }}>
|
||
<input
|
||
placeholder="Nieuwe functie"
|
||
value={nieuweFunctie}
|
||
onChange={(e) => setNieuweFunctie(e.target.value)}
|
||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); maakFunctie(); } }}
|
||
/>
|
||
<button type="button" className="btn" onClick={maakFunctie}>+ Functie</button>
|
||
</div>
|
||
</div>
|
||
<div className="leden-keuze">
|
||
<div className="muted small">Vakken die gegeven kunnen worden{form.rol === "leerkracht" ? " (theorievakken worden automatisch gekoppeld)" : ""}:</div>
|
||
<div className="chips">
|
||
{vakken.map((a) => <button type="button" key={a.id} className={form.vakken.includes(a.id) ? "chip on" : "chip"} onClick={() => toggle("vakken", a.id)}>{a.naam}{a.theorievak ? " ●" : ""}</button>)}
|
||
{vakken.length === 0 && <span className="muted small">Nog geen vakken/activiteiten.</span>}
|
||
</div>
|
||
</div>
|
||
<div className="leden-keuze">
|
||
<div className="muted small">Subgroepen (begeleiding):</div>
|
||
<div className="chips">
|
||
{subgroepen.map((s) => <button type="button" key={s.id} className={form.begeleide_subgroepen.includes(s.id) ? "chip on" : "chip"} onClick={() => toggle("begeleide_subgroepen", s.id)}>{s.naam}</button>)}
|
||
{subgroepen.length === 0 && <span className="muted small">Nog geen subgroepen.</span>}
|
||
</div>
|
||
</div>
|
||
{leerpleinen.length > 0 && (
|
||
<div className="leden-keuze">
|
||
<div className="muted small">{BOVENGROEP_LABEL_MV}:</div>
|
||
<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>)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
<div className="form-row">
|
||
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
|
||
{editId && <button type="button" className="btn" onClick={() => kiesModus(modus)}>Annuleren</button>}
|
||
</div>
|
||
</form>
|
||
|
||
<div className="filterbar">
|
||
<span className="muted">Toon:</span>
|
||
<button className={filter === "" ? "chip on" : "chip"} onClick={() => setFilter("")}>Alle</button>
|
||
<button className={filter === "leerling" ? "chip on" : "chip"} onClick={() => setFilter("leerling")}>Leerlingen</button>
|
||
<button className={filter === "medewerker" ? "chip on" : "chip"} onClick={() => setFilter("medewerker")}>Medewerkers</button>
|
||
</div>
|
||
|
||
{loading ? <p>Laden…</p> : (
|
||
<table className="grid">
|
||
<thead><tr><th>Naam</th><th>Rol</th><th>Details</th><th></th></tr></thead>
|
||
<tbody>
|
||
{zichtbaar.map((p) => (
|
||
<tr key={p.id}>
|
||
<td>{p.volledige_naam}</td>
|
||
<td>{p.rol_label}</td>
|
||
<td className="muted small">
|
||
{p.rol === "leerling" ? (
|
||
<>
|
||
{p.groep_naam ? <span>🗂️ {p.groep_naam} </span> : null}
|
||
{p.geboortedatum ? <span>🎂 {p.geboortedatum}</span> : null}
|
||
{!p.groep_naam && !p.geboortedatum ? "—" : null}
|
||
</>
|
||
) : (
|
||
<>
|
||
{p.stamgroep_naam ? <div>🗂️ {p.stamgroep_naam}</div> : null}
|
||
{p.functies_namen.length ? <div>🪪 {p.functies_namen.map((x) => x.naam).join(", ")}</div> : null}
|
||
{p.vakken_namen.length ? <div>📚 {p.vakken_namen.map((x) => x.naam).join(", ")}</div> : null}
|
||
{!p.stamgroep_naam && !p.functies_namen.length && !p.vakken_namen.length ? "—" : null}
|
||
</>
|
||
)}
|
||
</td>
|
||
<td className="row-actions">
|
||
<button className="link" onClick={() => bewerk(p)}>bewerken</button>
|
||
<button className="link danger" onClick={() => verwijder(p.id)}>verwijderen</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
{zichtbaar.length === 0 && <tr><td colSpan="4" className="muted">Geen personen.</td></tr>}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* ------------------------------- Subgroepen ------------------------------- */
|
||
export function SubgroepenPage() {
|
||
return <GroepsindelingPage />;
|
||
}
|
||
|
||
/* ------------------------------- Tijdsloten ------------------------------- */
|
||
export function TijdslotenPage() {
|
||
const { items, error, loading, reload, setError } = useList("tijdsloten");
|
||
const [groepen, setGroepen] = useState([]);
|
||
const leeg = { naam: "", dag: 0, start_tijd: "08:30", eind_tijd: "10:00", volgorde: 1, groep: "" };
|
||
const [form, setForm] = useState(leeg);
|
||
const [editId, setEditId] = useState(null);
|
||
|
||
useEffect(() => { listResource("groepen").then(setGroepen).catch(() => {}); }, []);
|
||
|
||
async function opslaan(e) {
|
||
e.preventDefault();
|
||
try {
|
||
const payload = { ...form, groep: form.groep || null };
|
||
if (editId) await updateResource("tijdsloten", editId, payload);
|
||
else await createResource("tijdsloten", payload);
|
||
setForm(leeg);
|
||
setEditId(null);
|
||
await reload();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
async function verwijder(id) {
|
||
if (!confirm("Dit tijdslot verwijderen?")) return;
|
||
try {
|
||
await deleteResource("tijdsloten", id);
|
||
await reload();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<section>
|
||
<h1>Tijdsloten</h1>
|
||
<p className="muted">Vaste tijden per schooldag. Laat de groep leeg voor de algemene tijden, of kies een groep voor afwijkende tijden.</p>
|
||
<Foutmelding msg={error} />
|
||
<form className="form-row" onSubmit={opslaan}>
|
||
<input placeholder="Naam (bv. Blok 1)" value={form.naam} required onChange={(e) => setForm({ ...form, naam: e.target.value })} />
|
||
<select value={form.dag} onChange={(e) => setForm({ ...form, dag: Number(e.target.value) })}>
|
||
{WEEKDAGEN.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
|
||
</select>
|
||
<input type="time" value={form.start_tijd} onChange={(e) => setForm({ ...form, start_tijd: e.target.value })} />
|
||
<input type="time" value={form.eind_tijd} onChange={(e) => setForm({ ...form, eind_tijd: e.target.value })} />
|
||
<input type="number" min="0" style={{ width: 80 }} title="Volgorde" value={form.volgorde} onChange={(e) => setForm({ ...form, volgorde: Number(e.target.value) })} />
|
||
<select value={form.groep || ""} title="Groep (leeg = algemeen)" onChange={(e) => setForm({ ...form, groep: e.target.value })}>
|
||
<option value="">algemeen</option>
|
||
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam}</option>)}
|
||
</select>
|
||
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
|
||
{editId && <button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>}
|
||
</form>
|
||
{loading ? <p>Laden…</p> : (
|
||
<table className="grid">
|
||
<thead><tr><th>Dag</th><th>Tijd</th><th>Naam</th><th>Geldt voor</th><th></th></tr></thead>
|
||
<tbody>
|
||
{items.map((t) => (
|
||
<tr key={t.id}>
|
||
<td>{t.dag_label}</td>
|
||
<td>{t.start_tijd?.slice(0, 5)}–{t.eind_tijd?.slice(0, 5)}</td>
|
||
<td>{t.naam}</td>
|
||
<td className="muted">{t.groep_naam || "algemeen"}</td>
|
||
<td className="row-actions">
|
||
<button className="link" onClick={() => { setEditId(t.id); setForm({ naam: t.naam, dag: t.dag, start_tijd: t.start_tijd?.slice(0, 5), eind_tijd: t.eind_tijd?.slice(0, 5), volgorde: t.volgorde, groep: t.groep || "" }); }}>bewerken</button>
|
||
<button className="link danger" onClick={() => verwijder(t.id)}>verwijderen</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
{items.length === 0 && <tr><td colSpan="5" className="muted">Nog geen tijdsloten.</td></tr>}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* --------------------------- Schooljaar & kalender ------------------------ */
|
||
export function SchooljaarPage() {
|
||
const { items: jaren, error, loading, reload, setError } = useList("schooljaren");
|
||
const leeg = { naam: "", start_datum: "", eind_datum: "" };
|
||
const [form, setForm] = useState(leeg);
|
||
const [gekozen, setGekozen] = useState(null);
|
||
|
||
async function opslaan(e) {
|
||
e.preventDefault();
|
||
try {
|
||
await createResource("schooljaren", form);
|
||
setForm(leeg);
|
||
await reload();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
async function verwijder(id) {
|
||
if (!confirm("Dit schooljaar (incl. kalender) verwijderen?")) return;
|
||
try {
|
||
await deleteResource("schooljaren", id);
|
||
if (gekozen === id) setGekozen(null);
|
||
await reload();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<section>
|
||
<h1>Schooljaar & kalender</h1>
|
||
<p className="muted">Schooljaren met studiedagen, vrije dagen en vakanties. Op die dagen vervallen de roosterblokken.</p>
|
||
<Foutmelding msg={error} />
|
||
<form className="form-row" onSubmit={opslaan}>
|
||
<input placeholder="Naam (bv. 2026-2027)" value={form.naam} required onChange={(e) => setForm({ ...form, naam: e.target.value })} />
|
||
<label className="veld">Start <input type="date" value={form.start_datum} required onChange={(e) => setForm({ ...form, start_datum: e.target.value })} /></label>
|
||
<label className="veld">Eind <input type="date" value={form.eind_datum} required onChange={(e) => setForm({ ...form, eind_datum: e.target.value })} /></label>
|
||
<button className="btn primary" type="submit">Toevoegen</button>
|
||
</form>
|
||
{loading ? <p>Laden…</p> : (
|
||
<table className="grid">
|
||
<thead><tr><th>Schooljaar</th><th>Periode</th><th></th></tr></thead>
|
||
<tbody>
|
||
{jaren.map((j) => (
|
||
<tr key={j.id} className={gekozen === j.id ? "rij-actief" : ""}>
|
||
<td>{j.naam}</td>
|
||
<td className="muted">{j.start_datum} t/m {j.eind_datum}</td>
|
||
<td className="row-actions">
|
||
<button className="link" onClick={() => setGekozen(gekozen === j.id ? null : j.id)}>
|
||
{gekozen === j.id ? "kalender verbergen" : "kalender beheren"}
|
||
</button>
|
||
<button className="link danger" onClick={() => verwijder(j.id)}>verwijderen</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
{jaren.length === 0 && <tr><td colSpan="3" className="muted">Nog geen schooljaren.</td></tr>}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
{gekozen && <Kalender schooljaarId={gekozen} />}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function Kalender({ schooljaarId }) {
|
||
const { items, error, reload, setError } = useList("kalenderdagen", `?schooljaar=${schooljaarId}`);
|
||
const leeg = { datum: "", type: "vrije_dag", omschrijving: "" };
|
||
const [form, setForm] = useState(leeg);
|
||
|
||
async function opslaan(e) {
|
||
e.preventDefault();
|
||
try {
|
||
await createResource("kalenderdagen", { ...form, schooljaar: schooljaarId });
|
||
setForm(leeg);
|
||
await reload();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
async function verwijder(id) {
|
||
try {
|
||
await deleteResource("kalenderdagen", id);
|
||
await reload();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="subpaneel">
|
||
<h2>Kalender</h2>
|
||
<Foutmelding msg={error} />
|
||
<form className="form-row" onSubmit={opslaan}>
|
||
<input type="date" value={form.datum} required onChange={(e) => setForm({ ...form, datum: e.target.value })} />
|
||
<select value={form.type} onChange={(e) => setForm({ ...form, type: e.target.value })}>
|
||
{DAGTYPES.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
|
||
</select>
|
||
<input placeholder="Omschrijving (bv. Kerstvakantie)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} />
|
||
<button className="btn primary" type="submit">Dag toevoegen</button>
|
||
</form>
|
||
<table className="grid">
|
||
<thead><tr><th>Datum</th><th>Type</th><th>Omschrijving</th><th></th></tr></thead>
|
||
<tbody>
|
||
{items.map((d) => (
|
||
<tr key={d.id}>
|
||
<td>{d.datum}</td>
|
||
<td><span className={`tag dag-${d.type}`}>{d.type_label}</span></td>
|
||
<td className="muted">{d.omschrijving || "—"}</td>
|
||
<td className="row-actions"><button className="link danger" onClick={() => verwijder(d.id)}>verwijderen</button></td>
|
||
</tr>
|
||
))}
|
||
{items.length === 0 && <tr><td colSpan="4" className="muted">Nog geen bijzondere dagen.</td></tr>}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ------------------------------- Afwezigheid ------------------------------ */
|
||
export function AfwezigheidPage() {
|
||
const { items, error, loading, reload, setError } = useList("afwezigheden");
|
||
const [personen, setPersonen] = useState([]);
|
||
const leeg = { persoon: "", start_datum: "", eind_datum: "", reden: "" };
|
||
const [form, setForm] = useState(leeg);
|
||
|
||
useEffect(() => { listResource("personen").then(setPersonen).catch(() => {}); }, []);
|
||
|
||
async function opslaan(e) {
|
||
e.preventDefault();
|
||
try {
|
||
await createResource("afwezigheden", form);
|
||
setForm(leeg);
|
||
await reload();
|
||
} catch (err) { setError(err.message); }
|
||
}
|
||
async function verwijder(id) {
|
||
if (!confirm("Deze afwezigheid verwijderen?")) return;
|
||
try { await deleteResource("afwezigheden", id); await reload(); } catch (err) { setError(err.message); }
|
||
}
|
||
|
||
return (
|
||
<section>
|
||
<h1>Afwezigheid</h1>
|
||
<p className="muted">Leg vast wie wanneer afwezig is. In het rooster zie je dat terug bij de betrokken blokken.</p>
|
||
<Foutmelding msg={error} />
|
||
<form className="form-row" onSubmit={opslaan}>
|
||
<select value={form.persoon} required onChange={(e) => setForm({ ...form, persoon: e.target.value })}>
|
||
<option value="">— persoon —</option>
|
||
{personen.map((p) => <option key={p.id} value={p.id}>{p.volledige_naam} ({p.rol_label})</option>)}
|
||
</select>
|
||
<label className="veld">Van <input type="date" value={form.start_datum} required onChange={(e) => setForm({ ...form, start_datum: e.target.value })} /></label>
|
||
<label className="veld">T/m <input type="date" value={form.eind_datum} required onChange={(e) => setForm({ ...form, eind_datum: e.target.value })} /></label>
|
||
<input placeholder="Reden (optioneel)" value={form.reden} onChange={(e) => setForm({ ...form, reden: e.target.value })} />
|
||
<button className="btn primary" type="submit">Toevoegen</button>
|
||
</form>
|
||
{loading ? <p>Laden…</p> : (
|
||
<table className="grid">
|
||
<thead><tr><th>Persoon</th><th>Periode</th><th>Reden</th><th></th></tr></thead>
|
||
<tbody>
|
||
{items.map((a) => (
|
||
<tr key={a.id}>
|
||
<td>{a.persoon_naam}</td>
|
||
<td className="muted">{a.start_datum} t/m {a.eind_datum}</td>
|
||
<td className="muted">{a.reden || "—"}</td>
|
||
<td className="row-actions"><button className="link danger" onClick={() => verwijder(a.id)}>verwijderen</button></td>
|
||
</tr>
|
||
))}
|
||
{items.length === 0 && <tr><td colSpan="4" className="muted">Geen afwezigheden.</td></tr>}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* ----------------------------- Groepsindeling ----------------------------- */
|
||
// Gezamenlijk beheer: bovenliggende groep -> groep -> subgroep.
|
||
export function GroepsindelingPage() {
|
||
const [leerpleinen, setLeerpleinen] = useState([]);
|
||
const [groepen, setGroepen] = useState([]);
|
||
const [subgroepen, setSubgroepen] = useState([]);
|
||
const [leerlingen, setLeerlingen] = useState([]);
|
||
const [medewerkers, setMedewerkers] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState(null);
|
||
|
||
const leegLeerplein = { naam: "", omschrijving: "", leerlingen: [], medewerkers: [] };
|
||
const leegGroep = { naam: "", omschrijving: "", leerplein: "", leerlingen: [] };
|
||
const leegSubgroep = { naam: "", omschrijving: "", groep: "", leerplein: "", ledenBron: "groep", leden: [], begeleiders: [] };
|
||
const [leerpleinForm, setLeerpleinForm] = useState(leegLeerplein);
|
||
const [groepForm, setGroepForm] = useState(leegGroep);
|
||
const [subgroepForm, setSubgroepForm] = useState(leegSubgroep);
|
||
const [editLeerpleinId, setEditLeerpleinId] = useState(null);
|
||
const [editGroepId, setEditGroepId] = useState(null);
|
||
const [editSubgroepId, setEditSubgroepId] = useState(null);
|
||
|
||
async function laadAlles() {
|
||
setLoading(true);
|
||
try {
|
||
const [lp, gr, sg, ll, ps] = await Promise.all([
|
||
listResource("leerpleinen"),
|
||
listResource("groepen"),
|
||
listResource("subgroepen"),
|
||
listResource("personen", "?rol=leerling"),
|
||
listResource("personen"),
|
||
]);
|
||
setLeerpleinen(lp);
|
||
setGroepen(gr);
|
||
setSubgroepen(sg);
|
||
setLeerlingen(ll);
|
||
setMedewerkers(ps.filter((p) => p.rol !== "leerling"));
|
||
setError(null);
|
||
} catch (err) {
|
||
setError(err.message);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
useEffect(() => { laadAlles(); }, []);
|
||
|
||
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 leerpleinVanGroep = (groepId) => leerpleinen.find((lp) => (lp.groepen || []).includes(Number(groepId)));
|
||
const leerpleinVanSubgroep = (subgroepId) => leerpleinen.find((lp) => (lp.subgroepen || []).includes(Number(subgroepId)));
|
||
const leerlingenVanGroep = (groepId) => leerlingen.filter((l) => l.groep === Number(groepId));
|
||
function leerlingenVanLeerplein(leerpleinId) {
|
||
const lp = leerpleinen.find((x) => x.id === Number(leerpleinId));
|
||
if (!lp) return [];
|
||
const ids = new Set(lp.leerlingen || []);
|
||
leerlingen.forEach((l) => { if ((lp.groepen || []).includes(l.groep)) ids.add(l.id); });
|
||
(lp.subgroepen || []).forEach((sid) => {
|
||
const s = subgroepen.find((x) => x.id === sid);
|
||
(s?.leden_namen || []).forEach((lid) => ids.add(lid.id));
|
||
});
|
||
return leerlingen.filter((l) => ids.has(l.id));
|
||
}
|
||
const subgroepLeerlingen = subgroepForm.ledenBron === "leerplein"
|
||
? leerlingenVanLeerplein(subgroepForm.leerplein)
|
||
: leerlingenVanGroep(subgroepForm.groep);
|
||
|
||
function Vinkjes({ items, geselecteerd, toggle, leeg = "Geen leerlingen gevonden." }) {
|
||
return (
|
||
<div className="check-grid">
|
||
{items.map((item) => (
|
||
<label className="check-item" key={item.id}>
|
||
<input type="checkbox" checked={geselecteerd.includes(item.id)} onChange={() => toggle(item.id)} />
|
||
<span>{item.volledige_naam || item.naam}</span>
|
||
</label>
|
||
))}
|
||
{items.length === 0 && <span className="muted small">{leeg}</span>}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
async function syncGroepLeerplein(groepId, leerpleinId) {
|
||
const gid = Number(groepId);
|
||
const lpid = Number(leerpleinId);
|
||
for (const lp of leerpleinen) {
|
||
const heeft = (lp.groepen || []).includes(gid);
|
||
const moet = lp.id === lpid;
|
||
if (moet && !heeft) await updateResource("leerpleinen", lp.id, { groepen: [...(lp.groepen || []), gid] });
|
||
if (!moet && heeft) await updateResource("leerpleinen", lp.id, { groepen: (lp.groepen || []).filter((id) => id !== gid) });
|
||
}
|
||
}
|
||
async function syncSubgroepLeerplein(subgroepId, leerpleinId) {
|
||
const sid = Number(subgroepId);
|
||
const lpid = leerpleinId ? Number(leerpleinId) : null;
|
||
for (const lp of leerpleinen) {
|
||
const heeft = (lp.subgroepen || []).includes(sid);
|
||
const moet = lp.id === lpid;
|
||
if (moet && !heeft) await updateResource("leerpleinen", lp.id, { subgroepen: [...(lp.subgroepen || []), sid] });
|
||
if (!moet && heeft) await updateResource("leerpleinen", lp.id, { subgroepen: (lp.subgroepen || []).filter((id) => id !== sid) });
|
||
}
|
||
}
|
||
async function syncGroepLeerlingen(groepId, gekozenIds) {
|
||
const gid = Number(groepId);
|
||
for (const l of leerlingen) {
|
||
const gekozen = gekozenIds.includes(l.id);
|
||
if (gekozen && l.groep !== gid) await updateResource("personen", l.id, { groep: gid });
|
||
if (!gekozen && l.groep === gid) await updateResource("personen", l.id, { groep: null });
|
||
}
|
||
}
|
||
|
||
async function opslaanLeerplein(e) {
|
||
e.preventDefault();
|
||
try {
|
||
const payload = { ...leerpleinForm };
|
||
if (editLeerpleinId) await updateResource("leerpleinen", editLeerpleinId, payload);
|
||
else await createResource("leerpleinen", payload);
|
||
setLeerpleinForm(leegLeerplein);
|
||
setEditLeerpleinId(null);
|
||
await laadAlles();
|
||
} catch (err) { setError(err.message); }
|
||
}
|
||
async function opslaanGroep(e) {
|
||
e.preventDefault();
|
||
if (!groepForm.leerplein) { setError(`Kies een ${BOVENGROEP_LABEL_LC} voor deze groep.`); return; }
|
||
try {
|
||
const payload = { naam: groepForm.naam, omschrijving: groepForm.omschrijving };
|
||
const opgeslagen = editGroepId ? await updateResource("groepen", editGroepId, payload) : await createResource("groepen", payload);
|
||
await syncGroepLeerplein(opgeslagen.id, groepForm.leerplein);
|
||
await syncGroepLeerlingen(opgeslagen.id, groepForm.leerlingen);
|
||
setGroepForm(leegGroep);
|
||
setEditGroepId(null);
|
||
await laadAlles();
|
||
} catch (err) { setError(err.message); }
|
||
}
|
||
async function opslaanSubgroep(e) {
|
||
e.preventDefault();
|
||
if (!subgroepForm.groep) { setError("Kies een groep waar deze subgroep onder valt."); return; }
|
||
if (subgroepForm.ledenBron === "leerplein" && !subgroepForm.leerplein) { setError(`Kies een ${BOVENGROEP_LABEL_LC} voor leden uit de volledige ${BOVENGROEP_LABEL_LC}.`); return; }
|
||
try {
|
||
const payload = {
|
||
naam: subgroepForm.naam,
|
||
omschrijving: subgroepForm.omschrijving,
|
||
groep: subgroepForm.groep,
|
||
leden: subgroepForm.leden,
|
||
begeleiders: subgroepForm.begeleiders,
|
||
};
|
||
const opgeslagen = editSubgroepId ? await updateResource("subgroepen", editSubgroepId, payload) : await createResource("subgroepen", payload);
|
||
await syncSubgroepLeerplein(opgeslagen.id, subgroepForm.leerplein);
|
||
setSubgroepForm(leegSubgroep);
|
||
setEditSubgroepId(null);
|
||
await laadAlles();
|
||
} catch (err) { setError(err.message); }
|
||
}
|
||
async function verwijderLeerplein(id) {
|
||
const lp = leerpleinen.find((x) => x.id === id);
|
||
if ((lp?.groepen || []).length > 0) { setError(`Verplaats of verwijder eerst de groepen onder deze ${BOVENGROEP_LABEL_LC}.`); return; }
|
||
if (!confirm(`Deze ${BOVENGROEP_LABEL_LC} verwijderen?`)) return;
|
||
try { await deleteResource("leerpleinen", id); await laadAlles(); } catch (err) { setError(err.message); }
|
||
}
|
||
async function verwijderGroep(id) {
|
||
if (!confirm("Deze groep verwijderen?")) return;
|
||
try { await deleteResource("groepen", id); await laadAlles(); } catch (err) { setError(err.message); }
|
||
}
|
||
async function verwijderSubgroep(id) {
|
||
if (!confirm("Deze subgroep verwijderen?")) return;
|
||
try { await syncSubgroepLeerplein(id, null); await deleteResource("subgroepen", id); await laadAlles(); } catch (err) { setError(err.message); }
|
||
}
|
||
function bewerkLeerplein(lp) {
|
||
setEditLeerpleinId(lp.id);
|
||
setLeerpleinForm({ naam: lp.naam, omschrijving: lp.omschrijving || "", leerlingen: lp.leerlingen || [], medewerkers: lp.medewerkers || [] });
|
||
}
|
||
function bewerkGroep(g) {
|
||
const lp = leerpleinVanGroep(g.id);
|
||
setEditGroepId(g.id);
|
||
setGroepForm({ naam: g.naam, omschrijving: g.omschrijving || "", leerplein: lp?.id || "", leerlingen: leerlingen.filter((l) => l.groep === g.id).map((l) => l.id) });
|
||
}
|
||
function bewerkSubgroep(s) {
|
||
const lp = leerpleinVanSubgroep(s.id) || leerpleinVanGroep(s.groep);
|
||
setEditSubgroepId(s.id);
|
||
setSubgroepForm({
|
||
naam: s.naam, omschrijving: s.omschrijving || "", groep: s.groep || "",
|
||
leerplein: lp?.id || "", ledenBron: lp ? "leerplein" : "groep",
|
||
leden: s.leden_namen.map((x) => x.id), begeleiders: s.begeleiders_namen.map((x) => x.id),
|
||
});
|
||
}
|
||
|
||
return (
|
||
<section>
|
||
<h1>Groepsindeling</h1>
|
||
<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} />
|
||
|
||
<form className="form-card indeling-form" onSubmit={opslaanLeerplein}>
|
||
<h2>{BOVENGROEP_LABEL}</h2>
|
||
<div className="form-row">
|
||
<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 })} />
|
||
</div>
|
||
<div className="leden-keuze">
|
||
<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) }))} />
|
||
</div>
|
||
<div className="leden-keuze">
|
||
<div className="muted small">Medewerkers:</div>
|
||
<Vinkjes items={medewerkers} geselecteerd={leerpleinForm.medewerkers} toggle={(id) => setLeerpleinForm((f) => ({ ...f, medewerkers: toggleArray(f.medewerkers, id) }))} leeg="Geen medewerkers gevonden." />
|
||
</div>
|
||
<div className="form-row">
|
||
<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>}
|
||
</div>
|
||
</form>
|
||
|
||
<form className="form-card indeling-form" onSubmit={opslaanGroep}>
|
||
<h2>Groep</h2>
|
||
<div className="form-row">
|
||
<select value={groepForm.leerplein} required onChange={(e) => setGroepForm({ ...groepForm, leerplein: e.target.value })}>
|
||
<option value="">— {BOVENGROEP_LABEL_LC} —</option>
|
||
{leerpleinen.map((lp) => <option key={lp.id} value={lp.id}>{lp.naam}</option>)}
|
||
</select>
|
||
<input placeholder="Naam groep" value={groepForm.naam} required onChange={(e) => setGroepForm({ ...groepForm, naam: e.target.value })} />
|
||
<input placeholder="Omschrijving (optioneel)" value={groepForm.omschrijving} onChange={(e) => setGroepForm({ ...groepForm, omschrijving: e.target.value })} />
|
||
</div>
|
||
<div className="leden-keuze">
|
||
<div className="lijst-kop">
|
||
<span className="muted small">Leerlingen op schoolniveau in deze groep:</span>
|
||
<span>
|
||
<button type="button" className="link small" onClick={() => setGroepForm((f) => ({ ...f, leerlingen: leerlingen.map((l) => l.id) }))}>alles</button>
|
||
<button type="button" className="link small" onClick={() => setGroepForm((f) => ({ ...f, leerlingen: [] }))}>geen</button>
|
||
</span>
|
||
</div>
|
||
<Vinkjes items={leerlingen} geselecteerd={groepForm.leerlingen} toggle={(id) => setGroepForm((f) => ({ ...f, leerlingen: toggleArray(f.leerlingen, id) }))} />
|
||
</div>
|
||
<div className="form-row">
|
||
<button className="btn primary" type="submit">{editGroepId ? "Groep opslaan" : "Groep toevoegen"}</button>
|
||
{editGroepId && <button type="button" className="btn" onClick={() => { setEditGroepId(null); setGroepForm(leegGroep); }}>Annuleren</button>}
|
||
</div>
|
||
</form>
|
||
|
||
<form className="form-card indeling-form" onSubmit={opslaanSubgroep}>
|
||
<h2>Subgroep</h2>
|
||
<div className="form-row">
|
||
<select value={subgroepForm.groep} required onChange={(e) => {
|
||
const groepId = e.target.value;
|
||
const lp = leerpleinVanGroep(groepId);
|
||
setSubgroepForm({ ...subgroepForm, groep: groepId, leerplein: subgroepForm.leerplein || lp?.id || "" });
|
||
}}>
|
||
<option value="">— groep —</option>
|
||
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam} ({leerpleinVanGroep(g.id)?.naam || `geen ${BOVENGROEP_LABEL_LC}`})</option>)}
|
||
</select>
|
||
<select value={subgroepForm.leerplein} onChange={(e) => setSubgroepForm({ ...subgroepForm, leerplein: e.target.value })}>
|
||
<option value="">— koppel ook aan {BOVENGROEP_LABEL_LC} —</option>
|
||
{leerpleinen.map((lp) => <option key={lp.id} value={lp.id}>{lp.naam}</option>)}
|
||
</select>
|
||
<input placeholder="Naam subgroep" value={subgroepForm.naam} required onChange={(e) => setSubgroepForm({ ...subgroepForm, naam: e.target.value })} />
|
||
<input placeholder="Omschrijving (optioneel)" value={subgroepForm.omschrijving} onChange={(e) => setSubgroepForm({ ...subgroepForm, omschrijving: e.target.value })} />
|
||
</div>
|
||
<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 === "leerplein"} onChange={() => setSubgroepForm({ ...subgroepForm, ledenBron: "leerplein" })} /> leerlingen uit volledige {BOVENGROEP_LABEL_LC}</label>
|
||
</div>
|
||
<div className="leden-keuze">
|
||
<div className="lijst-kop">
|
||
<span className="muted small">Leerlingen in subgroep:</span>
|
||
<span>
|
||
<button type="button" className="link small" onClick={() => setSubgroepForm((f) => ({ ...f, leden: subgroepLeerlingen.map((l) => l.id) }))}>alles</button>
|
||
<button type="button" className="link small" onClick={() => setSubgroepForm((f) => ({ ...f, leden: [] }))}>geen</button>
|
||
</span>
|
||
</div>
|
||
<Vinkjes items={subgroepLeerlingen} geselecteerd={subgroepForm.leden} toggle={(id) => setSubgroepForm((f) => ({ ...f, leden: toggleArray(f.leden, id) }))} />
|
||
</div>
|
||
<div className="leden-keuze">
|
||
<div className="muted small">Begeleiders:</div>
|
||
<Vinkjes items={medewerkers} geselecteerd={subgroepForm.begeleiders} toggle={(id) => setSubgroepForm((f) => ({ ...f, begeleiders: toggleArray(f.begeleiders, id) }))} leeg="Geen medewerkers gevonden." />
|
||
</div>
|
||
<div className="form-row">
|
||
<button className="btn primary" type="submit">{editSubgroepId ? "Subgroep opslaan" : "Subgroep toevoegen"}</button>
|
||
{editSubgroepId && <button type="button" className="btn" onClick={() => { setEditSubgroepId(null); setSubgroepForm(leegSubgroep); }}>Annuleren</button>}
|
||
</div>
|
||
</form>
|
||
|
||
{loading ? <p>Laden…</p> : (
|
||
<div className="indeling-tree">
|
||
{leerpleinen.map((lp) => {
|
||
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));
|
||
return (
|
||
<div className="tree-root" key={lp.id}>
|
||
<div className="tree-node tree-parent">
|
||
<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>
|
||
<div className="tree-children">
|
||
{lpGroepen.map((g) => {
|
||
const groepLeerlingen = leerlingenVanGroep(g.id);
|
||
const groepSubs = lpSubgroepen.filter((s) => s.groep === g.id);
|
||
return (
|
||
<div className="tree-branch" key={g.id}>
|
||
<div className="tree-node tree-group">
|
||
<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>
|
||
<div className="tree-meta muted small">{groepLeerlingen.map((l) => l.volledige_naam).join(", ") || "Geen leerlingen"}</div>
|
||
<div className="tree-children tree-subchildren">
|
||
{groepSubs.map((s) => (
|
||
<div className="tree-node tree-subgroup" key={s.id}>
|
||
<div className="tree-node-main">
|
||
<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>
|
||
</div>
|
||
))}
|
||
{groepSubs.length === 0 && <div className="tree-empty muted small">Geen subgroepen.</div>}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
{lpGroepen.length === 0 && <div className="tree-empty muted">Nog geen groepen onder deze {BOVENGROEP_LABEL_LC}.</div>}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
{leerpleinen.length === 0 && <div className="muted">Nog geen bovenliggende groepen.</div>}
|
||
{groepen.filter((g) => !leerpleinVanGroep(g.id)).length > 0 && (
|
||
<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>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* ---------------------------------- Stage --------------------------------- */
|
||
// Module-scherm: interne en externe stages. Beide met begeleider; intern + ruimte.
|
||
export function StagePage() {
|
||
const { items, error, loading, reload, setError } = useList("stages");
|
||
const [leerlingen, setLeerlingen] = useState([]);
|
||
const [medewerkers, setMedewerkers] = useState([]);
|
||
const [locaties, setLocaties] = useState([]);
|
||
const leeg = { leerling: "", type: "intern", begeleider: "", locatie: "", organisatie: "", dag: "", omschrijving: "" };
|
||
const [form, setForm] = useState(leeg);
|
||
const [editId, setEditId] = useState(null);
|
||
|
||
useEffect(() => {
|
||
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
|
||
listResource("personen").then((ps) => setMedewerkers(ps.filter((p) => p.rol !== "leerling"))).catch(() => {});
|
||
listResource("locaties").then(setLocaties).catch(() => {});
|
||
}, []);
|
||
|
||
const isIntern = form.type === "intern";
|
||
|
||
async function opslaan(e) {
|
||
e.preventDefault();
|
||
const payload = {
|
||
leerling: form.leerling, type: form.type, begeleider: form.begeleider,
|
||
locatie: isIntern ? (form.locatie || null) : null,
|
||
organisatie: isIntern ? "" : form.organisatie,
|
||
dag: form.dag === "" ? null : Number(form.dag),
|
||
omschrijving: form.omschrijving,
|
||
};
|
||
try {
|
||
if (editId) await updateResource("stages", editId, payload);
|
||
else await createResource("stages", payload);
|
||
setForm(leeg);
|
||
setEditId(null);
|
||
await reload();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
async function verwijder(id) {
|
||
if (!confirm("Deze stage verwijderen?")) return;
|
||
try { await deleteResource("stages", id); await reload(); } catch (err) { setError(err.message); }
|
||
}
|
||
|
||
return (
|
||
<section>
|
||
<h1>Stage</h1>
|
||
<p className="muted">Interne en externe stages. Beide krijgen een begeleider; een interne stage ook een ruimte.</p>
|
||
<Foutmelding msg={error} />
|
||
<form className="form-card" onSubmit={opslaan}>
|
||
<div className="form-row">
|
||
<select value={form.leerling} required onChange={(e) => setForm({ ...form, leerling: e.target.value })}>
|
||
<option value="">— leerling —</option>
|
||
{leerlingen.map((l) => <option key={l.id} value={l.id}>{l.volledige_naam}</option>)}
|
||
</select>
|
||
<select value={form.type} onChange={(e) => setForm({ ...form, type: e.target.value })}>
|
||
<option value="intern">Interne stage</option>
|
||
<option value="extern">Externe stage</option>
|
||
</select>
|
||
<select value={form.begeleider} required onChange={(e) => setForm({ ...form, begeleider: e.target.value })}>
|
||
<option value="">— begeleider —</option>
|
||
{medewerkers.map((m) => <option key={m.id} value={m.id}>{m.volledige_naam}</option>)}
|
||
</select>
|
||
</div>
|
||
<div className="form-row">
|
||
{isIntern ? (
|
||
<select value={form.locatie} required onChange={(e) => setForm({ ...form, locatie: e.target.value })}>
|
||
<option value="">— ruimte —</option>
|
||
{locaties.map((l) => <option key={l.id} value={l.id}>{l.naam}</option>)}
|
||
</select>
|
||
) : (
|
||
<input placeholder="Externe organisatie" value={form.organisatie} required onChange={(e) => setForm({ ...form, organisatie: e.target.value })} />
|
||
)}
|
||
<select value={form.dag} onChange={(e) => setForm({ ...form, dag: e.target.value })}>
|
||
<option value="">— dag (optioneel) —</option>
|
||
{WEEKDAGEN.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
|
||
</select>
|
||
<input placeholder="Opmerking (optioneel)" value={form.omschrijving} onChange={(e) => setForm({ ...form, omschrijving: e.target.value })} />
|
||
</div>
|
||
<div className="form-row">
|
||
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
|
||
{editId && <button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>}
|
||
</div>
|
||
</form>
|
||
{loading ? <p>Laden…</p> : (
|
||
<table className="grid">
|
||
<thead><tr><th>Leerling</th><th>Type</th><th>Begeleider</th><th>Waar</th><th>Dag</th><th></th></tr></thead>
|
||
<tbody>
|
||
{items.map((s) => (
|
||
<tr key={s.id}>
|
||
<td>{s.leerling_naam}</td>
|
||
<td>{s.type_label}</td>
|
||
<td>{s.begeleider_naam}</td>
|
||
<td className="muted">{s.type === "intern" ? (s.locatie_naam || "—") : (s.organisatie || "—")}</td>
|
||
<td className="muted">{s.dag_label || "—"}</td>
|
||
<td className="row-actions">
|
||
<button className="link" onClick={() => { setEditId(s.id); setForm({ leerling: s.leerling, type: s.type, begeleider: s.begeleider, locatie: s.locatie || "", organisatie: s.organisatie || "", dag: s.dag ?? "", omschrijving: s.omschrijving || "" }); }}>bewerken</button>
|
||
<button className="link danger" onClick={() => verwijder(s.id)}>verwijderen</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
{items.length === 0 && <tr><td colSpan="6" className="muted">Nog geen stages.</td></tr>}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* -------------------------------- Gebruikers ------------------------------ */
|
||
const GEBRUIKERSROLLEN = [
|
||
{ value: "beheerder", label: "Beheerder" },
|
||
{ value: "medewerker", label: "Medewerker" },
|
||
{ value: "ouder", label: "Ouder" },
|
||
{ value: "leerling", label: "Leerling" },
|
||
];
|
||
|
||
export function GebruikersPage() {
|
||
const { items, error, loading, reload, setError } = useList("gebruikers");
|
||
const [personen, setPersonen] = useState([]);
|
||
const leeg = { username: "", password: "", first_name: "", last_name: "", email: "", rol: "medewerker", persoon: "", is_staff: false, is_active: true };
|
||
const [form, setForm] = useState(leeg);
|
||
const [editId, setEditId] = useState(null);
|
||
|
||
useEffect(() => { listResource("personen").then(setPersonen).catch(() => {}); }, []);
|
||
|
||
async function opslaan(e) {
|
||
e.preventDefault();
|
||
const payload = {
|
||
username: form.username, first_name: form.first_name, last_name: form.last_name,
|
||
email: form.email, rol: form.rol, persoon: form.persoon || null,
|
||
is_staff: form.is_staff, is_active: form.is_active,
|
||
};
|
||
if (form.password) payload.password = form.password;
|
||
try {
|
||
if (editId) await updateResource("gebruikers", editId, payload);
|
||
else await createResource("gebruikers", payload);
|
||
setForm(leeg);
|
||
setEditId(null);
|
||
await reload();
|
||
} catch (err) { setError(err.message); }
|
||
}
|
||
async function verwijder(id) {
|
||
if (!confirm("Deze gebruiker verwijderen?")) return;
|
||
try { await deleteResource("gebruikers", id); await reload(); } catch (err) { setError(err.message); }
|
||
}
|
||
function bewerk(u) {
|
||
setEditId(u.id);
|
||
setForm({ username: u.username, password: "", first_name: u.first_name || "", last_name: u.last_name || "", email: u.email || "", rol: u.rol || "medewerker", persoon: u.persoon || "", is_staff: u.is_staff, is_active: u.is_active });
|
||
}
|
||
|
||
return (
|
||
<section>
|
||
<h1>Gebruikers</h1>
|
||
<p className="muted">Beheer inloggers en hun rol. Alleen beheerders kunnen dit scherm gebruiken.</p>
|
||
<Foutmelding msg={error} />
|
||
<form className="form-card" onSubmit={opslaan}>
|
||
<div className="form-row">
|
||
<input placeholder="Gebruikersnaam" value={form.username} required onChange={(e) => setForm({ ...form, username: e.target.value })} />
|
||
<input type="password" placeholder={editId ? "Wachtwoord (leeg = ongewijzigd)" : "Wachtwoord"} value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} />
|
||
<select value={form.rol} onChange={(e) => setForm({ ...form, rol: e.target.value })}>
|
||
{GEBRUIKERSROLLEN.map((r) => <option key={r.value} value={r.value}>{r.label}</option>)}
|
||
</select>
|
||
</div>
|
||
<div className="form-row">
|
||
<input placeholder="Voornaam" value={form.first_name} onChange={(e) => setForm({ ...form, first_name: e.target.value })} />
|
||
<input placeholder="Achternaam" value={form.last_name} onChange={(e) => setForm({ ...form, last_name: e.target.value })} />
|
||
<input type="email" placeholder="E-mail" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} />
|
||
</div>
|
||
<div className="form-row">
|
||
<select value={form.persoon || ""} onChange={(e) => setForm({ ...form, persoon: e.target.value })}>
|
||
<option value="">— koppel aan persoon (optioneel) —</option>
|
||
{personen.map((p) => <option key={p.id} value={p.id}>{p.volledige_naam} ({p.rol_label})</option>)}
|
||
</select>
|
||
<label className="veld"><input type="checkbox" checked={form.is_staff} onChange={(e) => setForm({ ...form, is_staff: e.target.checked })} /> beheerder (is_staff)</label>
|
||
<label className="veld"><input type="checkbox" checked={form.is_active} onChange={(e) => setForm({ ...form, is_active: e.target.checked })} /> actief</label>
|
||
</div>
|
||
<div className="form-row">
|
||
<button className="btn primary" type="submit">{editId ? "Opslaan" : "Toevoegen"}</button>
|
||
{editId && <button type="button" className="btn" onClick={() => { setEditId(null); setForm(leeg); }}>Annuleren</button>}
|
||
</div>
|
||
</form>
|
||
{loading ? <p>Laden…</p> : (
|
||
<table className="grid">
|
||
<thead><tr><th>Gebruiker</th><th>Rol</th><th>Persoon</th><th>Status</th><th></th></tr></thead>
|
||
<tbody>
|
||
{items.map((u) => (
|
||
<tr key={u.id}>
|
||
<td>{u.username}{(u.first_name || u.last_name) && <div className="muted small">{u.first_name} {u.last_name}</div>}</td>
|
||
<td>{u.rol_label || "—"}{u.is_staff && <span className="tag core"> beheer</span>}</td>
|
||
<td className="muted">{u.persoon_naam || "—"}</td>
|
||
<td className="muted small">{u.is_active ? "actief" : "inactief"}</td>
|
||
<td className="row-actions">
|
||
<button className="link" onClick={() => bewerk(u)}>bewerken</button>
|
||
<button className="link danger" onClick={() => verwijder(u.id)}>verwijderen</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
{items.length === 0 && <tr><td colSpan="5" className="muted">Geen gebruikers.</td></tr>}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* -------------------------------- Conflicten ------------------------------ */
|
||
function Conflicten({ schooljaar }) {
|
||
const [data, setData] = useState(null);
|
||
const [error, setError] = useState(null);
|
||
useEffect(() => {
|
||
if (!schooljaar) return;
|
||
listResource("conflicten", `?schooljaar=${schooljaar}`).then(setData).catch((e) => setError(e.message));
|
||
}, [schooljaar]);
|
||
if (!schooljaar) return <p className="muted">Kies eerst een schooljaar.</p>;
|
||
if (error) return <Foutmelding msg={error} />;
|
||
if (!data) return <p>Laden…</p>;
|
||
return (
|
||
<div>
|
||
<p className="muted">{data.aantal === 0 ? "Geen conflicten gevonden 🎉" : `${data.fouten} fout(en) en ${data.waarschuwingen} waarschuwing(en).`}</p>
|
||
{data.conflicten.map((c, i) => (
|
||
<div className={c.ernst === "fout" ? "conflict fout" : "conflict waarschuwing"} key={i}>
|
||
<div className="conflict-kop"><strong>{c.tijdslot}</strong> <span className="tag">{c.type}</span></div>
|
||
<div>{c.detail}</div>
|
||
<div className="muted small">Blokken: {c.blokken.join(", ")}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ------------------------------ Datumhulpjes ------------------------------ */
|
||
function pad2(n) { return String(n).padStart(2, "0"); }
|
||
function fmtISO(d) { return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; }
|
||
function fmtDM(d) { return `${pad2(d.getDate())}-${pad2(d.getMonth() + 1)}`; }
|
||
function maandagVan(d) { const x = new Date(d); x.setHours(0, 0, 0, 0); const wd = (x.getDay() + 6) % 7; x.setDate(x.getDate() - wd); return x; }
|
||
function plusDagen(d, n) { const x = new Date(d); x.setDate(x.getDate() + n); return x; }
|
||
|
||
/* ------------------------- Rooster-scope (filter/print) ------------------- */
|
||
// Welke leerlingen raakt een blok (via groep, subgroep of individueel)?
|
||
export function leerlingenVanBlok(blok, ctx) {
|
||
if (blok.groep) return ctx.leerlingen.filter((l) => l.groep === blok.groep).map((l) => l.id);
|
||
if (blok.subgroep) {
|
||
const s = ctx.subgroepen.find((x) => x.id === blok.subgroep);
|
||
return s ? s.leden_namen.map((m) => m.id) : [];
|
||
}
|
||
if (blok.leerling) return [blok.leerling];
|
||
return [];
|
||
}
|
||
// Welke leerlingen vallen binnen een gekozen scope (klas/bovenliggende groep/leerling)?
|
||
export function scopeLeerlingen(scope, ctx) {
|
||
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 === "leerplein") {
|
||
const p = ctx.leerpleinen.find((x) => x.id === scope.id);
|
||
if (!p) return [];
|
||
const ids = new Set(p.leerlingen || []);
|
||
(p.groepen || []).forEach((g) => ctx.leerlingen.filter((l) => l.groep === g).forEach((l) => ids.add(l.id)));
|
||
(p.subgroepen || []).forEach((sid) => {
|
||
const s = ctx.subgroepen.find((x) => x.id === sid);
|
||
if (s) s.leden_namen.forEach((m) => ids.add(m.id));
|
||
});
|
||
return [...ids];
|
||
}
|
||
return [];
|
||
}
|
||
// Valt een blok binnen de gekozen scope?
|
||
export function blokInScope(blok, scope, ctx) {
|
||
if (scope.type === "alles" || !scope.id) return true;
|
||
if (scope.type === "personeel") return (blok.begeleiders || []).includes(scope.id);
|
||
const set = new Set(scopeLeerlingen(scope, ctx));
|
||
return leerlingenVanBlok(blok, ctx).some((id) => set.has(id));
|
||
}
|
||
|
||
function groepVanSubgroep(subgroepen, subgroepId) {
|
||
const sub = subgroepen.find((s) => s.id === subgroepId);
|
||
return sub ? sub.groep : null;
|
||
}
|
||
|
||
function bovenliggendeGroep(blok, subgroepen) {
|
||
if (blok.groep) return blok.groep;
|
||
if (blok.subgroep) return groepVanSubgroep(subgroepen, blok.subgroep);
|
||
return null;
|
||
}
|
||
|
||
/* ----------------------------- Blok bewerken/kopiëren --------------------- */
|
||
function BlokBewerker({ blok, ctx, onSluit, onKlaar }) {
|
||
const { activiteiten, locaties, groepen, subgroepen, leerlingen, medewerkers, tijdsloten } = ctx;
|
||
const [f, setF] = useState({
|
||
tijdslot: blok.tijdslot,
|
||
activiteit: blok.activiteit,
|
||
locatie: blok.locatie || "",
|
||
doel_type: blok.doel_type,
|
||
doel: blok.groep || blok.subgroep || blok.leerling || "",
|
||
begeleiders: [...(blok.begeleiders || [])],
|
||
opmerkingen: blok.opmerkingen || "",
|
||
});
|
||
const [kopieDoelen, setKopieDoelen] = useState([]);
|
||
const [error, setError] = useState(null);
|
||
const [subs, setSubs] = useState(subgroepen);
|
||
const [nieuweSub, setNieuweSub] = useState("");
|
||
const [splitSubs, setSplitSubs] = useState([]);
|
||
const [splitNieuw, setSplitNieuw] = useState("");
|
||
const [verwijderOrig, setVerwijderOrig] = useState(true);
|
||
const [subgroepGroepFilter, setSubgroepGroepFilter] = useState(() => (
|
||
blok.subgroep ? groepVanSubgroep(subgroepen, blok.subgroep) || "" : ""
|
||
));
|
||
const gekozenGroep = f.doel_type === "groep"
|
||
? (f.doel ? Number(f.doel) : null)
|
||
: f.doel_type === "subgroep"
|
||
? (subgroepGroepFilter || groepVanSubgroep(subs, Number(f.doel)))
|
||
: bovenliggendeGroep(blok, subs);
|
||
const subgroepOpties = gekozenGroep ? subs.filter((s) => s.groep === gekozenGroep) : subs;
|
||
const doelOpties = f.doel_type === "groep" ? groepen : f.doel_type === "subgroep" ? subgroepOpties : leerlingen;
|
||
const splitGroep = gekozenGroep || blok.groep || groepVanSubgroep(subs, blok.subgroep) || null;
|
||
const splitOpties = splitGroep ? subs.filter((s) => s.groep === splitGroep) : subs;
|
||
const splitGroepNaam = splitGroep ? (groepen.find((g) => g.id === splitGroep)?.naam || "groep") : "groep";
|
||
|
||
async function maakSubgroep() {
|
||
if (!nieuweSub.trim()) return;
|
||
try {
|
||
const s = await createResource("subgroepen", { naam: nieuweSub.trim(), groep: gekozenGroep || null });
|
||
setSubs([...subs, s]);
|
||
setSubgroepGroepFilter(s.groep || "");
|
||
setF({ ...f, doel_type: "subgroep", doel: s.id });
|
||
setNieuweSub("");
|
||
} catch (e) { setError(e.message); }
|
||
}
|
||
const toggleSplit = (id) => setSplitSubs((d) => d.includes(id) ? d.filter((x) => x !== id) : [...d, id]);
|
||
async function maakSplitSub() {
|
||
if (!splitNieuw.trim()) return;
|
||
try {
|
||
const s = await createResource("subgroepen", { naam: splitNieuw.trim(), groep: splitGroep });
|
||
setSubs([...subs, s]);
|
||
setSplitSubs([...splitSubs, s.id]);
|
||
setSplitNieuw("");
|
||
} catch (e) { setError(e.message); }
|
||
}
|
||
async function opdelen() {
|
||
if (splitSubs.length === 0) return;
|
||
try {
|
||
for (const sid of splitSubs) {
|
||
await createResource("roosterblokken", { schooljaar: blok.schooljaar, tijdslot: f.tijdslot, activiteit: f.activiteit, locatie: f.locatie || null, subgroep: sid, begeleiders: f.begeleiders, opmerkingen: f.opmerkingen });
|
||
}
|
||
if (verwijderOrig) await deleteResource("roosterblokken", blok.id);
|
||
await onKlaar();
|
||
} catch (e) { setError(e.message); }
|
||
}
|
||
const toggleBeg = (id) => setF((s) => s.begeleiders.includes(id) ? { ...s, begeleiders: s.begeleiders.filter((x) => x !== id) } : { ...s, begeleiders: [...s.begeleiders, id] });
|
||
const toggleKopie = (id) => setKopieDoelen((d) => d.includes(id) ? d.filter((x) => x !== id) : [...d, id]);
|
||
const doelPayload = () => ({
|
||
groep: f.doel_type === "groep" ? f.doel : null,
|
||
subgroep: f.doel_type === "subgroep" ? f.doel : null,
|
||
leerling: f.doel_type === "leerling" ? f.doel : null,
|
||
});
|
||
async function opslaan() {
|
||
try { await updateResource("roosterblokken", blok.id, { tijdslot: f.tijdslot, activiteit: f.activiteit, locatie: f.locatie || null, ...doelPayload(), begeleiders: f.begeleiders, opmerkingen: f.opmerkingen }); await onKlaar(); }
|
||
catch (e) { setError(e.message); }
|
||
}
|
||
async function kopieer() {
|
||
try {
|
||
for (const ts of kopieDoelen) {
|
||
await createResource("roosterblokken", { schooljaar: blok.schooljaar, tijdslot: ts, activiteit: f.activiteit, locatie: f.locatie || null, ...doelPayload(), begeleiders: f.begeleiders, opmerkingen: f.opmerkingen });
|
||
}
|
||
await onKlaar();
|
||
} catch (e) { setError(e.message); }
|
||
}
|
||
async function verwijderen() {
|
||
if (!confirm("Dit blok (alle weken) verwijderen?")) return;
|
||
try { await deleteResource("roosterblokken", blok.id); await onKlaar(); } catch (e) { setError(e.message); }
|
||
}
|
||
return (
|
||
<div className="modal-overlay no-print" onClick={onSluit}>
|
||
<div className="modal rooster-modal" role="dialog" aria-modal="true" aria-label="Roosterblok bewerken" onClick={(e) => e.stopPropagation()}>
|
||
<div className="modal-kop">
|
||
<div>
|
||
<h2>{blok.activiteit_naam || "Roosterblok"}</h2>
|
||
<div className="muted small">{blok.doel_naam || "Bewerken en splitsen"}</div>
|
||
</div>
|
||
<button type="button" className="icon-btn" title="Sluiten" onClick={onSluit}>×</button>
|
||
</div>
|
||
<div className="blok-editor modal-editor">
|
||
<div className="blok-editor-kop">
|
||
<strong>Vak bewerken</strong>
|
||
<span className="muted small">Wijzig, splits of kopieer dit blok</span>
|
||
</div>
|
||
<Foutmelding msg={error} />
|
||
<div className="editor-grid">
|
||
<select value={f.tijdslot} title="Tijdslot" onChange={(e) => setF({ ...f, tijdslot: e.target.value })}>
|
||
{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={f.activiteit} title="Activiteit" onChange={(e) => setF({ ...f, activiteit: e.target.value })}>
|
||
{activiteiten.map((a) => <option key={a.id} value={a.id}>{a.naam}</option>)}
|
||
</select>
|
||
<select value={f.locatie} title="Locatie" onChange={(e) => setF({ ...f, locatie: e.target.value })}>
|
||
<option value="">— locatie —</option>
|
||
{locaties.map((l) => <option key={l.id} value={l.id}>{l.naam}</option>)}
|
||
</select>
|
||
<input placeholder="Opmerking" value={f.opmerkingen} onChange={(e) => setF({ ...f, opmerkingen: e.target.value })} />
|
||
</div>
|
||
|
||
<details className="editor-detail">
|
||
<summary>Doelgroep en begeleiding</summary>
|
||
<div className="editor-grid">
|
||
<select value={f.doel_type} onChange={(e) => {
|
||
setF({ ...f, doel_type: e.target.value, doel: "" });
|
||
if (e.target.value !== "subgroep") setSubgroepGroepFilter("");
|
||
}}>
|
||
<option value="groep">Groep</option>
|
||
<option value="subgroep">Subgroep</option>
|
||
<option value="leerling">Individuele leerling</option>
|
||
</select>
|
||
<select value={f.doel} onChange={(e) => setF({ ...f, doel: e.target.value })}>
|
||
<option value="">— kies {f.doel_type} —</option>
|
||
{doelOpties.map((o) => <option key={o.id} value={o.id}>{o.naam || o.volledige_naam}</option>)}
|
||
</select>
|
||
{f.doel_type === "subgroep" && (
|
||
<>
|
||
<select value={subgroepGroepFilter || ""} onChange={(e) => { setSubgroepGroepFilter(e.target.value ? Number(e.target.value) : ""); setF({ ...f, doel: "", doel_type: "subgroep" }); }}>
|
||
<option value="">— alle groepen —</option>
|
||
{groepen.map((g) => <option key={g.id} value={g.id}>{g.naam}</option>)}
|
||
</select>
|
||
<input placeholder="Nieuwe subgroep" value={nieuweSub} onChange={(e) => setNieuweSub(e.target.value)} />
|
||
<button type="button" className="btn icon-label" onClick={maakSubgroep} title="Subgroep aanmaken">+ <span>Subgroep</span></button>
|
||
</>
|
||
)}
|
||
</div>
|
||
<div className="chips compact">
|
||
{medewerkers.map((m) => <button type="button" key={m.id} className={f.begeleiders.includes(m.id) ? "chip on" : "chip"} onClick={() => toggleBeg(m.id)} title={m.rol_label || "Begeleider"}>{m.volledige_naam}{m.rol_label ? ` · ${m.rol_label}` : ""}</button>)}
|
||
</div>
|
||
</details>
|
||
|
||
<div className="editor-actions">
|
||
<button type="button" className="btn primary icon-label" onClick={opslaan} title="Opslaan">✓ <span>Opslaan</span></button>
|
||
<button type="button" className="btn icon-label" onClick={verwijderen} title="Verwijderen">🗑 <span>Verwijderen</span></button>
|
||
</div>
|
||
|
||
<details className="editor-detail split-editor" open>
|
||
<summary>Splitsen onder {splitGroepNaam}</summary>
|
||
<div className="chips compact">
|
||
{splitOpties.map((s) => <button type="button" key={s.id} className={splitSubs.includes(s.id) ? "chip on" : "chip"} onClick={() => toggleSplit(s.id)}>{s.naam}</button>)}
|
||
{splitOpties.length === 0 && <span className="muted small">Nog geen subgroepen onder deze groep.</span>}
|
||
</div>
|
||
{splitSubs.length > 0 && (
|
||
<div className="split-preview">
|
||
<div className="split-preview-root">{splitGroepNaam}</div>
|
||
<div className="split-preview-children">
|
||
{splitSubs.map((sid) => {
|
||
const sub = splitOpties.find((s) => s.id === sid) || subs.find((s) => s.id === sid);
|
||
return <span key={sid}>{sub ? sub.naam : "Subgroep"}</span>;
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div className="editor-grid">
|
||
<input placeholder="Nieuwe subgroep" value={splitNieuw} onChange={(e) => setSplitNieuw(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); maakSplitSub(); } }} />
|
||
<button type="button" className="btn icon-label" onClick={maakSplitSub} title="Subgroep toevoegen">+ <span>Subgroep</span></button>
|
||
</div>
|
||
<label className="veld editor-check"><input type="checkbox" checked={verwijderOrig} onChange={(e) => setVerwijderOrig(e.target.checked)} /> oorspronkelijke blok verwijderen</label>
|
||
<button type="button" className="btn primary icon-label" disabled={splitSubs.length === 0} onClick={opdelen} title="Opdelen">✂ <span>{splitSubs.length}</span></button>
|
||
</details>
|
||
|
||
<details className="editor-detail">
|
||
<summary>Kopiëren</summary>
|
||
<div className="chips compact">
|
||
{tijdsloten.map((t) => <button type="button" key={t.id} className={kopieDoelen.includes(t.id) ? "chip on" : "chip"} onClick={() => toggleKopie(t.id)}>{t.dag_label} {t.start_tijd?.slice(0, 5)} {t.naam}</button>)}
|
||
</div>
|
||
<button type="button" className="btn icon-label" disabled={kopieDoelen.length === 0} onClick={kopieer} title="Kopiëren">⧉ <span>{kopieDoelen.length}</span></button>
|
||
</details>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ------------------------------- Groepsindeling --------------------------- */
|
||
// Verdeel een hoofdgroep (of bovenliggende groep) kolomsgewijs in subgroepen: per kolom
|
||
// de subgroep met haar verantwoordelijke(n) en de vakken die er gegeven worden.
|
||
function Groepsindeling({ groepen, leerpleinen, subgroepen, medewerkers, blokken, onWijzig }) {
|
||
const [type, setType] = useState("groep");
|
||
const [hoofdId, setHoofdId] = useState("");
|
||
const [nieuw, setNieuw] = useState({ naam: "", groep: "", begeleiders: [] });
|
||
const [error, setError] = useState(null);
|
||
|
||
const opties = type === "groep" ? groepen : leerpleinen;
|
||
const hid = hoofdId ? Number(hoofdId) : null;
|
||
const hoofd = opties.find((o) => o.id === hid);
|
||
|
||
let subs = [];
|
||
if (hid && type === "groep") subs = subgroepen.filter((s) => s.groep === hid);
|
||
if (hid && type === "leerplein" && hoofd) subs = subgroepen.filter((s) => (hoofd.subgroepen || []).includes(s.id));
|
||
const leerpleinGroepen = type === "leerplein" && hoofd
|
||
? groepen.filter((g) => (hoofd.groepen || []).includes(g.id))
|
||
: [];
|
||
const groepenMetSubgroepen = type === "leerplein"
|
||
? leerpleinGroepen.map((g) => ({ groep: g, subs: subgroepen.filter((s) => s.groep === g.id) }))
|
||
: [];
|
||
|
||
const vakkenVan = (subId) => [...new Set(blokken.filter((b) => b.subgroep === subId).map((b) => b.activiteit_naam))];
|
||
const toggleBeg = (id) => setNieuw((n) => n.begeleiders.includes(id) ? { ...n, begeleiders: n.begeleiders.filter((x) => x !== id) } : { ...n, begeleiders: [...n.begeleiders, id] });
|
||
|
||
async function voegToe(e) {
|
||
e.preventDefault();
|
||
try {
|
||
await createResource("subgroepen", { naam: nieuw.naam, groep: type === "groep" ? hid : nieuw.groep, begeleiders: nieuw.begeleiders });
|
||
setNieuw({ naam: "", groep: "", begeleiders: [] });
|
||
await onWijzig();
|
||
} catch (err) { setError(err.message); }
|
||
}
|
||
async function verwijderSub(id) {
|
||
if (!confirm("Deze subgroep verwijderen?")) return;
|
||
try { await deleteResource("subgroepen", id); await onWijzig(); } catch (err) { setError(err.message); }
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<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} />
|
||
<div className="rooster-balk">
|
||
<label className="veld">Type
|
||
<select value={type} onChange={(e) => { setType(e.target.value); setHoofdId(""); setNieuw({ naam: "", groep: "", begeleiders: [] }); }}>
|
||
<option value="groep">Groep</option>
|
||
<option value="leerplein">{BOVENGROEP_LABEL}</option>
|
||
</select>
|
||
</label>
|
||
<select value={hoofdId} onChange={(e) => setHoofdId(e.target.value)}>
|
||
<option value="">— kies {type} —</option>
|
||
{opties.map((o) => <option key={o.id} value={o.id}>{o.naam}</option>)}
|
||
</select>
|
||
</div>
|
||
|
||
{hid && hoofd && (
|
||
<div className="indeling">
|
||
<div className="indeling-hoofd">{hoofd.naam} <span className="muted small">— {type}</span></div>
|
||
{type === "leerplein" ? (
|
||
<div className="indeling-groepen">
|
||
{groepenMetSubgroepen.map(({ groep, subs: groepSubs }) => (
|
||
<div className="indeling-groep" key={groep.id}>
|
||
<div className="indeling-groep-kop">{groep.naam}</div>
|
||
<div className="indeling-kolommen">
|
||
{groepSubs.map((s) => (
|
||
<div className="indeling-kolom" key={s.id}>
|
||
<div className="indeling-kop">{s.naam}</div>
|
||
<div className="small">Verantwoordelijke: {s.begeleiders_namen && s.begeleiders_namen.length ? s.begeleiders_namen.map((x) => x.naam).join(", ") : "geen"}</div>
|
||
<div className="small">Vakken: {vakkenVan(s.id).join(", ") || "nog geen vakken"}</div>
|
||
<div className="small muted">{s.aantal_leden} leerling(en)</div>
|
||
<button className="link danger small" onClick={() => verwijderSub(s.id)}>verwijderen</button>
|
||
</div>
|
||
))}
|
||
{groepSubs.length === 0 && <div className="muted small">Nog geen subgroepen onder {groep.naam}.</div>}
|
||
</div>
|
||
</div>
|
||
))}
|
||
{leerpleinGroepen.length === 0 && <div className="muted">Nog geen groepen onder {hoofd.naam}.</div>}
|
||
</div>
|
||
) : (
|
||
<div className="indeling-kolommen">
|
||
{subs.map((s) => (
|
||
<div className="indeling-kolom" key={s.id}>
|
||
<div className="indeling-kop">{s.naam}</div>
|
||
<div className="small">Verantwoordelijke: {s.begeleiders_namen && s.begeleiders_namen.length ? s.begeleiders_namen.map((x) => x.naam).join(", ") : "geen"}</div>
|
||
<div className="small">Vakken: {vakkenVan(s.id).join(", ") || "nog geen vakken"}</div>
|
||
<div className="small muted">{s.aantal_leden} leerling(en)</div>
|
||
<button className="link danger small" onClick={() => verwijderSub(s.id)}>verwijderen</button>
|
||
</div>
|
||
))}
|
||
{subs.length === 0 && <div className="muted">Nog geen subgroepen onder {hoofd.naam}.</div>}
|
||
</div>
|
||
)}
|
||
|
||
<form className="form-card" onSubmit={voegToe}>
|
||
<div className="muted small">Subgroep toevoegen onder {type === "groep" ? hoofd.naam : `een groep binnen deze ${BOVENGROEP_LABEL_LC}`}:</div>
|
||
<div className="form-row">
|
||
{type === "leerplein" && (
|
||
<select value={nieuw.groep || ""} required onChange={(e) => setNieuw({ ...nieuw, groep: e.target.value })}>
|
||
<option value="">— groep —</option>
|
||
{leerpleinGroepen.map((g) => <option key={g.id} value={g.id}>{g.naam}</option>)}
|
||
</select>
|
||
)}
|
||
<input placeholder="Naam subgroep" value={nieuw.naam} required onChange={(e) => setNieuw({ ...nieuw, naam: e.target.value })} />
|
||
<button className="btn primary" type="submit">+ Subgroep</button>
|
||
</div>
|
||
<div className="leden-keuze">
|
||
<div className="muted small">Verantwoordelijke(n):</div>
|
||
<div className="chips">
|
||
{medewerkers.map((m) => <button type="button" key={m.id} className={nieuw.begeleiders.includes(m.id) ? "chip on" : "chip"} onClick={() => toggleBeg(m.id)}>{m.volledige_naam}</button>)}
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ---------------------------------- Rooster ------------------------------- */
|
||
// 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([]);
|
||
const [activiteiten, setActiviteiten] = useState([]);
|
||
const [locaties, setLocaties] = useState([]);
|
||
const [groepen, setGroepen] = useState([]);
|
||
const [subgroepen, setSubgroepen] = useState([]);
|
||
const [leerlingen, setLeerlingen] = useState([]);
|
||
const [medewerkers, setMedewerkers] = useState([]);
|
||
const [blokken, setBlokken] = useState([]);
|
||
const [kalender, setKalender] = useState([]);
|
||
const [afwezig, setAfwezig] = useState([]);
|
||
const [uitzonderingen, setUitzonderingen] = useState([]);
|
||
const [leerpleinen, setLeerpleinen] = useState([]);
|
||
const [maandag, setMaandag] = useState(() => maandagVan(new Date()));
|
||
const [toonForm, setToonForm] = useState(false);
|
||
const [tab, setTab] = useState("week");
|
||
const [error, setError] = useState(null);
|
||
// Scope-filter (ook voor gericht afdrukken): per klas/bovenliggende groep/leerling/personeel.
|
||
const [scopeType, setScopeType] = useState("alles");
|
||
const [scopeId, setScopeId] = useState("");
|
||
// Blok bewerken/kopiëren (alleen in de Roostermaker).
|
||
const [bewerkBlok, setBewerkBlok] = useState(null);
|
||
|
||
const leeg = { tijdslot: "", activiteit: "", locatie: "", doel_type: "groep", doel: "", begeleiders: [], opmerkingen: "" };
|
||
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).
|
||
const dragRef = useRef(null);
|
||
const laadRef = useRef(() => {});
|
||
const [ghost, setGhost] = useState(null);
|
||
const [hoverSlot, setHoverSlot] = useState(null);
|
||
|
||
useEffect(() => {
|
||
listResource("schooljaren").then((j) => { setSchooljaren(j); if (j.length) setSchooljaar((s) => s || String(j[0].id)); }).catch((e) => setError(e.message));
|
||
listResource("tijdsloten").then(setTijdsloten).catch(() => {});
|
||
listResource("activiteiten").then(setActiviteiten).catch(() => {});
|
||
listResource("locaties").then(setLocaties).catch(() => {});
|
||
listResource("groepen").then(setGroepen).catch(() => {});
|
||
listResource("subgroepen").then(setSubgroepen).catch(() => {});
|
||
listResource("personen", "?rol=leerling").then(setLeerlingen).catch(() => {});
|
||
listResource("personen").then((ps) => setMedewerkers(ps.filter((p) => p.rol !== "leerling"))).catch(() => {});
|
||
listResource("leerpleinen").then(setLeerpleinen).catch(() => {});
|
||
// Afdrukken alleen tonen als de print-module aanstaat.
|
||
getModules().then((ms) => setPrintAan(ms.some((m) => m.key === "printing" && m.enabled))).catch(() => {});
|
||
}, []);
|
||
|
||
// Scope-context + gefilterde blokken (voor weergave/afdrukken).
|
||
const scopeCtx = { leerlingen, subgroepen, leerpleinen };
|
||
const scope = { type: scopeType, id: scopeId ? Number(scopeId) : "" };
|
||
const zichtbareBlokken = blokken.filter((b) => blokInScope(b, scope, scopeCtx));
|
||
const scopeOpties = scopeType === "klas" ? groepen
|
||
: scopeType === "leerplein" ? leerpleinen
|
||
: scopeType === "leerling" ? leerlingen
|
||
: scopeType === "personeel" ? medewerkers
|
||
: [];
|
||
|
||
const weekDatums = [0, 1, 2, 3, 4].map((i) => plusDagen(maandag, i));
|
||
const vanISO = fmtISO(maandag);
|
||
const totISO = fmtISO(plusDagen(maandag, 6));
|
||
|
||
async function laadBlokken() {
|
||
if (!schooljaar) { setBlokken([]); return; }
|
||
try {
|
||
setBlokken(await listResource("roosterblokken", `?schooljaar=${schooljaar}`));
|
||
setKalender(await listResource("kalenderdagen", `?schooljaar=${schooljaar}`));
|
||
setError(null);
|
||
} catch (e) { setError(e.message); }
|
||
}
|
||
async function laadWeek() {
|
||
if (!schooljaar) return;
|
||
try {
|
||
setAfwezig(await listResource("afwezigheden", `?van=${vanISO}&tot=${totISO}`));
|
||
setUitzonderingen(await listResource("blok-uitzonderingen", `?schooljaar=${schooljaar}&van=${vanISO}&tot=${totISO}`));
|
||
} catch (e) { setError(e.message); }
|
||
}
|
||
async function herlaadNaBlokWijziging() {
|
||
try { setSubgroepen(await listResource("subgroepen")); } catch { /* blokken verversen blijft leidend */ }
|
||
await laadBlokken();
|
||
}
|
||
laadRef.current = laadBlokken;
|
||
useEffect(() => { laadBlokken(); }, [schooljaar]);
|
||
useEffect(() => { laadWeek(); }, [schooljaar, vanISO]);
|
||
|
||
// Listeners voor drag-and-drop, één keer opgezet.
|
||
useEffect(() => {
|
||
function move(e) {
|
||
if (!dragRef.current) return;
|
||
setGhost({ x: e.clientX, y: e.clientY, label: dragRef.current.activiteit_naam });
|
||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||
const cell = el && el.closest("[data-tijdslot]");
|
||
setHoverSlot(cell ? Number(cell.dataset.tijdslot) : null);
|
||
}
|
||
async function up(e) {
|
||
if (!dragRef.current) return;
|
||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||
const cell = el && el.closest("[data-tijdslot]");
|
||
const doel = cell ? Number(cell.dataset.tijdslot) : null;
|
||
const blok = dragRef.current;
|
||
dragRef.current = null;
|
||
setGhost(null);
|
||
setHoverSlot(null);
|
||
if (doel && doel !== blok.tijdslot) {
|
||
try { await updateResource("roosterblokken", blok.id, { tijdslot: doel }); await laadRef.current(); }
|
||
catch (err) { setError(err.message); }
|
||
}
|
||
}
|
||
window.addEventListener("pointermove", move);
|
||
window.addEventListener("pointerup", up);
|
||
return () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
|
||
}, []);
|
||
|
||
// Sneltoetsen (alleen in de Roostermaker).
|
||
useEffect(() => {
|
||
if (!edit) return;
|
||
function onKey(e) {
|
||
const t = e.target;
|
||
const typing = t && (t.tagName === "INPUT" || t.tagName === "SELECT" || t.tagName === "TEXTAREA" || t.isContentEditable);
|
||
if (e.key === "Escape") { setBewerkBlok(null); sluitNieuwBlok(); return; }
|
||
if (typing) return;
|
||
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 === "ArrowRight") { setMaandag((m) => plusDagen(m, 7)); }
|
||
else if (e.key === "t" || e.key === "T") { setMaandag(maandagVan(new Date())); }
|
||
}
|
||
window.addEventListener("keydown", onKey);
|
||
return () => window.removeEventListener("keydown", onKey);
|
||
}, [edit]);
|
||
|
||
function startDrag(e, blok) {
|
||
e.preventDefault();
|
||
dragRef.current = blok;
|
||
setGhost({ x: e.clientX, y: e.clientY, label: blok.activiteit_naam });
|
||
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 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) {
|
||
setForm((f) => f.begeleiders.includes(id) ? { ...f, begeleiders: f.begeleiders.filter((x) => x !== id) } : { ...f, begeleiders: [...f.begeleiders, id] });
|
||
}
|
||
async function opslaan(e) {
|
||
e.preventDefault();
|
||
const basis = { schooljaar, tijdslot: form.tijdslot, activiteit: form.activiteit, locatie: form.locatie || null, begeleiders: form.begeleiders, opmerkingen: form.opmerkingen };
|
||
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) {
|
||
if (!confirm("Dit roosterblok (alle weken) verwijderen?")) return;
|
||
try { await deleteResource("roosterblokken", id); await laadBlokken(); } catch (err) { setError(err.message); }
|
||
}
|
||
async function laatVervallen(blok, iso) {
|
||
try { await createResource("blok-uitzonderingen", { roosterblok: blok.id, datum: iso, vervalt: true }); await laadWeek(); }
|
||
catch (err) { setError(err.message); }
|
||
}
|
||
async function herstel(uitId) {
|
||
try { await deleteResource("blok-uitzonderingen", uitId); await laadWeek(); } catch (err) { setError(err.message); }
|
||
}
|
||
async function dagVervallen(iso) {
|
||
try { await createResource("kalenderdagen", { schooljaar, datum: iso, type: "vrije_dag", omschrijving: "vervallen" }); await laadBlokken(); }
|
||
catch (err) { setError(err.message); }
|
||
}
|
||
async function dagHerstel(kalId) {
|
||
try { await deleteResource("kalenderdagen", kalId); await laadBlokken(); }
|
||
catch (err) { setError(err.message); }
|
||
}
|
||
|
||
// Hulpfuncties voor het effectieve rooster.
|
||
const kalenderOp = (iso) => kalender.find((k) => k.datum === iso);
|
||
const uitzOp = (blokId, iso) => uitzonderingen.find((u) => u.roosterblok === blokId && u.datum === iso);
|
||
// 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));
|
||
function renderRoosterBlok(b, iso, kal) {
|
||
const uit = uitzOp(b.id, iso);
|
||
const vervallen = !!kal || (uit && uit.vervalt);
|
||
return (
|
||
<div
|
||
className={(vervallen ? "rooster-blok vervallen" : "rooster-blok") + (edit ? " klikbaar" : "") + (isOpenBlok(b) ? " open" : "")}
|
||
key={b.id}
|
||
style={{ borderLeftColor: b.activiteit_kleur }}
|
||
onClick={edit ? () => toggleOpenBlok(b) : undefined}
|
||
title={edit ? "Klik om te bewerken" : undefined}
|
||
>
|
||
<div className="rb-top">
|
||
{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>
|
||
{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>}
|
||
</div>
|
||
<div className="small">Doel: {b.doel_naam} <span className="muted">({b.doel_type})</span></div>
|
||
{b.locatie_naam && <div className="small">Locatie: {b.locatie_naam}</div>}
|
||
{b.begeleiders_namen.length > 0 && (
|
||
<div className="small muted">Begeleiding: {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>
|
||
))}</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})` : ""}{edit && <button type="button" className="icon-btn" title="Herstellen" onClick={(e) => { e.stopPropagation(); herstel(uit.id); }}>↺</button>}</div>}
|
||
{!kal && uit && !uit.vervalt && uit.vervanger_naam && <div className="small rb-verv">Vervanger: {uit.vervanger_naam}</div>}
|
||
{edit && !kal && !uit && (
|
||
<button type="button" className="icon-btn danger rb-verval no-print" title="Dit vak laten vervallen" onClick={(e) => { e.stopPropagation(); laatVervallen(b, iso); }}>⊘</button>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
function renderCompactBlok(b, iso, kal) {
|
||
const uit = uitzOp(b.id, iso);
|
||
const vervallen = !!kal || (uit && uit.vervalt);
|
||
return (
|
||
<div
|
||
className={(vervallen ? "split-vak vervallen" : "split-vak") + (edit ? " klikbaar" : "") + (isOpenBlok(b) ? " open" : "")}
|
||
key={b.id}
|
||
style={{ borderLeftColor: b.activiteit_kleur }}
|
||
onClick={edit ? () => toggleOpenBlok(b) : undefined}
|
||
title={edit ? "Klik om te bewerken" : undefined}
|
||
>
|
||
<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>}
|
||
<strong>{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 danger" onClick={(e) => { e.stopPropagation(); verwijder(b.id); }} title="Blok verwijderen">×</button>}
|
||
</div>
|
||
<div className="split-vak-meta">
|
||
{b.locatie_naam && <span>{b.locatie_naam}</span>}
|
||
{b.begeleiders_namen.length > 0 && <span>{b.begeleiders_namen.map((x) => x.naam).join(", ")}</span>}
|
||
{vervallen && <span className="rb-status">Vervalt</span>}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
function renderSlotBlokken(slotBlokken, iso, kal) {
|
||
const losse = [];
|
||
const perGroep = new Map();
|
||
slotBlokken.forEach((b) => {
|
||
const gid = bovenliggendeGroep(b, subgroepen);
|
||
if (!gid) { losse.push(b); return; }
|
||
if (!perGroep.has(gid)) perGroep.set(gid, []);
|
||
perGroep.get(gid).push(b);
|
||
});
|
||
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(
|
||
<div className="blok-splitsing leerplein-splitsing" key={`leerplein-${scopeId}`}>
|
||
<div className="split-header">
|
||
<span className="split-icon">⤴</span>
|
||
<div className="split-title">
|
||
<strong>{leerplein ? leerplein.naam : BOVENGROEP_LABEL}</strong>
|
||
<span>{BOVENGROEP_LABEL} → groepen</span>
|
||
</div>
|
||
<span className="split-count">{kolommen.length}</span>
|
||
</div>
|
||
<div className="split-kolommen split-vertakking">
|
||
{kolommen.map(([gid, groepBlokken]) => {
|
||
const groep = groepen.find((g) => g.id === gid);
|
||
return (
|
||
<div className="split-kolom" key={gid}>
|
||
<div className="split-kop split-node-kop">{groep ? groep.naam : "Groep"}</div>
|
||
<div className="split-vakken">{groepBlokken.map((b) => renderCompactBlok(b, iso, kal))}</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
return out;
|
||
}
|
||
perGroep.forEach((groepBlokken, gid) => {
|
||
const heeftSplitsing = groepBlokken.some((b) => b.subgroep);
|
||
if (!heeftSplitsing) {
|
||
groepBlokken.forEach((b) => out.push(renderRoosterBlok(b, iso, kal)));
|
||
return;
|
||
}
|
||
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(
|
||
<div className="blok-splitsing" key={`split-${gid}`}>
|
||
<div className="split-header">
|
||
<span className="split-icon">⤴</span>
|
||
<div className="split-title">
|
||
<strong>{groep ? groep.naam : "Groep"}</strong>
|
||
<span>Groep → subgroepen</span>
|
||
</div>
|
||
<span className="split-count">{kolomAantal}</span>
|
||
</div>
|
||
<div className="split-kolommen split-vertakking">
|
||
{heleGroep.length > 0 && (
|
||
<div className="split-kolom">
|
||
<div className="split-kop split-node-kop">Hele groep</div>
|
||
<div className="split-vakken">{heleGroep.map((b) => renderCompactBlok(b, iso, kal))}</div>
|
||
</div>
|
||
)}
|
||
{subIds.map((sid) => {
|
||
const sub = subgroepen.find((s) => s.id === sid);
|
||
const blocks = groepBlokken.filter((b) => b.subgroep === sid);
|
||
return (
|
||
<div className="split-kolom" key={sid}>
|
||
<div className="split-kop split-node-kop">{sub ? sub.naam : "Subgroep"}</div>
|
||
<div className="split-vakken">{blocks.map((b) => renderCompactBlok(b, iso, kal))}</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
});
|
||
return out;
|
||
}
|
||
|
||
return (
|
||
<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 no-print">
|
||
<label className="veld">Schooljaar
|
||
<select value={schooljaar} onChange={(e) => setSchooljaar(e.target.value)}>
|
||
<option value="">— kies —</option>
|
||
{schooljaren.map((j) => <option key={j.id} value={j.id}>{j.naam}</option>)}
|
||
</select>
|
||
</label>
|
||
{schooljaar && (
|
||
<div className="week-nav">
|
||
<button className="btn" onClick={() => setMaandag(plusDagen(maandag, -7))}>‹ vorige</button>
|
||
<button className="btn" onClick={() => setMaandag(maandagVan(new Date()))}>deze week</button>
|
||
<button className="btn" onClick={() => setMaandag(plusDagen(maandag, 7))}>volgende ›</button>
|
||
<span className="week-label">week van {fmtDM(maandag)}</span>
|
||
</div>
|
||
)}
|
||
{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) && (
|
||
<button className="btn print-knop icon-label" title="Rooster afdrukken" onClick={() => window.print()}>🖨 <span>Print</span></button>
|
||
)}
|
||
</div>
|
||
|
||
{schooljaar && (edit ? tab === "week" : true) && (
|
||
<div className="rooster-balk no-print scope-balk">
|
||
<label className="veld">Toon
|
||
<select value={scopeType} onChange={(e) => { setScopeType(e.target.value); setScopeId(""); }}>
|
||
<option value="alles">Alles</option>
|
||
<option value="klas">Per klas</option>
|
||
<option value="leerplein">Per {BOVENGROEP_LABEL_LC}</option>
|
||
<option value="leerling">Per leerling</option>
|
||
<option value="personeel">Per personeel</option>
|
||
</select>
|
||
</label>
|
||
{scopeType !== "alles" && (
|
||
<select value={scopeId} onChange={(e) => setScopeId(e.target.value)}>
|
||
<option value="">— kies —</option>
|
||
{scopeOpties.map((o) => <option key={o.id} value={o.id}>{o.naam || o.volledige_naam}</option>)}
|
||
</select>
|
||
)}
|
||
{scopeType !== "alles" && scopeId && <span className="muted small">{zichtbareBlokken.length} blok(ken)</span>}
|
||
{edit && groepen.length > 0 && (
|
||
<div className="klas-strip">
|
||
<button type="button" className={scopeType === "alles" ? "chip on" : "chip"} onClick={() => { setScopeType("alles"); setScopeId(""); }}>Alle</button>
|
||
{groepen.map((g) => (
|
||
<button
|
||
type="button"
|
||
key={g.id}
|
||
className={scopeType === "klas" && Number(scopeId) === g.id ? "chip on" : "chip"}
|
||
onClick={() => { setScopeType("klas"); setScopeId(String(g.id)); }}
|
||
>
|
||
{g.naam}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{edit && (
|
||
<div className="tabbar no-print">
|
||
<button className={tab === "week" ? "tab on" : "tab"} title="Weekrooster" onClick={() => setTab("week")}>▦ Week</button>
|
||
<button className={tab === "conflicten" ? "tab on" : "tab"} title="Conflicten" onClick={() => setTab("conflicten")}>⚠ Conflicten</button>
|
||
<button className={tab === "schooljaar" ? "tab on" : "tab"} title="Schooljaar" onClick={() => setTab("schooljaar")}>◷ Jaar</button>
|
||
<button className={tab === "instellingen" ? "tab on" : "tab"} title="Instellingen" onClick={() => setTab("instellingen")}>⚙ Instellingen</button>
|
||
</div>
|
||
)}
|
||
{edit && tab === "week" && (
|
||
<p className="muted small no-print sneltoets-hint">Sneltoetsen: <b>N</b> nieuw blok · <b>←</b>/<b>→</b> week · <b>T</b> deze week · <b>Esc</b> sluiten</p>
|
||
)}
|
||
|
||
{edit && schooljaar && tab === "week" && toonForm && (
|
||
<div className="modal-overlay no-print" onClick={sluitNieuwBlok}>
|
||
<form className="modal rooster-modal" role="dialog" aria-modal="true" aria-label="Vak toevoegen" onSubmit={opslaan} onClick={(e) => e.stopPropagation()}>
|
||
<div className="modal-kop">
|
||
<div>
|
||
<h2>Vak toevoegen</h2>
|
||
<div className="muted small">Maak één blok of splits direct over subgroepen.</div>
|
||
</div>
|
||
<button type="button" className="icon-btn" title="Sluiten" onClick={sluitNieuwBlok}>×</button>
|
||
</div>
|
||
<div className="blok-editor modal-editor">
|
||
<div className="blok-editor-kop">
|
||
<strong>Basis</strong>
|
||
<span className="muted small">Tijd, vak en doel</span>
|
||
</div>
|
||
<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" && (
|
||
<div className="rooster-grid">
|
||
{WEEKDAGEN.map((d, i) => {
|
||
const datum = weekDatums[i];
|
||
const iso = fmtISO(datum);
|
||
const kal = kalenderOp(iso);
|
||
const slots = tijdsloten.filter((t) => t.dag === d.value).sort((a, b) => a.volgorde - b.volgorde);
|
||
return (
|
||
<div className={kal ? "rooster-kolom vrij" : "rooster-kolom"} key={d.value}>
|
||
<div className="rooster-dag">
|
||
{d.label} <span className="muted">{fmtDM(datum)}</span>
|
||
{kal && <div className={`tag dag-${kal.type}`}>{kal.type_label}{kal.omschrijving ? `: ${kal.omschrijving}` : ""}</div>}
|
||
{edit && (kal
|
||
? <button type="button" className="icon-btn no-print dag-actie" title="Weer lesdag maken" onClick={() => dagHerstel(kal.id)}>↺</button>
|
||
: <button type="button" className="icon-btn danger no-print dag-actie" title="Dag laten vervallen" onClick={() => dagVervallen(iso)}>⊘</button>
|
||
)}
|
||
</div>
|
||
{slots.length === 0 && <div className="muted small">—</div>}
|
||
{slots.map((t) => {
|
||
const slotBlokken = zichtbareBlokken.filter((b) => b.tijdslot === t.id);
|
||
return (
|
||
<div
|
||
className={hoverSlot === t.id ? "slot-cel hover" : "slot-cel"}
|
||
data-tijdslot={t.id}
|
||
key={t.id}
|
||
>
|
||
<div className="slot-tijd">{t.start_tijd?.slice(0, 5)} {t.naam}</div>
|
||
{renderSlotBlokken(slotBlokken, iso, kal)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{schooljaar && tab === "conflicten" && <Conflicten schooljaar={schooljaar} />}
|
||
|
||
{tab === "schooljaar" && (
|
||
<div className="instellingen">
|
||
<p className="muted">Maak hier snel een schooljaar aan en beheer de kalender (studiedagen, vrije dagen, vakanties).</p>
|
||
<SchooljaarPage />
|
||
</div>
|
||
)}
|
||
|
||
{schooljaar && tab === "instellingen" && (
|
||
<div className="instellingen">
|
||
<p className="muted">Bouwstenen van het rooster — beheer ze hier zonder het Rooster te verlaten.</p>
|
||
<TijdslotenPage />
|
||
<ActiviteitenPage />
|
||
<LocatiesPage />
|
||
</div>
|
||
)}
|
||
|
||
{edit && bewerkBlok && (
|
||
<BlokBewerker
|
||
blok={bewerkBlok}
|
||
ctx={editorCtx}
|
||
onSluit={() => setBewerkBlok(null)}
|
||
onKlaar={async () => { setBewerkBlok(null); await herlaadNaBlokWijziging(); }}
|
||
/>
|
||
)}
|
||
{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" />; }
|