From 607dab4f265f8ad07e6b97d4fe95a5630f2c003b Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sun, 19 Apr 2026 08:37:46 +0200 Subject: [PATCH] fix(pitch-deck): KPIs + Charts on Folie 28 now read from fp_* tables directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously KPIs/Charts used useFinancialModel (simplified model) which had different assumptions than the fp_* tables (source of truth). Now: KPIs tab loads from fp_guv, fp_liquiditaet, fp_personalkosten, fp_kunden via API. Charts (MRR, EBIT, Headcount) also use fp_* data. Removed dependency on useFinancialModel and computeAnnualKPIs for this slide. Added Liquidität (Dez) row to KPIs table. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/slides/FinanzplanSlide.tsx | 119 +++++++++++++----- 1 file changed, 85 insertions(+), 34 deletions(-) diff --git a/pitch-deck/components/slides/FinanzplanSlide.tsx b/pitch-deck/components/slides/FinanzplanSlide.tsx index 0385dbf..cb38e00 100644 --- a/pitch-deck/components/slides/FinanzplanSlide.tsx +++ b/pitch-deck/components/slides/FinanzplanSlide.tsx @@ -1,10 +1,8 @@ 'use client' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { Language } from '@/lib/types' import { t } from '@/lib/i18n' -import { useFinancialModel } from '@/lib/hooks/useFinancialModel' -import { computeAnnualKPIs } from '@/lib/finanzplan/annual-kpis' import ProjectionFooter from '../ui/ProjectionFooter' import GradientText from '../ui/GradientText' import FadeInView from '../ui/FadeInView' @@ -103,12 +101,60 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId const [yearOffset, setYearOffset] = useState(0) // 0=2026, 1=2027, ... const de = lang === 'de' - // Financial model — same source as FinancialsSlide (Slide 15) - const fm = useFinancialModel(investorId || null, preferredScenarioId) - const annualKPIs = useMemo( - () => computeAnnualKPIs(fm.activeResults?.results || []), - [fm.activeResults], - ) + // KPIs loaded directly from fp_* tables (source of truth) + const [fpKPIs, setFpKPIs] = useState>>({}) + + useEffect(() => { + async function loadKPIs() { + const param = selectedScenarioId ? `?scenarioId=${selectedScenarioId}` : '' + try { + const [guvRes, liqRes, persRes, kundenRes] = await Promise.all([ + fetch(`/api/finanzplan/guv${param}`, { cache: 'no-store' }), + fetch(`/api/finanzplan/liquiditaet${param}`, { cache: 'no-store' }), + fetch(`/api/finanzplan/personalkosten${param}`, { cache: 'no-store' }), + fetch(`/api/finanzplan/kunden${param}`, { cache: 'no-store' }), + ]) + const [guv, liq, pers, kunden] = await Promise.all([guvRes.json(), liqRes.json(), persRes.json(), kundenRes.json()]) + + const guvRows = guv.rows || [] + const liqRows = liq.rows || [] + const persRows = pers.rows || [] + const kundenRows = kunden.rows || [] + + const findGuv = (label: string) => guvRows.find((r: SheetRow) => (r.row_label || '').includes(label)) + const findLiq = (label: string) => liqRows.find((r: SheetRow) => (r.row_label || '').includes(label)) + const kundenGesamt = kundenRows.find((r: SheetRow) => r.row_label === 'Bestandskunden gesamt') + + const years = [2026, 2027, 2028, 2029, 2030] + const kpis: Record> = {} + + for (const y of years) { + const yk = `y${y}` + const m12 = (y - 2026) * 12 + 12 // December of each year + const mk = `m${m12}` + + const revenue = findGuv('Umsatzerlöse')?.values?.[yk] || 0 + const ebit = findGuv('EBIT')?.values?.[yk] || 0 + const personal = findGuv('Summe Personalaufwand')?.values?.[yk] || 0 + const netIncome = findGuv('Jahresüberschuss')?.values?.[yk] || findGuv('Jahresueber')?.values?.[yk] || 0 + const steuern = findGuv('Steuern gesamt')?.values?.[yk] || 0 + const liquiditaet = findLiq('LIQUIDIT')?.values?.[mk] || findLiq('LIQUIDITAET')?.values?.[mk] || 0 + const customers = kundenGesamt?.values?.[mk] || 0 + const headcount = persRows.filter((r: SheetRow) => ((r.values_total || r.values)?.[mk] || 0) > 0).length + const mrr = revenue > 0 ? Math.round(revenue / 12) : 0 + const arr = mrr * 12 + const arpu = customers > 0 ? Math.round(mrr / customers) : 0 + const revPerEmp = headcount > 0 ? Math.round(revenue / headcount) : 0 + const ebitMargin = revenue > 0 ? Math.round((ebit / revenue) * 100) : 0 + const burnRate = liquiditaet < 0 ? Math.round(Math.abs(ebit / 12)) : 0 + + kpis[yk] = { revenue, ebit, personal, netIncome, steuern, liquiditaet, customers, headcount, mrr, arr, arpu, revPerEmp, ebitMargin, burnRate } + } + setFpKPIs(kpis) + } catch { /* ignore */ } + } + loadKPIs() + }, [selectedScenarioId]) // Load sheet list + scenarios useEffect(() => { @@ -247,25 +293,25 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId {(() => { - if (annualKPIs.length === 0) return ( - {de ? 'Finanzmodell wird geladen...' : 'Loading financial model...'} + const years = ['y2026', 'y2027', 'y2028', 'y2029', 'y2030'] + if (!fpKPIs['y2026']) return ( + {de ? 'Finanzplan wird geladen...' : 'Loading financial plan...'} ) + const v = (yk: string, key: string) => fpKPIs[yk]?.[key] || 0 const kpiRows = [ - { label: 'MRR (Dez)', values: annualKPIs.map(k => k.mrr), unit: '€', bold: true }, - { label: 'ARR', values: annualKPIs.map(k => k.arr), unit: '€', bold: true }, - { label: de ? 'Kunden (Dez)' : 'Customers (Dec)', values: annualKPIs.map(k => k.customers), unit: '', bold: false }, - { label: 'ARPU (MRR/Kunden)', values: annualKPIs.map(k => k.arpu), unit: '€', bold: false }, - { label: de ? 'Mitarbeiter' : 'Employees', values: annualKPIs.map(k => k.employees), unit: '', bold: false }, - { label: de ? 'Umsatz/Mitarbeiter' : 'Revenue/Employee', values: annualKPIs.map(k => k.revenuePerEmployee), unit: '€', bold: false }, - { label: de ? 'Personalkosten' : 'Personnel Costs', values: annualKPIs.map(k => k.personnelCosts), unit: '€', bold: false }, - { label: 'EBIT', values: annualKPIs.map(k => k.ebit), unit: '€', bold: true }, - { label: de ? 'EBIT-Marge' : 'EBIT Margin', values: annualKPIs.map(k => k.ebitMargin), unit: '%', bold: false }, - { label: de ? 'Steuern' : 'Taxes', values: annualKPIs.map(k => k.taxes), unit: '€', bold: false }, - { label: de ? 'Jahresueberschuss' : 'Net Income', values: annualKPIs.map(k => k.netIncome), unit: '€', bold: true }, - { label: de ? 'Serverkosten/Kunde' : 'Server Cost/Customer', values: annualKPIs.map(k => k.serverCostPerCustomer), unit: '€', bold: false }, - { label: de ? 'Bruttomarge' : 'Gross Margin', values: annualKPIs.map(k => k.grossMargin), unit: '%', bold: false }, - { label: 'Burn Rate (Dez)', values: annualKPIs.map(k => k.burnRate), unit: '€/Mo', bold: false }, - { label: de ? 'Runway (Monate)' : 'Runway (months)', values: annualKPIs.map(k => k.runway === null ? '∞' : k.runway), unit: '', bold: false }, + { label: 'MRR (Dez)', values: years.map(y => v(y, 'mrr')), unit: '€', bold: true }, + { label: 'ARR', values: years.map(y => v(y, 'arr')), unit: '€', bold: true }, + { label: de ? 'Kunden (Dez)' : 'Customers (Dec)', values: years.map(y => v(y, 'customers')), unit: '', bold: false }, + { label: 'ARPU (MRR/Kunden)', values: years.map(y => v(y, 'arpu')), unit: '€', bold: false }, + { label: de ? 'Mitarbeiter' : 'Employees', values: years.map(y => v(y, 'headcount')), unit: '', bold: false }, + { label: de ? 'Umsatz/Mitarbeiter' : 'Revenue/Employee', values: years.map(y => v(y, 'revPerEmp')), unit: '€', bold: false }, + { label: de ? 'Personalkosten' : 'Personnel Costs', values: years.map(y => v(y, 'personal')), unit: '€', bold: false }, + { label: 'EBIT', values: years.map(y => v(y, 'ebit')), unit: '€', bold: true }, + { label: de ? 'EBIT-Marge' : 'EBIT Margin', values: years.map(y => v(y, 'ebitMargin')), unit: '%', bold: false }, + { label: de ? 'Steuern' : 'Taxes', values: years.map(y => v(y, 'steuern')), unit: '€', bold: false }, + { label: de ? 'Jahresüberschuss' : 'Net Income', values: years.map(y => v(y, 'netIncome')), unit: '€', bold: true }, + { label: de ? 'Liquidität (Dez)' : 'Cash (Dec)', values: years.map(y => v(y, 'liquiditaet')), unit: '€', bold: true }, + { label: 'Burn Rate', values: years.map(y => v(y, 'burnRate')), unit: '€/Mo', bold: false }, ] return kpiRows.map((row, idx) => ( @@ -299,9 +345,11 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId

{de ? 'MRR & Kundenentwicklung' : 'MRR & Customer Growth'}

{(() => { - const maxMrr = Math.max(...annualKPIs.map(k => k.mrr), 1) - const maxCust = Math.max(...annualKPIs.map(k => k.customers), 1) - return annualKPIs.map(k => ({ year: String(k.year), mrr: k.mrr, cust: k.customers, max_mrr: maxMrr, max_cust: maxCust })) + const years = ['y2026','y2027','y2028','y2029','y2030'] + const data = years.map(y => ({ year: y.slice(1), mrr: fpKPIs[y]?.mrr || 0, cust: fpKPIs[y]?.customers || 0 })) + const maxMrr = Math.max(...data.map(d => d.mrr), 1) + const maxCust = Math.max(...data.map(d => d.cust), 1) + return data.map(d => ({ ...d, max_mrr: maxMrr, max_cust: maxCust })) })().map((d, idx) => (
@@ -332,8 +380,10 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId

EBIT

{(() => { - const maxAbs = Math.max(...annualKPIs.map(k => Math.abs(k.ebit)), 1) - return annualKPIs.map((k, idx) => { + const years = ['y2026','y2027','y2028','y2029','y2030'] + const data = years.map(y => ({ year: y.slice(1), ebit: fpKPIs[y]?.ebit || 0 })) + const maxAbs = Math.max(...data.map(d => Math.abs(d.ebit)), 1) + return data.map((k, idx) => { const h = Math.abs(k.ebit) / maxAbs * 100 return (
@@ -362,9 +412,10 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId

{de ? 'Personalaufbau' : 'Headcount'}

{(() => { - const maxEmp = Math.max(...annualKPIs.map(k => k.employees), 1) - return annualKPIs.map(k => ({ year: String(k.year), val: k.employees })) - .map((d, idx) => ( + const years = ['y2026','y2027','y2028','y2029','y2030'] + const data = years.map(y => ({ year: y.slice(1), val: fpKPIs[y]?.headcount || 0 })) + const maxEmp = Math.max(...data.map(d => d.val), 1) + return data.map((d, idx) => (