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,152 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user