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