// 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; 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; }