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:
2026-10-08 10:26:30 +02:00
parent 46eecc31a4
commit 9f0bd87c40
25 changed files with 2161 additions and 55 deletions
+118
View File
@@ -0,0 +1,118 @@
// Rikx-rekenmethode, overgenomen uit _doc/Rikx rekenmethode uitleg.xlsx (tabblad "Uitleg").
// Pure functies zonder database-toegang, zodat het formulier live kan rekenen.
export const BONUS_MALUS_OPTIONS = [
{ id: "++", label: "++", factor: 1.2 },
{ id: "+", label: "+", factor: 1.1 },
{ id: "", label: "Geen", factor: 1.0 },
// In de spreadsheet staat "- = 1.1" en "-- = -1.2"; dat zijn vermoedelijk typefouten.
{ id: "-", label: "-", factor: 0.9 },
{ id: "--", label: "--", factor: 0.8 },
] as const;
export const EXPERT_CRITERIA = [
{ id: "duur_impact", label: "Duur van impact" },
{ id: "levensdomeinen", label: "Aantal levensdomeinen" },
{ id: "basissituatie", label: "Basissituatie doelgroep" },
] as const;
export const EXPERT_COUNT = 3;
export type ExpertCriterium = (typeof EXPERT_CRITERIA)[number]["id"];
export type ExpertScore = Record<ExpertCriterium, number | null>;
export type RikxInput = {
projectkosten: number | null;
deelnemers_werk: number | null;
deelnemers_skills: number | null;
financiering_extern_pct: number | null;
bonus_malus: string | null;
// Uit de Rikx Ronde van het project
rikx_waarde: number | null;
rikx_prijs: number | null;
expert_scores: ExpertScore[];
};
export type RikxResult = {
n_score: number | null;
scale: number | null;
impactscore_m: number | null;
is_bm: number | null;
impactwaarde: number | null;
minus_cofinanciering: number | null;
i_cofin: number | null;
kosten_impact: number | null;
totale_impactwaarde: number | null;
totaal_aantal_rikx: number | null;
meerwaarde: number | null;
efficientie_waarde: number | null;
};
function average(values: (number | null)[]): number | null {
const nums = values.filter((v): v is number => v !== null && Number.isFinite(v));
return nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : null;
}
// Staffel uit de spreadsheet: 1 = 1, 2-5 = 2, 5-10 = 3, 10-25 = 4, >25 = 5
export function scaleFromNScore(n: number): number {
if (n <= 1) return 1;
if (n <= 5) return 2;
if (n <= 10) return 3;
if (n <= 25) return 4;
return 5;
}
export function bonusMalusFactor(id: string | null): number {
return BONUS_MALUS_OPTIONS.find((o) => o.id === (id ?? ""))?.factor ?? 1;
}
// Vermenigvuldigen/delen die null doorgeven als een invoer ontbreekt of de uitkomst ongeldig is.
function mul(a: number | null, b: number | null) {
return a === null || b === null ? null : a * b;
}
function div(a: number | null, b: number | null) {
return a === null || b === null || b === 0 ? null : a / b;
}
export function calculateRikx(input: RikxInput): RikxResult {
const { projectkosten, deelnemers_werk, deelnemers_skills, rikx_waarde, rikx_prijs } = input;
// De spreadsheet-formule (B4*5)+(B5*1)/6 mist haakjes; bedoeld is ((werk*5)+skills)/6.
const n_score =
deelnemers_werk === null || deelnemers_skills === null
? null
: (deelnemers_werk * 5 + deelnemers_skills) / 6;
const scale = n_score === null ? null : scaleFromNScore(n_score);
const impactscore_m = average(input.expert_scores.map((s) => average(Object.values(s))));
const is_bm = impactscore_m === null ? null : impactscore_m * impactscore_m * bonusMalusFactor(input.bonus_malus);
const impactwaarde = mul(is_bm, scale);
const minus_cofinanciering = (100 - (input.financiering_extern_pct ?? 0)) / 100;
const i_cofin = mul(impactwaarde, minus_cofinanciering);
const kosten_impact = div(projectkosten, i_cofin);
const totale_impactwaarde = mul(i_cofin, rikx_waarde);
const totaal_aantal_rikx = div(projectkosten, rikx_prijs);
const meerwaarde =
totale_impactwaarde === null || projectkosten === null ? null : totale_impactwaarde - projectkosten;
const efficientie_waarde = div(meerwaarde, projectkosten);
return {
n_score,
scale,
impactscore_m,
is_bm,
impactwaarde,
minus_cofinanciering,
i_cofin,
kosten_impact,
totale_impactwaarde,
totaal_aantal_rikx,
meerwaarde,
efficientie_waarde,
};
}
// Rikx prijs volgens de sheet: Rikx waarde / 10 (behapbaarder bedrag voor kleine investeerders)
export function defaultRikxPrijs(rikxWaarde: number): number {
return rikxWaarde / 10;
}