From f66f32ee9ded86399043ab66d0a491f3977d07fa Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sun, 19 Apr 2026 17:00:27 +0200 Subject: [PATCH] fix(pitch-deck): all financial slides now read from fp_* tables via useFpKPIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New shared hook: useFpKPIs — loads annual KPIs from fp_guv/liquiditaet/personal/kunden. Replaces useFinancialModel (simplified model) for KPI display on all slides. Slides updated: - CompetitionSlide: "110 Gesetze" → "380+ Regularien & Normen" - BusinessModelSlide: ACV + Gross Margin from fp_* (was useFinancialModel) - ExecutiveSummarySlide: Unternehmensentwicklung from fp_* (was useFinancialModel) - FinancialsSlide: KPI cards from fp_* (ARR, Customers, Break-Even, EBIT 2030) All slides now show consistent numbers from the same source of truth (fp_* tables). Co-Authored-By: Claude Opus 4.6 (1M context) --- pitch-deck/components/PitchDeck.tsx | 6 +- .../components/slides/BusinessModelSlide.tsx | 17 ++-- .../components/slides/CompetitionSlide.tsx | 2 +- .../slides/ExecutiveSummarySlide.tsx | 26 +++--- .../components/slides/FinancialsSlide.tsx | 40 ++++++--- pitch-deck/lib/hooks/useFpKPIs.ts | 90 +++++++++++++++++++ 6 files changed, 141 insertions(+), 40 deletions(-) create mode 100644 pitch-deck/lib/hooks/useFpKPIs.ts diff --git a/pitch-deck/components/PitchDeck.tsx b/pitch-deck/components/PitchDeck.tsx index 7a9215a..720cee9 100644 --- a/pitch-deck/components/PitchDeck.tsx +++ b/pitch-deck/components/PitchDeck.tsx @@ -161,7 +161,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, /> ) case 'executive-summary': - return + return case 'cover': return case 'problem': @@ -179,7 +179,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, case 'market': return case 'business-model': - return + return case 'traction': return case 'competition': @@ -187,7 +187,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, case 'team': return case 'financials': - return + return case 'the-ask': return case 'cap-table': diff --git a/pitch-deck/components/slides/BusinessModelSlide.tsx b/pitch-deck/components/slides/BusinessModelSlide.tsx index deb4fb5..db3d5fb 100644 --- a/pitch-deck/components/slides/BusinessModelSlide.tsx +++ b/pitch-deck/components/slides/BusinessModelSlide.tsx @@ -2,7 +2,7 @@ import { Language } from '@/lib/types' import { t } from '@/lib/i18n' -import { useFinancialModel } from '@/lib/hooks/useFinancialModel' +import { useFpKPIs } from '@/lib/hooks/useFpKPIs' import GradientText from '../ui/GradientText' import FadeInView from '../ui/FadeInView' import GlassCard from '../ui/GlassCard' @@ -13,18 +13,17 @@ interface BusinessModelSlideProps { products?: unknown[] investorId?: string | null preferredScenarioId?: string | null + isWandeldarlehen?: boolean } -export default function BusinessModelSlide({ lang, investorId, preferredScenarioId }: BusinessModelSlideProps) { +export default function BusinessModelSlide({ lang, isWandeldarlehen }: BusinessModelSlideProps) { const i = t(lang) const de = lang === 'de' - const fm = useFinancialModel(investorId || null, preferredScenarioId) - const summary = fm.activeResults?.summary - const results = fm.activeResults?.results || [] - const lastResult = results[results.length - 1] - const finalCustomers = summary?.final_customers || 0 - const finalArr = summary?.final_arr || 0 + const { last } = useFpKPIs(isWandeldarlehen) + const finalCustomers = last?.customers || 0 + const finalArr = last?.arr || 0 const acv = finalCustomers > 0 ? Math.round(finalArr / finalCustomers) : 0 + const grossMargin = last?.grossMargin ?? 0 const tiers = [ { @@ -62,7 +61,7 @@ export default function BusinessModelSlide({ lang, investorId, preferredScenario }, ] - const grossMargin = lastResult?.gross_margin_pct ?? 0 + // grossMargin already defined above from useFpKPIs const acvLabel = acv > 0 ? (de ? `${(acv / 1000).toFixed(1).replace('.', ',')}k EUR` : `EUR ${(acv / 1000).toFixed(1)}k`) : '—' diff --git a/pitch-deck/components/slides/CompetitionSlide.tsx b/pitch-deck/components/slides/CompetitionSlide.tsx index c9f0577..fb176d6 100644 --- a/pitch-deck/components/slides/CompetitionSlide.tsx +++ b/pitch-deck/components/slides/CompetitionSlide.tsx @@ -193,7 +193,7 @@ const ALL_FEATURES: ComparisonFeature[] = [ // Top 5 Differentiators (isDiff=true) — no other vendor has ANY of these { de: 'Self-Hosted / On-Premise', en: 'Self-Hosted / On-Premise', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true }, { de: 'Code-Security & DevSecOps (6 Tools)', en: 'Code Security & DevSecOps (6 Tools)', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true }, - { de: '110 Gesetze & Regularien, 25.000+ Sicherheitskontrollen', en: '110 Laws & Regulations, 25,000+ Security Controls', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true }, + { de: '380+ Regularien & Normen, 25.000+ Prüfaspekte', en: '380+ Regulations & Standards, 25,000+ Audit Controls', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true }, { de: 'Hardware-Moat (Mac Mini/Studio)', en: 'Hardware Moat (Mac Mini/Studio)', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true }, { de: 'PII-Redaction LLM Gateway', en: 'PII Redaction LLM Gateway', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true }, // More USPs diff --git a/pitch-deck/components/slides/ExecutiveSummarySlide.tsx b/pitch-deck/components/slides/ExecutiveSummarySlide.tsx index 27d2cb5..3ff9c32 100644 --- a/pitch-deck/components/slides/ExecutiveSummarySlide.tsx +++ b/pitch-deck/components/slides/ExecutiveSummarySlide.tsx @@ -1,10 +1,9 @@ 'use client' -import { useCallback, useMemo } from 'react' +import { useCallback } from 'react' import { Language, PitchData } from '@/lib/types' import { t, formatEur } from '@/lib/i18n' -import { useFinancialModel } from '@/lib/hooks/useFinancialModel' -import { computeAnnualKPIs } from '@/lib/finanzplan/annual-kpis' +import { useFpKPIs } from '@/lib/hooks/useFpKPIs' import GradientText from '../ui/GradientText' import FadeInView from '../ui/FadeInView' import GlassCard from '../ui/GlassCard' @@ -15,19 +14,16 @@ interface ExecutiveSummarySlideProps { data: PitchData investorId?: string | null preferredScenarioId?: string | null + isWandeldarlehen?: boolean } -export default function ExecutiveSummarySlide({ lang, data, investorId, preferredScenarioId }: ExecutiveSummarySlideProps) { +export default function ExecutiveSummarySlide({ lang, data, investorId, preferredScenarioId, isWandeldarlehen }: ExecutiveSummarySlideProps) { const i = t(lang) const es = i.executiveSummary const de = lang === 'de' - // Financial model for Unternehmensentwicklung - const fm = useFinancialModel(investorId || null, preferredScenarioId) - const annualKPIs = useMemo( - () => computeAnnualKPIs(fm.activeResults?.results || []), - [fm.activeResults], - ) + // Unternehmensentwicklung from fp_* tables (source of truth) + const { kpis: fpKPIs } = useFpKPIs(isWandeldarlehen) const funding = data.funding const amount = funding?.amount_eur || 0 @@ -538,16 +534,18 @@ export default function ExecutiveSummarySlide({ lang, data, investorId, preferre {de ? 'Jahr' : 'Year'}MA{de ? 'Kunden' : 'Customers'}ARR
- {annualKPIs.length === 0 ? ( + {!fpKPIs.y2026 ? (

{de ? 'Lade Finanzplan...' : 'Loading financial plan...'}

- ) : annualKPIs.map((k, idx) => { + ) : [2026, 2027, 2028, 2029, 2030].map((year, idx) => { + const k = fpKPIs[`y${year}`] + if (!k) return null const arrLabel = k.arr >= 1_000_000 ? (de ? `~${(k.arr / 1_000_000).toFixed(1).replace('.', ',')} Mio. EUR` : `~EUR ${(k.arr / 1_000_000).toFixed(1)}M`) : (de ? `~${Math.round(k.arr / 1000)}k EUR` : `~EUR ${Math.round(k.arr / 1000)}k`) return (
- {k.year} - {k.employees} + {year} + {k.headcount} ~{k.customers.toLocaleString('de-DE')} = 3 ? 'text-emerald-300 font-bold' : 'text-white/70'}`}>{arrLabel}
diff --git a/pitch-deck/components/slides/FinancialsSlide.tsx b/pitch-deck/components/slides/FinancialsSlide.tsx index 727adff..e25b4f2 100644 --- a/pitch-deck/components/slides/FinancialsSlide.tsx +++ b/pitch-deck/components/slides/FinancialsSlide.tsx @@ -5,6 +5,7 @@ import { Language } from '@/lib/types' import { t } from '@/lib/i18n' import ProjectionFooter from '../ui/ProjectionFooter' import { useFinancialModel } from '@/lib/hooks/useFinancialModel' +import { useFpKPIs } from '@/lib/hooks/useFpKPIs' import GradientText from '../ui/GradientText' import FadeInView from '../ui/FadeInView' import FinancialChart from '../ui/FinancialChart' @@ -23,9 +24,10 @@ interface FinancialsSlideProps { lang: Language investorId: string | null preferredScenarioId?: string | null + isWandeldarlehen?: boolean } -export default function FinancialsSlide({ lang, investorId, preferredScenarioId }: FinancialsSlideProps) { +export default function FinancialsSlide({ lang, investorId, preferredScenarioId, isWandeldarlehen }: FinancialsSlideProps) { const i = t(lang) const fm = useFinancialModel(investorId, preferredScenarioId) const [activeTab, setActiveTab] = useState('overview') @@ -35,6 +37,18 @@ export default function FinancialsSlide({ lang, investorId, preferredScenarioId const summary = activeResults?.summary const lastResult = activeResults?.results[activeResults.results.length - 1] + // KPI cards from fp_* tables (source of truth) + const { last: fpLast, kpis: fpKPIs } = useFpKPIs(isWandeldarlehen) + const kpiArr = fpLast?.arr || summary?.final_arr || 0 + const kpiCustomers = fpLast?.customers || summary?.final_customers || 0 + const kpiEbit = fpKPIs?.y2029?.ebit // First profitable year + const kpiBreakEven = (() => { + for (const y of [2026, 2027, 2028, 2029, 2030]) { + if ((fpKPIs[`y${y}`]?.ebit || 0) > 0) return y + } + return 0 + })() + // Build scenario color map const scenarioColors: Record = {} fm.scenarios.forEach(s => { scenarioColors[s.id] = s.color }) @@ -74,9 +88,9 @@ export default function FinancialsSlide({ lang, investorId, preferredScenarioId
= 1_000_000 ? Math.round(kpiArr / 1_000_000 * 10) / 10 : Math.round(kpiArr / 1000)} + suffix={kpiArr >= 1_000_000 ? ' Mio.' : 'k'} + decimals={kpiArr >= 1_000_000 ? 1 : 0} trend="up" color="#6366f1" delay={0.1} @@ -84,28 +98,28 @@ export default function FinancialsSlide({ lang, investorId, preferredScenarioId /> = 3 ? 'up' : 'down'} + trend={(fpLast?.ebit || 0) > 0 ? 'up' : 'down'} color="#a855f7" delay={0.25} + subLabel="EUR" />
diff --git a/pitch-deck/lib/hooks/useFpKPIs.ts b/pitch-deck/lib/hooks/useFpKPIs.ts new file mode 100644 index 0000000..f19d48b --- /dev/null +++ b/pitch-deck/lib/hooks/useFpKPIs.ts @@ -0,0 +1,90 @@ +'use client' + +import { useState, useEffect } from 'react' + +export interface FpAnnualKPIs { + revenue: number + ebit: number + personal: number + netIncome: number + steuern: number + liquiditaet: number + customers: number + headcount: number + mrr: number + arr: number + arpu: number + revPerEmp: number + ebitMargin: number + grossMargin: number +} + +interface SheetRow { + row_label?: string + values?: Record + values_total?: Record +} + +/** + * Loads annual KPIs directly from fp_* tables (source of truth). + * Returns a map of year keys (y2026-y2030) to KPI objects. + */ +export function useFpKPIs(isWandeldarlehen?: boolean) { + const [kpis, setKpis] = useState>({}) + const [loading, setLoading] = useState(true) + + useEffect(() => { + async function load() { + try { + const param = isWandeldarlehen ? '?scenarioId=c0000000-0000-0000-0000-000000000200' : '' + 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: SheetRow[] = guv.rows || [] + const liqRows: SheetRow[] = liq.rows || [] + const persRows: SheetRow[] = pers.rows || [] + const kundenRows: SheetRow[] = kunden.rows || [] + + const findGuv = (label: string) => guvRows.find(r => (r.row_label || '').includes(label)) + const findLiq = (label: string) => liqRows.find(r => (r.row_label || '').includes(label)) + const kundenGesamt = kundenRows.find(r => r.row_label === 'Bestandskunden gesamt') + + const result: Record = {} + + for (const y of [2026, 2027, 2028, 2029, 2030]) { + const yk = `y${y}` + const mk = `m${(y - 2026) * 12 + 12}` // December + + 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 => ((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 grossMargin = revenue > 0 ? Math.round(((revenue - (findGuv('Summe Materialaufwand')?.values?.[yk] || 0)) / revenue) * 100) : 0 + + result[yk] = { revenue, ebit, personal, netIncome, steuern, liquiditaet, customers, headcount, mrr, arr, arpu, revPerEmp, ebitMargin, grossMargin } + } + + setKpis(result) + } catch { /* ignore */ } + setLoading(false) + } + load() + }, [isWandeldarlehen]) + + const last = kpis.y2030 + return { kpis, loading, last } +}