Files
rikx-beheer/app/projecten/[id]/ProjectPlanFields.tsx
T
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

153 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import type { Project } from "../../../lib/db";
import {
ANDERS,
PROJECT_PLAN_GROUPS,
andersKey,
type PlanField,
type ProjectPlan,
} from "../../../lib/projectPlan";
import { MultiSelectDropdown } from "./MultiSelectDropdown";
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 textareaClass =
"w-full min-h-28 rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-rikx-orange";
type ColumnKey = "projectkosten" | "deelnemers_werk" | "deelnemers_skills" | "financiering_extern_bedrag" | "financiering_extern_pct";
function FieldInput({
field,
project,
plan,
onSelectChange,
}: {
field: PlanField;
project: Project;
plan: ProjectPlan;
onSelectChange: (key: string, value: string) => void;
}) {
const id = `plan-${field.key}`;
const saved = plan[field.key];
const [anders, setAnders] = useState(Array.isArray(saved) && saved.includes(ANDERS));
switch (field.kind) {
case "textarea":
return <textarea id={id} name={field.key} defaultValue={typeof saved === "string" ? saved : ""} className={textareaClass} />;
case "number": {
const value = field.column ? project[field.key as ColumnKey] : saved;
return (
<input
id={id}
name={field.key}
type="number"
min={field.min}
max={field.max}
step={field.step}
defaultValue={typeof value === "number" ? value : ""}
className={inputClass}
/>
);
}
case "date":
return <input id={id} name={field.key} type="date" defaultValue={typeof saved === "string" ? saved : ""} className={inputClass} />;
case "select":
return (
<select
id={id}
name={field.key}
defaultValue={typeof saved === "string" ? saved : ""}
onChange={(e) => onSelectChange(field.key, e.target.value)}
className={inputClass}
>
<option value="">Maak een keuze</option>
{field.options.map((o) => (
<option key={o} value={o}>
{o}
</option>
))}
</select>
);
case "multiselect": {
const andersValue = plan[andersKey(field.key)];
return (
<>
<MultiSelectDropdown
id={id}
name={field.key}
options={field.options}
defaultValue={Array.isArray(saved) ? saved : []}
onChange={(values) => setAnders(values.includes(ANDERS))}
className={inputClass}
/>
{anders && (
<input
name={andersKey(field.key)}
aria-label={`${field.label} – anders, namelijk`}
placeholder="Anders, namelijk…"
defaultValue={typeof andersValue === "string" ? andersValue : ""}
className={`${inputClass} mt-2`}
/>
)}
</>
);
}
}
}
// Stap 1 "Project plan": alle velden uit lib/projectPlan.ts, gegroepeerd.
export function ProjectPlanFields({ project, plan }: { project: Project; plan: ProjectPlan }) {
// Huidige waarden van selects, voor velden met showIf (bv. "Zo ja, licht toe")
const [selects, setSelects] = useState<Record<string, string>>(() =>
Object.fromEntries(
PROJECT_PLAN_GROUPS.flatMap((g) => g.fields)
.filter((f) => f.kind === "select")
.map((f) => [f.key, typeof plan[f.key] === "string" ? (plan[f.key] as string) : ""])
)
);
return (
<div className="flex flex-col gap-8">
{PROJECT_PLAN_GROUPS.map((group) => (
<fieldset key={group.title} className="flex flex-col gap-4">
<legend className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 mb-3 pb-2 border-b border-zinc-200 dark:border-zinc-800 w-full">
{group.title}
</legend>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-5">
{group.fields
.filter((f) => !f.showIf || selects[f.showIf.key] === f.showIf.equals)
.map((field) => (
<div
key={field.key}
className={[
"flex flex-col gap-1",
field.wide ? "sm:col-span-2" : "",
field.newRow ? "sm:col-start-1" : "",
field.followUp ? "-mt-3 ml-2 pl-4 border-l-2 border-rikx-orange" : "",
].join(" ")}
>
{/* Minimaal twee regels hoog, zodat velden naast elkaar uitlijnen bij labels van ongelijke lengte */}
<label
htmlFor={`plan-${field.key}`}
className={`text-sm font-medium text-zinc-700 dark:text-zinc-300 ${field.wide || field.newRow ? "" : "sm:min-h-10"}`}
>
{field.label}
</label>
<FieldInput
field={field}
project={project}
plan={plan}
onSelectChange={(key, value) => setSelects((s) => ({ ...s, [key]: value }))}
/>
{field.hint && <span className="text-xs text-zinc-500">{field.hint}</span>}
</div>
))}
</div>
</fieldset>
))}
</div>
);
}