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>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user