9f0bd87c40
- 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>
119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
// 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;
|
|
}
|