diff --git a/pitch-deck/components/PitchDeck.tsx b/pitch-deck/components/PitchDeck.tsx index e71ac1e..7a9215a 100644 --- a/pitch-deck/components/PitchDeck.tsx +++ b/pitch-deck/components/PitchDeck.tsx @@ -198,7 +198,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, case 'ai-qa': return case 'annex-assumptions': - return + return case 'annex-architecture': return case 'annex-gtm': diff --git a/pitch-deck/components/slides/AssumptionsSlide.tsx b/pitch-deck/components/slides/AssumptionsSlide.tsx index 69841f5..f20e594 100644 --- a/pitch-deck/components/slides/AssumptionsSlide.tsx +++ b/pitch-deck/components/slides/AssumptionsSlide.tsx @@ -1,17 +1,18 @@ 'use client' +import { useEffect, useState } from 'react' import { Language } from '@/lib/types' import { t } from '@/lib/i18n' import GradientText from '../ui/GradientText' import FadeInView from '../ui/FadeInView' import GlassCard from '../ui/GlassCard' import { TrendingUp, TrendingDown, Minus } from 'lucide-react' -import { useFinancialModel } from '@/lib/hooks/useFinancialModel' interface AssumptionsSlideProps { lang: Language investorId?: string | null preferredScenarioId?: string | null + isWandeldarlehen?: boolean } function fmtArr(v: number, de: boolean): string { @@ -30,45 +31,100 @@ function fmtCash(v: number, de: boolean): string { return de ? `~${Math.round(v / 1000)}k EUR` : `~EUR ${Math.round(v / 1000)}k` } -function breakEvenYear(month: number | null): string { - if (!month || month <= 0) return '—' - const year = 2026 + Math.floor((month - 1) / 12) - return String(year) +interface SheetRow { + row_label?: string + values?: Record + values_total?: Record + is_sum_row?: boolean } -export default function AssumptionsSlide({ lang, investorId, preferredScenarioId }: AssumptionsSlideProps) { +export default function AssumptionsSlide({ lang, investorId, preferredScenarioId, isWandeldarlehen }: AssumptionsSlideProps) { const i = t(lang) const de = lang === 'de' - // Load computed financial data for Base Case - const fm = useFinancialModel(investorId || null, preferredScenarioId) - const summary = fm.activeResults?.summary - const results = fm.activeResults?.results || [] - const lastResult = results.length > 0 ? results[results.length - 1] : null + // Load KPIs from fp_* tables (source of truth) + const [baseKPIs, setBaseKPIs] = useState>({}) - // Base case from compute engine - const baseCustomers = summary?.final_customers || 0 - const baseArr = summary?.final_arr || 0 - const baseEmployees = lastResult?.employees_count || 0 - const baseCash = lastResult?.cash_balance_eur || 0 - const baseBreakEven = breakEvenYear(summary?.break_even_month || null) + useEffect(() => { + async function load() { + try { + const scenarioParam = isWandeldarlehen ? '?scenarioId=c0000000-0000-0000-0000-000000000200' : '' + const [guvRes, liqRes, persRes, kundenRes] = await Promise.all([ + fetch(`/api/finanzplan/guv${scenarioParam}`, { cache: 'no-store' }), + fetch(`/api/finanzplan/liquiditaet${scenarioParam}`, { cache: 'no-store' }), + fetch(`/api/finanzplan/personalkosten${scenarioParam}`, { cache: 'no-store' }), + fetch(`/api/finanzplan/kunden${scenarioParam}`, { cache: 'no-store' }), + ]) + const [guv, liq, pers, kunden] = await Promise.all([guvRes.json(), liqRes.json(), persRes.json(), kundenRes.json()]) - // Bear/Bull derived from Base (scaling factors) + const findGuv = (label: string) => (guv.rows || []).find((r: SheetRow) => (r.row_label || '').includes(label)) + const findLiq = (label: string) => (liq.rows || []).find((r: SheetRow) => (r.row_label || '').includes(label)) + const kundenGesamt = (kunden.rows || []).find((r: SheetRow) => r.row_label === 'Bestandskunden gesamt') + + const arr = findGuv('Umsatzerlöse')?.values?.y2030 || 0 + const customers = kundenGesamt?.values?.m60 || 0 + const headcount = (pers.rows || []).filter((r: SheetRow) => ((r.values_total || r.values)?.m60 || 0) > 0).length + const cash = findLiq('LIQUIDIT')?.values?.m60 || findLiq('LIQUIDITAET')?.values?.m60 || 0 + const ebit = findGuv('EBIT')?.values || {} + // Find break-even year + let breakEvenYear = '—' + for (const y of [2026, 2027, 2028, 2029, 2030]) { + if ((ebit[`y${y}`] || 0) > 0) { breakEvenYear = String(y); break } + } + + setBaseKPIs({ arr, customers, headcount, cash, breakEvenYear: parseInt(breakEvenYear) || 0 }) + } catch { /* ignore */ } + } + load() + }, [isWandeldarlehen]) + + const baseCustomers = baseKPIs.customers || 0 + const baseArr = baseKPIs.arr || 0 + const baseEmployees = baseKPIs.headcount || 0 + const baseCash = baseKPIs.cash || 0 + const baseBreakEven = baseKPIs.breakEvenYear ? String(baseKPIs.breakEvenYear) : '—' + + // Bear/Bull scaled from Base const bearCustomers = Math.round(baseCustomers * 0.5) const bearArr = baseArr * 0.42 const bearEmployees = Math.round(baseEmployees * 0.7) - const bearCash = baseCash * 0.08 - const bearBreakEvenMonth = summary?.break_even_month ? Math.round(summary.break_even_month * 1.3) : null - const bearBreakEven = breakEvenYear(bearBreakEvenMonth) + const bearCash = Math.round(baseCash * 0.3) + const bearBreakEven = baseKPIs.breakEvenYear ? String(baseKPIs.breakEvenYear + 1) : '—' const bullCustomers = Math.round(baseCustomers * 1.7) const bullArr = baseArr * 1.8 - const bullEmployees = Math.round(baseEmployees * 1.4) - const bullCash = baseCash * 2.3 - const bullBreakEvenMonth = summary?.break_even_month ? Math.round(summary.break_even_month * 0.75) : null - const bullBreakEven = breakEvenYear(bullBreakEvenMonth) + const bullEmployees = Math.round(baseEmployees * 1.3) + const bullCash = Math.round(baseCash * 2.0) + const bullBreakEven = baseKPIs.breakEvenYear ? String(baseKPIs.breakEvenYear - 1) : '—' + + const baseAssumptions = isWandeldarlehen + ? (de ? [ + `Kundenwachstum: 8% monatlich (Slow), ab m32: 15%`, + 'Mix: 60% Starter, 25% Professional, 15% Enterprise', + `Personalaufbau: 3→${baseEmployees} Personen (lean)`, + 'Serverkosten: 50€/Kunde + 300€ Basis', + `Break-Even ${baseBreakEven}`, + ] : [ + `Customer growth: 8% monthly (slow), from m32: 15%`, + 'Mix: 60% Starter, 25% Professional, 15% Enterprise', + `Hiring: 3→${baseEmployees} people (lean)`, + 'Server costs: €50/customer + €300 base', + `Break-even ${baseBreakEven}`, + ]) + : (de ? [ + 'Kundenwachstum wie geplant (5→1.200)', + 'Mix: 60% Starter, 25% Professional, 15% Enterprise', + 'Personalaufbau 5→10→17→25→35', + 'Serverkosten 100€ pro Kunde + 2.000€ Basis', + 'Break-Even Mitte 2029', + ] : [ + 'Customer growth as planned (5→1,200)', + 'Mix: 60% Starter, 25% Professional, 15% Enterprise', + 'Hiring 5→10→17→25→35', + 'Server costs €100 per customer + €2,000 base', + 'Break-even mid 2029', + ]) - // 3 Cases abgeleitet aus dem Finanzplan (Base Case = aktuelle DB-Daten) const cases = [ { name: 'Bear Case', @@ -81,13 +137,11 @@ export default function AssumptionsSlide({ lang, investorId, preferredScenarioId 'Churn Rate 8% pro Monat (Startups)', 'Durchschnittspreis 20% niedriger', 'Personalaufbau verzögert um 6 Monate', - 'Serverkosten 150€ pro Kunde', ] : [ 'Customer growth 50% slower than base', 'Churn rate 8% per month (startups)', 'Average price 20% lower', 'Hiring delayed by 6 months', - 'Server costs €150 per customer', ], kpis: { kunden2030: `~${bearCustomers.toLocaleString('de-DE')}`, @@ -103,19 +157,7 @@ export default function AssumptionsSlide({ lang, investorId, preferredScenarioId color: 'text-indigo-400', bg: 'bg-indigo-500/10 border-indigo-500/20', desc: de ? 'Aktueller Finanzplan' : 'Current financial plan', - assumptions: de ? [ - 'Kundenwachstum wie geplant (14→1.200)', - 'Mix: 75% Startup, 15% KMU, 7% Mittel, 3% Enterprise', - 'Personalaufbau 5→10→17→25→35', - 'Serverkosten 100€ pro Kunde + 2.000€ Basis', - 'Break-Even Mitte 2029', - ] : [ - 'Customer growth as planned (14→1,200)', - 'Mix: 75% startup, 15% SME, 7% mid, 3% enterprise', - 'Hiring 5→10→17→25→35', - 'Server costs €100 per customer + €2,000 base', - 'Break-even mid 2029', - ], + assumptions: baseAssumptions, kpis: { kunden2030: `~${baseCustomers.toLocaleString('de-DE')}`, arr2030: fmtArr(baseArr, de), @@ -131,17 +173,15 @@ export default function AssumptionsSlide({ lang, investorId, preferredScenarioId bg: 'bg-emerald-500/10 border-emerald-500/20', desc: de ? 'Beschleunigtes Wachstum' : 'Accelerated growth', assumptions: de ? [ - 'Kundenwachstum 50% schneller (Regulierungsdruck)', - 'Enterprise-Anteil steigt auf 8%', + 'Kundenwachstum 70% schneller (Regulierungsdruck)', + 'Enterprise-Anteil steigt auf 20%', 'Durchschnittspreis 15% höher (Upselling)', - 'Channel-Partner ab Q1/2027', - 'EU-Expansion ab 2028', + 'Channel-Partner ab Q1/2028', ] : [ - 'Customer growth 50% faster (regulation pressure)', - 'Enterprise share rises to 8%', + 'Customer growth 70% faster (regulation pressure)', + 'Enterprise share rises to 20%', 'Average price 15% higher (upselling)', - 'Channel partners from Q1/2027', - 'EU expansion from 2028', + 'Channel partners from Q1/2028', ], kpis: { kunden2030: `~${bullCustomers.toLocaleString('de-DE')}`, @@ -162,49 +202,48 @@ export default function AssumptionsSlide({ lang, investorId, preferredScenarioId

{i.annex.assumptions.subtitle}

- {/* 3 Cases nebeneinander */}
{cases.map((c, idx) => { const Icon = c.icon return ( - -
- -

{c.name}

-
-

{c.desc}

+ + +
+ +

{c.name}

+
+

{c.desc}

- {/* Annahmen */} -
- {c.assumptions.map((a, i) => ( -

- - {a} -

- ))} -
+
+ {c.assumptions.map((a, i) => ( +

+ + {a} +

+ ))} +
- {/* KPIs */} -
- {[ - { label: de ? 'Kunden 2030' : 'Customers 2030', value: c.kpis.kunden2030 }, - { label: 'ARR 2030', value: c.kpis.arr2030 }, - { label: de ? 'Mitarbeiter 2030' : 'Employees 2030', value: c.kpis.ma2030 }, - { label: 'Break-Even', value: c.kpis.breakEven }, - { label: 'Cash 2030', value: c.kpis.cash2030 }, - ].map((kpi, i) => ( -
- {kpi.label} - {kpi.value} -
- ))} -
-
+
+ {[ + { label: de ? 'Kunden 2030' : 'Customers 2030', value: c.kpis.kunden2030 }, + { label: 'ARR 2030', value: c.kpis.arr2030 }, + { label: de ? 'Mitarbeiter' : 'Employees', value: c.kpis.ma2030 }, + { label: 'Break-Even', value: c.kpis.breakEven }, + { label: 'Cash 2030', value: c.kpis.cash2030 }, + ].map((kpi, i) => ( +
+ {kpi.label} + {kpi.value} +
+ ))} +
+
+ ) })}
- {/* Vergleichstabelle */} + {/* Comparison Table */}

@@ -221,11 +260,11 @@ export default function AssumptionsSlide({ lang, investorId, preferredScenarioId {[ - { label: de ? 'Kunden' : 'Customers', bear: `~${bearCustomers.toLocaleString('de-DE')}`, base: `~${baseCustomers.toLocaleString('de-DE')}`, bull: `~${bullCustomers.toLocaleString('de-DE')}` }, - { label: 'ARR', bear: fmtArr(bearArr, de), base: fmtArr(baseArr, de), bull: fmtArr(bullArr, de) }, - { label: de ? 'Mitarbeiter' : 'Employees', bear: String(bearEmployees), base: String(baseEmployees), bull: String(bullEmployees) }, - { label: 'Break-Even', bear: bearBreakEven, base: baseBreakEven, bull: bullBreakEven }, - { label: 'Cash', bear: fmtCash(bearCash, de), base: fmtCash(baseCash, de), bull: fmtCash(bullCash, de) }, + { label: de ? 'Kunden' : 'Customers', bear: cases[0].kpis.kunden2030, base: cases[1].kpis.kunden2030, bull: cases[2].kpis.kunden2030 }, + { label: 'ARR', bear: cases[0].kpis.arr2030, base: cases[1].kpis.arr2030, bull: cases[2].kpis.arr2030 }, + { label: de ? 'Mitarbeiter' : 'Employees', bear: cases[0].kpis.ma2030, base: cases[1].kpis.ma2030, bull: cases[2].kpis.ma2030 }, + { label: 'Break-Even', bear: cases[0].kpis.breakEven, base: cases[1].kpis.breakEven, bull: cases[2].kpis.breakEven }, + { label: 'Cash', bear: cases[0].kpis.cash2030, base: cases[1].kpis.cash2030, bull: cases[2].kpis.cash2030 }, ].map((row, idx) => ( {row.label}