Groepsindeling: toon-filter + multi-select; handmatig vak-icoon (migratie core 0007)

This commit is contained in:
bes-r 2026-06-16 22:50:40 +02:00
parent 77a5eacfed
commit e4a75c9794
6 changed files with 143 additions and 27 deletions

View file

@ -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

View 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'),
),
]

View file

@ -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,

View file

@ -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",

View file

@ -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,26 +1103,49 @@ 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>
<div className="pool-sectie"> {toonRol("leerling") && (
<div className="pool-kop">Leerlingen</div> <div className="pool-sectie">
<div className="chip-rij"> <div className="pool-kop">Leerlingen</div>
{losseLeerlingen.map((l) => chip(l, "leerling", null))} <div className="chip-rij">
{losseLeerlingen.length === 0 && <span className="muted small"> allemaal ingedeeld </span>} {losseLeerlingen.map((l) => chip(l, "leerling", null))}
{losseLeerlingen.length === 0 && <span className="muted small"> allemaal ingedeeld </span>}
</div>
</div> </div>
</div> )}
<div className="pool-sectie"> {toonRol("medewerker") && (
<div className="pool-kop">Personeel</div> <div className="pool-sectie">
<div className="chip-rij"> <div className="pool-kop">Personeel</div>
{losseMedewerkers.map((m) => chip(m, "medewerker", null))} <div className="chip-rij">
{losseMedewerkers.length === 0 && <span className="muted small"> allemaal ingedeeld </span>} {losseMedewerkers.map((m) => chip(m, "medewerker", null))}
{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>

View file

@ -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; }