// Rikx Waardeberekening Arbeid, overgenomen uit _doc/Rikx rekenmethode uitleg.xlsx (tabblad "Uitleg") // en _doc/Waarderingssystematiek Rikx juni 2023.pdf. // 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; // Schalen volgens _doc/Waarderingssystematiek Rikx juni 2023.pdf ("Inhoudelijke focus") export const EXPERT_CRITERIA = [ { id: "duur_impact", label: "Duur van impact", schaal: "kortstondig → levenslang" }, { id: "levensdomeinen", label: "Aantal levensdomeinen", schaal: "één levensdomein → alle levensdomeinen" }, { id: "basissituatie", label: "Basissituatie doelgroep", schaal: "relatief eenvoudig → relatief zwaar" }, ] as const; // Rikx kent twee rekenmethodes. Alleen "Arbeid" is uitgewerkt (de formule hieronder); // "Jeugd" is zichtbaar maar nog niet te kiezen. export const REKENMETHODES = [ { id: "arbeid", label: "Rikx Waardeberekening Arbeid", omschrijving: "Impact op de arbeidsmarkt: uitstroom naar werk en het opdoen van skills.", beschikbaar: true, }, { id: "jeugd", label: "Rikx Waardeberekening Jeugd", omschrijving: "Nog niet beschikbaar.", beschikbaar: false, }, ] as const; export const DEFAULT_REKENMETHODE = "arbeid"; export function isBeschikbareRekenmethode(value: unknown): value is string { return REKENMETHODES.some((m) => m.id === value && m.beschikbaar); } 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; } // Bouwt de invoer voor calculateRikx uit een opgeslagen project, de beoordelingen van de experts // (elk aantal; Impactscore M = gemiddelde over de experts) en zijn Rikx Ronde. export function rikxInputFromProject( project: { projectkosten: number | null; deelnemers_werk: number | null; deelnemers_skills: number | null; financiering_extern_pct: number | null; bonus_malus: string | null; }, beoordelingen: { duur_impact: number | null; levensdomeinen: number | null; basissituatie: number | null }[], ronde: { rikx_waarde: number | null; rikx_prijs: number | null } | null ): RikxInput { return { projectkosten: project.projectkosten, deelnemers_werk: project.deelnemers_werk, deelnemers_skills: project.deelnemers_skills, financiering_extern_pct: project.financiering_extern_pct, bonus_malus: project.bonus_malus, rikx_waarde: ronde?.rikx_waarde ?? null, rikx_prijs: ronde?.rikx_prijs ?? null, expert_scores: beoordelingen.map((b) => ({ duur_impact: b.duur_impact, levensdomeinen: b.levensdomeinen, basissituatie: b.basissituatie, })), }; }