Files
rikx-beheer/app/projecten/[id]/MultiSelectDropdown.tsx
remco-vermeer 9f0bd87c40 Voeg Module 1 Projecten toe met project plan, Rikx calculatie en Rikx rondes
- Projectenpagina met pipeline (standaard) en tabelweergave, filters op
  status, Rikx Ronde en zoeken
- Projectpagina in stappen: Project plan (velden uit de Rikx-aanvraag) en
  Rikx calculatie achter een schuif, live berekend volgens de rekenmethode
- Rikx rondes beheren (label, Rikx waarde, Rikx prijs) en koppelen aan projecten
- Oranje Rikx-menubalk met menu per module, gedeeld door alle pagina's
- Oude offerte-tabellen opgeruimd: leeg wordt verwijderd, met data hernoemd
  naar legacy_*
- CLAUDE.md bijgewerkt naar de huidige stand

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:26:30 +02:00

93 lines
3.0 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
// Dropdown waarin meerdere opties aangevinkt kunnen worden. De keuzes gaan als
// checkboxes met dezelfde `name` mee in het formulier (FormData.getAll).
export function MultiSelectDropdown({
id,
name,
options,
defaultValue,
onChange,
className,
}: {
id: string;
name: string;
options: string[];
defaultValue: string[];
onChange?: (values: string[]) => void;
className: string;
}) {
const [open, setOpen] = useState(false);
const [selected, setSelected] = useState<string[]>(defaultValue);
const rootRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const close = (e: MouseEvent | KeyboardEvent) => {
if (e instanceof KeyboardEvent ? e.key === "Escape" : !rootRef.current?.contains(e.target as Node)) {
setOpen(false);
}
};
document.addEventListener("mousedown", close);
document.addEventListener("keydown", close);
return () => {
document.removeEventListener("mousedown", close);
document.removeEventListener("keydown", close);
};
}, [open]);
function toggle(option: string, checked: boolean) {
const next = checked ? options.filter((o) => o === option || selected.includes(o)) : selected.filter((o) => o !== option);
setSelected(next);
onChange?.(next);
}
const summary =
selected.length === 0 ? "Maak een keuze" : selected.length === 1 ? selected[0] : `${selected.length} geselecteerd`;
return (
<div ref={rootRef} className="relative">
<button
id={id}
type="button"
aria-haspopup="listbox"
aria-expanded={open}
onClick={() => setOpen((o) => !o)}
className={`${className} flex items-center justify-between gap-2 text-left`}
>
<span className={`truncate ${selected.length ? "" : "text-zinc-400"}`}>{summary}</span>
<svg viewBox="0 0 10 6" aria-hidden className="w-2.5 h-2.5 shrink-0 fill-zinc-500">
<path d="M0 0h10L5 6z" />
</svg>
</button>
{/* Altijd gerenderd zodat de checkboxes meegaan in het formulier, ook als de lijst dicht is. */}
<div
hidden={!open}
className="absolute left-0 right-0 top-full z-30 mt-1 max-h-72 overflow-y-auto rounded-lg bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-black/10 py-1"
>
{options.map((option) => (
<label
key={option}
className="flex items-start gap-2 px-3 py-2 text-sm text-zinc-800 dark:text-zinc-200 hover:bg-orange-50 dark:hover:bg-zinc-800 cursor-pointer"
>
<input
type="checkbox"
name={name}
value={option}
checked={selected.includes(option)}
onChange={(e) => toggle(option, e.target.checked)}
className="mt-0.5 accent-rikx-orange"
/>
{option}
</label>
))}
</div>
{selected.length > 1 && (
<p className="mt-1 text-xs text-zinc-500">{selected.join(" · ")}</p>
)}
</div>
);
}