"use client"; import Link from "next/link"; import { useRef, useState } from "react"; import type { Project, RikxRonde } from "../../../lib/db"; import { PROJECT_STATUSES, RIKX_CALCULATIE_VANAF_STATUS, isStatusVanaf } from "../../../lib/projectStatus"; import { BONUS_MALUS_OPTIONS, REKENMETHODES, calculateRikx, rikxInputFromProject, type ExpertScore, } from "../../../lib/rikxCalculatie"; import { parseProjectPlanJson } from "../../../lib/projectPlan"; import { updateProject } from "../actions"; import { PROJECT_FORM_ID } from "./formId"; import { ProjectPlanFields } from "./ProjectPlanFields"; import { ExpertBeoordelingen, type BeoordelingMetNaam } from "./ExpertBeoordelingen"; 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[], expertScores: ExpertScore[]) { 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, // Expertscores komen van de experts zelf en zijn hier niet te wijzigen expert_scores: expertScores, }; } 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 ( ); } function TabHeading({ title, description }: { title: string; description: string }) { return (

{title}

{description}

); } export type ProjectTab = "plan" | "calculatie"; const BEOORDELINGEN = [ { id: "goedgekeurd", label: "Goedgekeurd", activeClass: "bg-green-600 text-white" }, { id: "afgekeurd", label: "Afgekeurd", activeClass: "bg-red-600 text-white" }, ] as const; // Schakelaar Goedgekeurd | Afgekeurd. Begint leeg (nog niet beoordeeld); // nogmaals op de actieve keuze klikken maakt de keuze ongedaan. function BeoordelingSwitch({ defaultValue }: { defaultValue: string | null }) { const [value, setValue] = useState(defaultValue ?? ""); return (
{BEOORDELINGEN.map((b) => { const active = value === b.id; return ( ); })}
{!value && Nog niet beoordeeld}
); } export function ProjectDetailForm({ project, beoordelingen, rondes, impactmakers, initialTab, }: { project: Project; beoordelingen: BeoordelingMetNaam[]; rondes: RikxRonde[]; impactmakers: { id: number; name: string }[]; initialTab: ProjectTab; }) { const formRef = useRef(null); // Live status uit de dropdown, zodat de tab Rikx calculatie direct verschijnt of verdwijnt. const [status, setStatus] = useState(project.status); const calculatieZichtbaar = isStatusVanaf(status, RIKX_CALCULATIE_VANAF_STATUS); const [tab, setTab] = useState(initialTab); const activeTab: ProjectTab = tab === "calculatie" && calculatieZichtbaar ? "calculatie" : "plan"; const initialRonde = rondeById(rondes, project.ronde_id); const [input, setInput] = useState(() => ({ ronde_id: initialRonde?.id ?? null, ...rikxInputFromProject(project, beoordelingen, initialRonde), })); const result = calculateRikx(input); const ronde = rondeById(rondes, input.ronde_id); // De browser weigert te versturen bij een ongeldig veld, maar toont geen melding als dat veld op een // verborgen tab staat. Daarom: naar die tab gaan en de melding bij het eerste ongeldige veld tonen. const invalidHandledRef = useRef(false); function handleInvalid(e: React.FormEvent) { if (invalidHandledRef.current) return; const field = e.target as HTMLInputElement; const panel = field.closest("[role=tabpanel]"); if (!panel?.hidden) return; invalidHandledRef.current = true; setTab(panel.id === "panel-calculatie" ? "calculatie" : "plan"); setTimeout(() => { invalidHandledRef.current = false; field.reportValidity(); field.focus(); }, 50); } const results: { label: string; value: string; strong?: boolean }[] = [ { label: "N-score", value: fmt(result.n_score) }, { label: "Scale", value: fmt(result.scale, "geheel") }, { label: "Impactscore M", value: fmt(result.impactscore_m) }, { label: "IS +/- BM", value: fmt(result.is_bm) }, { label: "Impactwaarde", value: fmt(result.impactwaarde) }, { label: "Minus cofinanciering", value: fmt(result.minus_cofinanciering) }, { label: "I - cofin", value: fmt(result.i_cofin) }, { label: "Kosten/impact", value: fmt(result.kosten_impact) }, { label: "Totale impactwaarde", value: fmt(result.totale_impactwaarde, "euro"), strong: true }, { label: "Totaal aantal Rikx", value: fmt(result.totaal_aantal_rikx), strong: true }, { label: "Meerwaarde", value: fmt(result.meerwaarde, "euro"), strong: true }, { label: "Efficiëntiewaarde", value: fmt(result.efficientie_waarde), strong: true }, ]; return (
formRef.current && setInput(readInput(formRef.current, rondes, input.expert_scores))} onInvalidCapture={handleInvalid} className="flex flex-col gap-6" >
{([ { id: "plan", label: "Project plan", zichtbaar: true }, { id: "calculatie", label: "Rikx calculatie", zichtbaar: calculatieZichtbaar }, ] as const) .filter((t) => t.zichtbaar) .map((t) => ( ))}
{/* Tabs worden verborgen in plaats van weggelaten, zodat alle waarden bij opslaan meegaan. */}