+
+ {/* Altijd gerenderd zodat de checkboxes meegaan in het formulier, ook als de lijst dicht is. */}
+
+ {options.map((option) => (
+
+ ))}
+
+ {selected.length > 1 && (
+
{selected.join(" · ")}
+ )}
+
+ );
+}
diff --git a/app/projecten/[id]/ProjectDetailForm.tsx b/app/projecten/[id]/ProjectDetailForm.tsx
new file mode 100644
index 0000000..9a45023
--- /dev/null
+++ b/app/projecten/[id]/ProjectDetailForm.tsx
@@ -0,0 +1,313 @@
+"use client";
+
+import Link from "next/link";
+import { useRef, useState, useTransition } from "react";
+import type { Project, ProjectExpertScore, RikxRonde } from "../../../lib/db";
+import { PROJECT_STATUSES } from "../../../lib/projectStatus";
+import {
+ BONUS_MALUS_OPTIONS,
+ EXPERT_COUNT,
+ EXPERT_CRITERIA,
+ calculateRikx,
+ type ExpertScore,
+} from "../../../lib/rikxCalculatie";
+import { parseProjectPlanJson } from "../../../lib/projectPlan";
+import { setRikxCalculatie, updateProject } from "../actions";
+import { ProjectPlanFields } from "./ProjectPlanFields";
+
+const inputClass =
+ "w-full h-10 rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 text-sm text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-rikx-orange";
+const labelClass = "text-sm font-medium text-zinc-700 dark:text-zinc-300";
+const cardClass = "bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6";
+
+function toNumber(value: FormDataEntryValue | null): number | null {
+ if (typeof value !== "string" || value.trim() === "") return null;
+ const n = Number(value.replace(",", "."));
+ return Number.isFinite(n) ? n : null;
+}
+
+function rondeById(rondes: RikxRonde[], id: number | null) {
+ return rondes.find((r) => r.id === id) ?? null;
+}
+
+function readInput(form: HTMLFormElement, rondes: RikxRonde[]) {
+ const fd = new FormData(form);
+ const ronde = rondeById(rondes, Number(fd.get("ronde_id")) || null);
+ return {
+ ronde_id: ronde?.id ?? null,
+ projectkosten: toNumber(fd.get("projectkosten")),
+ deelnemers_werk: toNumber(fd.get("deelnemers_werk")),
+ deelnemers_skills: toNumber(fd.get("deelnemers_skills")),
+ financiering_extern_pct: toNumber(fd.get("financiering_extern_pct")),
+ bonus_malus: (fd.get("bonus_malus") as string) || null,
+ rikx_waarde: ronde?.rikx_waarde ?? null,
+ rikx_prijs: ronde?.rikx_prijs ?? null,
+ expert_scores: Array.from({ length: EXPERT_COUNT }, (_, i) =>
+ Object.fromEntries(EXPERT_CRITERIA.map((c) => [c.id, toNumber(fd.get(`expert_${i + 1}_${c.id}`))]))
+ ) as ExpertScore[],
+ };
+}
+
+const nl = (digits: number) => new Intl.NumberFormat("nl-NL", { maximumFractionDigits: digits });
+const euro = new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR" });
+const fmt = (v: number | null, kind: "getal" | "euro" | "geheel" = "getal") =>
+ v === null ? "—" : kind === "euro" ? euro.format(v) : nl(kind === "geheel" ? 0 : 2).format(v);
+
+function Field({
+ label,
+ hint,
+ children,
+}: {
+ label: string;
+ hint?: string;
+ children: React.ReactNode;
+}) {
+ return (
+