Groepsindeling: toon-filter + multi-select; handmatig vak-icoon (migratie core 0007)
This commit is contained in:
parent
77a5eacfed
commit
e4a75c9794
6 changed files with 143 additions and 27 deletions
|
|
@ -40,8 +40,8 @@ volgt.
|
||||||
|
|
||||||
- [x] Per vak **automatisch een passend icoon** (afgeleid van de naam) — getoond
|
- [x] Per vak **automatisch een passend icoon** (afgeleid van de naam) — getoond
|
||||||
in het rooster en bij Activiteiten/vakken.
|
in het rooster en bij Activiteiten/vakken.
|
||||||
- [ ] Optioneel: een icoon handmatig kunnen kiezen/overschrijven (vereist een
|
- [x] Optioneel: een icoon handmatig kunnen kiezen/overschrijven (`icoon`-veld
|
||||||
`icoon`-veld op Activiteit + migratie).
|
op Activiteit + migratie core 0007; leeg = automatisch op naam).
|
||||||
|
|
||||||
## Stage-module — uitbreidingen
|
## Stage-module — uitbreidingen
|
||||||
|
|
||||||
|
|
@ -68,6 +68,9 @@ volgt.
|
||||||
een zijbalk "nog niet ingedeeld". Personen naar elk niveau slepen; × haalt
|
een zijbalk "nog niet ingedeeld". Personen naar elk niveau slepen; × haalt
|
||||||
ze weer weg. Leerlingen licht gekleurd, personeel donker; duidelijke
|
ze weer weg. Leerlingen licht gekleurd, personeel donker; duidelijke
|
||||||
gekleurde structuur per niveau. Inline aanmaken/hernoemen.
|
gekleurde structuur per niveau. Inline aanmaken/hernoemen.
|
||||||
|
- [x] **Toon-filter** (Allen / Leerlingen / Personeel) en **multi-select**: meerdere
|
||||||
|
personen aanvinken en in één keer in een gekozen groep plaatsen (of de
|
||||||
|
selectie samen slepen).
|
||||||
|
|
||||||
## Koppelingen & inzetregels
|
## Koppelingen & inzetregels
|
||||||
|
|
||||||
|
|
|
||||||
18
backend/core/migrations/0007_activiteit_icoon.py
Normal file
18
backend/core/migrations/0007_activiteit_icoon.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.2.15 on 2026-06-16 20:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0006_lokaal_lokaalinstelling_roosterblok_lokaal'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='activiteit',
|
||||||
|
name='icoon',
|
||||||
|
field=models.CharField(blank=True, help_text='Optioneel emoji; leeg = automatisch gekozen op naam.', max_length=8, verbose_name='icoon'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -285,6 +285,7 @@ class Activiteit(models.Model):
|
||||||
|
|
||||||
naam = models.CharField("naam", max_length=80, unique=True)
|
naam = models.CharField("naam", max_length=80, unique=True)
|
||||||
kleur = models.CharField("kleur", max_length=7, default="#2563eb", help_text="hex, bv. #2563eb")
|
kleur = models.CharField("kleur", max_length=7, default="#2563eb", help_text="hex, bv. #2563eb")
|
||||||
|
icoon = models.CharField("icoon", max_length=8, blank=True, help_text="Optioneel emoji; leeg = automatisch gekozen op naam.")
|
||||||
omschrijving = models.CharField("omschrijving", max_length=255, blank=True)
|
omschrijving = models.CharField("omschrijving", max_length=255, blank=True)
|
||||||
theorievak = models.BooleanField(
|
theorievak = models.BooleanField(
|
||||||
"theorievak", default=False,
|
"theorievak", default=False,
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ class ActiviteitSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Activiteit
|
model = Activiteit
|
||||||
fields = ["id", "naam", "kleur", "omschrijving", "theorievak", "actief"]
|
fields = ["id", "naam", "kleur", "icoon", "omschrijving", "theorievak", "actief"]
|
||||||
|
|
||||||
|
|
||||||
class LocatieSerializer(serializers.ModelSerializer):
|
class LocatieSerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -207,6 +207,7 @@ class LokaalinstellingSerializer(serializers.ModelSerializer):
|
||||||
class RoosterblokSerializer(serializers.ModelSerializer):
|
class RoosterblokSerializer(serializers.ModelSerializer):
|
||||||
activiteit_naam = serializers.CharField(source="activiteit.naam", read_only=True)
|
activiteit_naam = serializers.CharField(source="activiteit.naam", read_only=True)
|
||||||
activiteit_kleur = serializers.CharField(source="activiteit.kleur", read_only=True)
|
activiteit_kleur = serializers.CharField(source="activiteit.kleur", read_only=True)
|
||||||
|
activiteit_icoon = serializers.CharField(source="activiteit.icoon", read_only=True, default="")
|
||||||
tijdslot_label = serializers.CharField(source="tijdslot.__str__", read_only=True)
|
tijdslot_label = serializers.CharField(source="tijdslot.__str__", read_only=True)
|
||||||
locatie_naam = serializers.CharField(source="locatie.naam", read_only=True, default=None)
|
locatie_naam = serializers.CharField(source="locatie.naam", read_only=True, default=None)
|
||||||
lokaal_naam = serializers.CharField(source="lokaal.naam", read_only=True, default=None)
|
lokaal_naam = serializers.CharField(source="lokaal.naam", read_only=True, default=None)
|
||||||
|
|
@ -218,7 +219,7 @@ class RoosterblokSerializer(serializers.ModelSerializer):
|
||||||
model = Roosterblok
|
model = Roosterblok
|
||||||
fields = [
|
fields = [
|
||||||
"id", "schooljaar", "tijdslot", "tijdslot_label",
|
"id", "schooljaar", "tijdslot", "tijdslot_label",
|
||||||
"activiteit", "activiteit_naam", "activiteit_kleur",
|
"activiteit", "activiteit_naam", "activiteit_kleur", "activiteit_icoon",
|
||||||
"locatie", "locatie_naam", "lokaal", "lokaal_naam",
|
"locatie", "locatie_naam", "lokaal", "lokaal_naam",
|
||||||
"groep", "subgroep", "leerling", "doel_type", "doel_naam",
|
"groep", "subgroep", "leerling", "doel_type", "doel_naam",
|
||||||
"begeleiders", "begeleiders_namen", "opmerkingen", "aangemaakt_op",
|
"begeleiders", "begeleiders_namen", "opmerkingen", "aangemaakt_op",
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTh
|
||||||
naam: "", omschrijving: "",
|
naam: "", omschrijving: "",
|
||||||
...(kleur ? { kleur: "#2563eb" } : {}),
|
...(kleur ? { kleur: "#2563eb" } : {}),
|
||||||
...(metTheorievak ? { theorievak: false } : {}),
|
...(metTheorievak ? { theorievak: false } : {}),
|
||||||
|
...(metIcoon ? { icoon: "" } : {}),
|
||||||
};
|
};
|
||||||
const [form, setForm] = useState(leeg);
|
const [form, setForm] = useState(leeg);
|
||||||
const [editId, setEditId] = useState(null);
|
const [editId, setEditId] = useState(null);
|
||||||
|
|
@ -126,6 +127,7 @@ function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTh
|
||||||
naam: it.naam, omschrijving: it.omschrijving,
|
naam: it.naam, omschrijving: it.omschrijving,
|
||||||
...(kleur ? { kleur: it.kleur } : {}),
|
...(kleur ? { kleur: it.kleur } : {}),
|
||||||
...(metTheorievak ? { theorievak: it.theorievak } : {}),
|
...(metTheorievak ? { theorievak: it.theorievak } : {}),
|
||||||
|
...(metIcoon ? { icoon: it.icoon || "" } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,6 +142,9 @@ function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTh
|
||||||
{kleur && (
|
{kleur && (
|
||||||
<input type="color" value={form.kleur} title="Kleur" onChange={(e) => setForm({ ...form, kleur: e.target.value })} />
|
<input type="color" value={form.kleur} title="Kleur" onChange={(e) => setForm({ ...form, kleur: e.target.value })} />
|
||||||
)}
|
)}
|
||||||
|
{metIcoon && (
|
||||||
|
<input className="icoon-input" maxLength={4} placeholder="icoon" title="Optioneel emoji; leeg = automatisch" value={form.icoon || ""} onChange={(e) => setForm({ ...form, icoon: e.target.value })} />
|
||||||
|
)}
|
||||||
{metTheorievak && (
|
{metTheorievak && (
|
||||||
<label className="veld">
|
<label className="veld">
|
||||||
<input type="checkbox" checked={!!form.theorievak} onChange={(e) => setForm({ ...form, theorievak: e.target.checked })} />
|
<input type="checkbox" checked={!!form.theorievak} onChange={(e) => setForm({ ...form, theorievak: e.target.checked })} />
|
||||||
|
|
@ -169,7 +174,7 @@ function CatalogusPage({ titel, uitleg, resource, kleur = false, telKolom, metTh
|
||||||
<tr key={it.id}>
|
<tr key={it.id}>
|
||||||
<td>
|
<td>
|
||||||
{kleur && <span className="kleur-stip" style={{ background: it.kleur }} />}
|
{kleur && <span className="kleur-stip" style={{ background: it.kleur }} />}
|
||||||
{metIcoon && <span className="vak-icoon">{vakIcoon(it.naam)} </span>}
|
{metIcoon && <span className="vak-icoon">{it.icoon || vakIcoon(it.naam)} </span>}
|
||||||
{it.naam}
|
{it.naam}
|
||||||
</td>
|
</td>
|
||||||
<td className="muted">{it.omschrijving || "—"}</td>
|
<td className="muted">{it.omschrijving || "—"}</td>
|
||||||
|
|
@ -828,6 +833,10 @@ export function GroepsindelingPage() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const [toon, setToon] = useState("allen"); // allen | leerling | personeel
|
||||||
|
const [selectie, setSelectie] = useState(new Set()); // persoon-ids
|
||||||
|
const [plaatsDoel, setPlaatsDoel] = useState(""); // "kind:id"
|
||||||
|
|
||||||
const dragRef = useRef(null);
|
const dragRef = useRef(null);
|
||||||
const [ghost, setGhost] = useState(null);
|
const [ghost, setGhost] = useState(null);
|
||||||
const [hoverZone, setHoverZone] = useState(null);
|
const [hoverZone, setHoverZone] = useState(null);
|
||||||
|
|
@ -854,13 +863,14 @@ export function GroepsindelingPage() {
|
||||||
|
|
||||||
const leerlingById = (id) => leerlingen.find((l) => l.id === id);
|
const leerlingById = (id) => leerlingen.find((l) => l.id === id);
|
||||||
const medewerkerById = (id) => medewerkers.find((m) => m.id === id);
|
const medewerkerById = (id) => medewerkers.find((m) => m.id === id);
|
||||||
|
const isLeerlingId = (id) => leerlingen.some((l) => l.id === id);
|
||||||
|
const toonRol = (rol) => toon === "allen" || (toon === "leerling" ? rol === "leerling" : rol !== "leerling");
|
||||||
const groepLeerlingen = (gid) => leerlingen.filter((l) => l.groep === gid);
|
const groepLeerlingen = (gid) => leerlingen.filter((l) => l.groep === gid);
|
||||||
const groepMedewerkers = (gid) => medewerkers.filter((m) => m.stamgroep === gid);
|
const groepMedewerkers = (gid) => medewerkers.filter((m) => m.stamgroep === gid);
|
||||||
const groepSubs = (gid) => subgroepen.filter((s) => s.groep === gid);
|
const groepSubs = (gid) => subgroepen.filter((s) => s.groep === gid);
|
||||||
const lpGroepen = (lp) => groepen.filter((g) => (lp.groepen || []).includes(g.id));
|
const lpGroepen = (lp) => groepen.filter((g) => (lp.groepen || []).includes(g.id));
|
||||||
const groepHeeftLeerplein = (gid) => leerpleinen.some((lp) => (lp.groepen || []).includes(gid));
|
const groepHeeftLeerplein = (gid) => leerpleinen.some((lp) => (lp.groepen || []).includes(gid));
|
||||||
|
|
||||||
// "Nog niet ingedeeld": personen die nergens voorkomen.
|
|
||||||
const ingedeeldeLeerlingen = (() => {
|
const ingedeeldeLeerlingen = (() => {
|
||||||
const s = new Set();
|
const s = new Set();
|
||||||
leerlingen.forEach((l) => { if (l.groep) s.add(l.id); });
|
leerlingen.forEach((l) => { if (l.groep) s.add(l.id); });
|
||||||
|
|
@ -878,15 +888,29 @@ export function GroepsindelingPage() {
|
||||||
const losseLeerlingen = leerlingen.filter((l) => !ingedeeldeLeerlingen.has(l.id));
|
const losseLeerlingen = leerlingen.filter((l) => !ingedeeldeLeerlingen.has(l.id));
|
||||||
const losseMedewerkers = medewerkers.filter((m) => !ingedeeldeMedewerkers.has(m.id));
|
const losseMedewerkers = medewerkers.filter((m) => !ingedeeldeMedewerkers.has(m.id));
|
||||||
|
|
||||||
|
const toggleSel = (id) => setSelectie((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
|
||||||
|
const wisSelectie = () => setSelectie(new Set());
|
||||||
|
|
||||||
|
const containerOpties = [];
|
||||||
|
leerpleinen.forEach((lp) => {
|
||||||
|
containerOpties.push({ value: `leerplein:${lp.id}`, label: `${lp.naam} (bovenliggende groep)` });
|
||||||
|
lpGroepen(lp).forEach((g) => {
|
||||||
|
containerOpties.push({ value: `groep:${g.id}`, label: `${lp.naam} › ${g.naam} (groep)` });
|
||||||
|
groepSubs(g.id).forEach((s) => containerOpties.push({ value: `subgroep:${s.id}`, label: `${lp.naam} › ${g.naam} › ${s.naam} (subgroep)` }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function startDrag(e, payload) {
|
function startDrag(e, payload) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dragRef.current = payload;
|
dragRef.current = payload;
|
||||||
setGhost({ x: e.clientX, y: e.clientY, label: payload.naam });
|
const meerdere = selectie.has(payload.id) && selectie.size > 1;
|
||||||
|
setGhost({ x: e.clientX, y: e.clientY, label: meerdere ? `${selectie.size} personen` : payload.naam });
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function move(e) {
|
function move(e) {
|
||||||
if (!dragRef.current) return;
|
if (!dragRef.current) return;
|
||||||
setGhost({ x: e.clientX, y: e.clientY, label: dragRef.current.naam });
|
const meerdere = selectie.has(dragRef.current.id) && selectie.size > 1;
|
||||||
|
setGhost({ x: e.clientX, y: e.clientY, label: meerdere ? `${selectie.size} personen` : dragRef.current.naam });
|
||||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||||
const z = el && el.closest("[data-drop-kind]");
|
const z = el && el.closest("[data-drop-kind]");
|
||||||
setHoverZone(z ? `${z.dataset.dropKind}:${z.dataset.dropId}` : null);
|
setHoverZone(z ? `${z.dataset.dropKind}:${z.dataset.dropId}` : null);
|
||||||
|
|
@ -899,12 +923,16 @@ export function GroepsindelingPage() {
|
||||||
dragRef.current = null;
|
dragRef.current = null;
|
||||||
setGhost(null);
|
setGhost(null);
|
||||||
setHoverZone(null);
|
setHoverZone(null);
|
||||||
if (z) await drop({ kind: z.dataset.dropKind, id: Number(z.dataset.dropId) }, payload);
|
if (z) {
|
||||||
|
const zone = { kind: z.dataset.dropKind, id: Number(z.dataset.dropId) };
|
||||||
|
if (selectie.has(payload.id) && selectie.size > 1) await plaatsSelectie(zone);
|
||||||
|
else await drop(zone, payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
window.addEventListener("pointermove", move);
|
window.addEventListener("pointermove", move);
|
||||||
window.addEventListener("pointerup", up);
|
window.addEventListener("pointerup", up);
|
||||||
return () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
|
return () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
|
||||||
}, [leerpleinen, groepen, subgroepen, leerlingen, medewerkers]);
|
}, [leerpleinen, groepen, subgroepen, leerlingen, medewerkers, selectie]);
|
||||||
|
|
||||||
async function drop(zone, p) {
|
async function drop(zone, p) {
|
||||||
const { kind, id } = zone;
|
const { kind, id } = zone;
|
||||||
|
|
@ -931,6 +959,31 @@ export function GroepsindelingPage() {
|
||||||
await laadAlles();
|
await laadAlles();
|
||||||
} catch (e) { setError(e.message); }
|
} catch (e) { setError(e.message); }
|
||||||
}
|
}
|
||||||
|
async function plaatsSelectie(zone) {
|
||||||
|
const ids = [...selectie];
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
const { kind, id } = zone;
|
||||||
|
const ll = ids.filter(isLeerlingId);
|
||||||
|
const mw = ids.filter((x) => !isLeerlingId(x));
|
||||||
|
try {
|
||||||
|
if (kind === "groep") {
|
||||||
|
for (const pid of ids) await updateResource("personen", pid, isLeerlingId(pid) ? { groep: id } : { stamgroep: id });
|
||||||
|
} else if (kind === "leerplein") {
|
||||||
|
const lp = leerpleinen.find((x) => x.id === id); if (!lp) return;
|
||||||
|
if (ll.length) await updateResource("leerpleinen", id, { leerlingen: [...new Set([...(lp.leerlingen || []), ...ll])] });
|
||||||
|
if (mw.length) await updateResource("leerpleinen", id, { medewerkers: [...new Set([...(lp.medewerkers || []), ...mw])] });
|
||||||
|
} else if (kind === "subgroep") {
|
||||||
|
const s = subgroepen.find((x) => x.id === id); if (!s) return;
|
||||||
|
if (ll.length) await updateResource("subgroepen", id, { leden: [...new Set([...s.leden_namen.map((x) => x.id), ...ll])] });
|
||||||
|
if (mw.length) await updateResource("subgroepen", id, { begeleiders: [...new Set([...s.begeleiders_namen.map((x) => x.id), ...mw])] });
|
||||||
|
}
|
||||||
|
wisSelectie();
|
||||||
|
setPlaatsDoel("");
|
||||||
|
await laadAlles();
|
||||||
|
} catch (e) { setError(e.message); }
|
||||||
|
}
|
||||||
|
const parseZone = (str) => { const [kind, id] = str.split(":"); return { kind, id: Number(id) }; };
|
||||||
|
|
||||||
async function haalWeg(container, p) {
|
async function haalWeg(container, p) {
|
||||||
const { kind, id } = container;
|
const { kind, id } = container;
|
||||||
try {
|
try {
|
||||||
|
|
@ -963,14 +1016,19 @@ export function GroepsindelingPage() {
|
||||||
className: "dropzone" + (hoverZone === `${kind}:${id}` ? " over" : ""),
|
className: "dropzone" + (hoverZone === `${kind}:${id}` ? " over" : ""),
|
||||||
});
|
});
|
||||||
const chip = (p, rol, container) => {
|
const chip = (p, rol, container) => {
|
||||||
if (!p) return null;
|
if (!p || !toonRol(rol)) return null;
|
||||||
|
const sel = selectie.has(p.id);
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
key={(container ? container.kind + container.id : "pool") + "-" + p.id}
|
key={(container ? container.kind + container.id : "pool") + "-" + p.id}
|
||||||
className={`chip-persoon ${rol === "leerling" ? "ll" : "mw"}`}
|
className={`chip-persoon ${rol === "leerling" ? "ll" : "mw"}${sel ? " geselecteerd" : ""}`}
|
||||||
onPointerDown={(e) => startDrag(e, { id: p.id, rol, naam: p.volledige_naam })}
|
onPointerDown={(e) => startDrag(e, { id: p.id, rol, naam: p.volledige_naam })}
|
||||||
title={p.volledige_naam}
|
title={p.volledige_naam}
|
||||||
>
|
>
|
||||||
|
<button type="button" className="chip-sel" title="(de)selecteer"
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
onClick={(e) => { e.stopPropagation(); toggleSel(p.id); }}
|
||||||
|
>{sel ? "☑" : "☐"}</button>
|
||||||
<span className="chip-naam">{p.volledige_naam}</span>
|
<span className="chip-naam">{p.volledige_naam}</span>
|
||||||
{rol !== "leerling" && SROL[p.rol] && <span className="chip-rol">{SROL[p.rol]}</span>}
|
{rol !== "leerling" && SROL[p.rol] && <span className="chip-rol">{SROL[p.rol]}</span>}
|
||||||
{container && (
|
{container && (
|
||||||
|
|
@ -1045,12 +1103,32 @@ export function GroepsindelingPage() {
|
||||||
return (
|
return (
|
||||||
<section className="indeling">
|
<section className="indeling">
|
||||||
<h1>Groepsindeling</h1>
|
<h1>Groepsindeling</h1>
|
||||||
<p className="muted">Sleep personen naar een bovenliggende groep, groep of subgroep. <span className="legenda ll">leerling</span> en <span className="legenda mw">personeel</span> hebben elk een eigen kleur. Klik × om iemand uit een groep te halen.</p>
|
<p className="muted">Sleep personen naar een bovenliggende groep, groep of subgroep. <span className="legenda ll">leerling</span> en <span className="legenda mw">personeel</span> hebben elk een eigen kleur. Vink meerdere personen aan om ze in één keer te plaatsen; × haalt iemand weer weg.</p>
|
||||||
<Foutmelding msg={error} />
|
<Foutmelding msg={error} />
|
||||||
|
<div className="indeling-toolbar no-print">
|
||||||
|
<span className="muted small">Tonen:</span>
|
||||||
|
<div className="segmented">
|
||||||
|
{[["allen", "Allen"], ["leerling", "Leerlingen"], ["personeel", "Personeel"]].map(([v, l]) => (
|
||||||
|
<button key={v} type="button" className={toon === v ? "seg on" : "seg"} onClick={() => setToon(v)}>{l}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selectie.size > 0 && (
|
||||||
|
<div className="selectie-bar no-print">
|
||||||
|
<strong>{selectie.size}</strong> geselecteerd
|
||||||
|
<select value={plaatsDoel} onChange={(e) => setPlaatsDoel(e.target.value)}>
|
||||||
|
<option value="">— plaats in… —</option>
|
||||||
|
{containerOpties.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||||
|
</select>
|
||||||
|
<button className="btn primary klein" disabled={!plaatsDoel} onClick={() => plaatsDoel && plaatsSelectie(parseZone(plaatsDoel))}>Plaats</button>
|
||||||
|
<button className="btn klein" onClick={wisSelectie}>Wis selectie</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{loading ? <p>Laden…</p> : (
|
{loading ? <p>Laden…</p> : (
|
||||||
<div className="indeling-layout">
|
<div className="indeling-layout">
|
||||||
<aside className="indeling-pool">
|
<aside className="indeling-pool">
|
||||||
<h3>Nog niet ingedeeld</h3>
|
<h3>Nog niet ingedeeld</h3>
|
||||||
|
{toonRol("leerling") && (
|
||||||
<div className="pool-sectie">
|
<div className="pool-sectie">
|
||||||
<div className="pool-kop">Leerlingen</div>
|
<div className="pool-kop">Leerlingen</div>
|
||||||
<div className="chip-rij">
|
<div className="chip-rij">
|
||||||
|
|
@ -1058,6 +1136,8 @@ export function GroepsindelingPage() {
|
||||||
{losseLeerlingen.length === 0 && <span className="muted small">— allemaal ingedeeld —</span>}
|
{losseLeerlingen.length === 0 && <span className="muted small">— allemaal ingedeeld —</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{toonRol("medewerker") && (
|
||||||
<div className="pool-sectie">
|
<div className="pool-sectie">
|
||||||
<div className="pool-kop">Personeel</div>
|
<div className="pool-kop">Personeel</div>
|
||||||
<div className="chip-rij">
|
<div className="chip-rij">
|
||||||
|
|
@ -1065,6 +1145,7 @@ export function GroepsindelingPage() {
|
||||||
{losseMedewerkers.length === 0 && <span className="muted small">— allemaal ingedeeld —</span>}
|
{losseMedewerkers.length === 0 && <span className="muted small">— allemaal ingedeeld —</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
<div className="indeling-board">
|
<div className="indeling-board">
|
||||||
<InlineAdd placeholder="Nieuwe bovenliggende groep…" onAdd={nieuweBovengroep} />
|
<InlineAdd placeholder="Nieuwe bovenliggende groep…" onAdd={nieuweBovengroep} />
|
||||||
|
|
@ -2032,7 +2113,7 @@ export function RoosterScherm({ mode }) {
|
||||||
>
|
>
|
||||||
<div className="rb-top">
|
<div className="rb-top">
|
||||||
{edit && <span className="rb-greep" onPointerDown={(e) => startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot">⠿</span>}
|
{edit && <span className="rb-greep" onPointerDown={(e) => startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot">⠿</span>}
|
||||||
<strong>{vakIcoon(b.activiteit_naam)} {b.activiteit_naam}</strong>
|
<strong>{b.activiteit_icoon || vakIcoon(b.activiteit_naam)} {b.activiteit_naam}</strong>
|
||||||
{edit && <button type="button" className="icon-btn" onClick={(e) => { e.stopPropagation(); toggleOpenBlok(b); }} title="Bewerken">✎</button>}
|
{edit && <button type="button" className="icon-btn" onClick={(e) => { e.stopPropagation(); toggleOpenBlok(b); }} title="Bewerken">✎</button>}
|
||||||
{edit && <button type="button" className="icon-btn danger rb-x" onClick={(e) => { e.stopPropagation(); verwijder(b.id); }} title="Blok verwijderen">×</button>}
|
{edit && <button type="button" className="icon-btn danger rb-x" onClick={(e) => { e.stopPropagation(); verwijder(b.id); }} title="Blok verwijderen">×</button>}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2067,7 +2148,7 @@ export function RoosterScherm({ mode }) {
|
||||||
>
|
>
|
||||||
<div className="split-vak-kop">
|
<div className="split-vak-kop">
|
||||||
{edit && <span className="rb-greep" onPointerDown={(e) => startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot">⠿</span>}
|
{edit && <span className="rb-greep" onPointerDown={(e) => startDrag(e, b)} onClick={(e) => e.stopPropagation()} title="Sleep naar ander tijdslot">⠿</span>}
|
||||||
<strong>{vakIcoon(b.activiteit_naam)} {b.activiteit_naam}</strong>
|
<strong>{b.activiteit_icoon || vakIcoon(b.activiteit_naam)} {b.activiteit_naam}</strong>
|
||||||
{edit && <button type="button" className="icon-btn" onClick={(e) => { e.stopPropagation(); toggleOpenBlok(b); }} title="Bewerken">✎</button>}
|
{edit && <button type="button" className="icon-btn" onClick={(e) => { e.stopPropagation(); toggleOpenBlok(b); }} title="Bewerken">✎</button>}
|
||||||
{edit && <button type="button" className="icon-btn danger" onClick={(e) => { e.stopPropagation(); verwijder(b.id); }} title="Blok verwijderen">×</button>}
|
{edit && <button type="button" className="icon-btn danger" onClick={(e) => { e.stopPropagation(); verwijder(b.id); }} title="Blok verwijderen">×</button>}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -408,3 +408,15 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
|
||||||
.indeling-layout { grid-template-columns: 1fr; }
|
.indeling-layout { grid-template-columns: 1fr; }
|
||||||
.indeling-pool { position: static; }
|
.indeling-pool { position: static; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Groepsindeling: filter + multi-select */
|
||||||
|
.indeling-toolbar { display: flex; align-items: center; gap: 10px; margin: 4px 0 12px; }
|
||||||
|
.segmented { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
||||||
|
.seg { border: 0; background: var(--panel); padding: 6px 12px; cursor: pointer; font: inherit; color: var(--ink); border-right: 1px solid var(--line); }
|
||||||
|
.seg:last-child { border-right: 0; }
|
||||||
|
.seg.on { background: var(--brand); color: #fff; }
|
||||||
|
.selectie-bar { position: sticky; top: 8px; z-index: 20; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; background: var(--brand-soft); border: 1px solid var(--brand); border-radius: 10px; padding: 8px 12px; margin-bottom: 12px; }
|
||||||
|
.selectie-bar select { min-width: 220px; }
|
||||||
|
.chip-sel { border: 0; background: transparent; cursor: pointer; font-size: 13px; padding: 0; line-height: 1; color: inherit; }
|
||||||
|
.chip-persoon.geselecteerd { outline: 2px solid var(--brand); outline-offset: 1px; }
|
||||||
|
.icoon-input { width: 60px; text-align: center; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue