"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 (
);
}
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 (
);
}