diff --git a/pitch-deck/components/PitchDeck.tsx b/pitch-deck/components/PitchDeck.tsx index 1b18fb9..24cb361 100644 --- a/pitch-deck/components/PitchDeck.tsx +++ b/pitch-deck/components/PitchDeck.tsx @@ -169,7 +169,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, case 'market': return case 'business-model': - return + return case 'traction': return case 'competition': @@ -188,7 +188,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': @@ -204,7 +204,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, case 'annex-strategy': return case 'annex-finanzplan': - return + return case 'annex-glossary': return case 'legal-disclaimer': diff --git a/pitch-deck/components/slides/AssumptionsSlide.tsx b/pitch-deck/components/slides/AssumptionsSlide.tsx index 3ebce50..f3f2372 100644 --- a/pitch-deck/components/slides/AssumptionsSlide.tsx +++ b/pitch-deck/components/slides/AssumptionsSlide.tsx @@ -6,15 +6,67 @@ 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 } -export default function AssumptionsSlide({ lang }: AssumptionsSlideProps) { +function fmtArr(v: number, de: boolean): string { + if (v >= 1_000_000) { + const m = (v / 1_000_000).toFixed(1) + return de ? `~${m.replace('.', ',')} Mio. EUR` : `~EUR ${m}M` + } + return de ? `~${Math.round(v / 1000)}k EUR` : `~EUR ${Math.round(v / 1000)}k` +} + +function fmtCash(v: number, de: boolean): string { + if (Math.abs(v) >= 1_000_000) { + const m = (v / 1_000_000).toFixed(1) + return de ? `~${m.replace('.', ',')} Mio. EUR` : `~EUR ${m}M` + } + 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) +} + +export default function AssumptionsSlide({ lang, investorId }: AssumptionsSlideProps) { const i = t(lang) const de = lang === 'de' + // Load computed financial data for Base Case + const fm = useFinancialModel(investorId || null) + const summary = fm.activeResults?.summary + const results = fm.activeResults?.results || [] + const lastResult = results.length > 0 ? results[results.length - 1] : null + + // 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) + + // Bear/Bull derived from Base (scaling factors) + 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 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) + // 3 Cases abgeleitet aus dem Finanzplan (Base Case = aktuelle DB-Daten) const cases = [ { @@ -37,11 +89,11 @@ export default function AssumptionsSlide({ lang }: AssumptionsSlideProps) { 'Server costs €150 per customer', ], kpis: { - kunden2030: '~600', - arr2030: de ? '~4,2 Mio. EUR' : '~EUR 4.2M', - ma2030: '25', - breakEven: '2030', - cash2030: de ? '~0,5 Mio. EUR' : '~EUR 0.5M', + kunden2030: `~${bearCustomers.toLocaleString('de-DE')}`, + arr2030: fmtArr(bearArr, de), + ma2030: String(bearEmployees), + breakEven: bearBreakEven, + cash2030: fmtCash(bearCash, de), }, }, { @@ -64,11 +116,11 @@ export default function AssumptionsSlide({ lang }: AssumptionsSlideProps) { 'Break-even mid 2029', ], kpis: { - kunden2030: '~1.200', - arr2030: de ? '~10 Mio. EUR' : '~EUR 10M', - ma2030: '35', - breakEven: '2029', - cash2030: de ? '~6,4 Mio. EUR' : '~EUR 6.4M', + kunden2030: `~${baseCustomers.toLocaleString('de-DE')}`, + arr2030: fmtArr(baseArr, de), + ma2030: String(baseEmployees), + breakEven: baseBreakEven, + cash2030: fmtCash(baseCash, de), }, }, { @@ -91,11 +143,11 @@ export default function AssumptionsSlide({ lang }: AssumptionsSlideProps) { 'EU expansion from 2028', ], kpis: { - kunden2030: '~2.000', - arr2030: de ? '~18 Mio. EUR' : '~EUR 18M', - ma2030: '50', - breakEven: '2028', - cash2030: de ? '~15 Mio. EUR' : '~EUR 15M', + kunden2030: `~${bullCustomers.toLocaleString('de-DE')}`, + arr2030: fmtArr(bullArr, de), + ma2030: String(bullEmployees), + breakEven: bullBreakEven, + cash2030: fmtCash(bullCash, de), }, }, ] @@ -168,11 +220,11 @@ export default function AssumptionsSlide({ lang }: AssumptionsSlideProps) { {[ - { label: de ? 'Kunden' : 'Customers', bear: '~600', base: '~1.200', bull: '~2.000' }, - { label: 'ARR', bear: de ? '~4,2 Mio.' : '~4.2M', base: de ? '~10 Mio.' : '~10M', bull: de ? '~18 Mio.' : '~18M' }, - { label: de ? 'Mitarbeiter' : 'Employees', bear: '25', base: '35', bull: '50' }, - { label: 'Break-Even', bear: '2030', base: '2029', bull: '2028' }, - { label: 'Cash', bear: de ? '~0,5 Mio.' : '~0.5M', base: de ? '~6,4 Mio.' : '~6.4M', bull: de ? '~15 Mio.' : '~15M' }, + { 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) }, ].map((row, idx) => ( {row.label} diff --git a/pitch-deck/components/slides/BusinessModelSlide.tsx b/pitch-deck/components/slides/BusinessModelSlide.tsx index 675e26f..304c1e8 100644 --- a/pitch-deck/components/slides/BusinessModelSlide.tsx +++ b/pitch-deck/components/slides/BusinessModelSlide.tsx @@ -2,6 +2,7 @@ import { Language } from '@/lib/types' import { t } from '@/lib/i18n' +import { useFinancialModel } from '@/lib/hooks/useFinancialModel' import GradientText from '../ui/GradientText' import FadeInView from '../ui/FadeInView' import GlassCard from '../ui/GlassCard' @@ -10,11 +11,19 @@ import { ArrowRight, TrendingUp, Target, Repeat, DollarSign, Users, BarChart3 } interface BusinessModelSlideProps { lang: Language products?: unknown[] + investorId?: string | null } -export default function BusinessModelSlide({ lang }: BusinessModelSlideProps) { +export default function BusinessModelSlide({ lang, investorId }: BusinessModelSlideProps) { const i = t(lang) const de = lang === 'de' + const fm = useFinancialModel(investorId || null) + 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 acv = finalCustomers > 0 ? Math.round(finalArr / finalCustomers) : 0 const tiers = [ { @@ -145,8 +154,8 @@ export default function BusinessModelSlide({ lang }: BusinessModelSlideProps) {

{de - ? '1.200 Kunden × 8.400 EUR ACV = ~10 Mio. EUR ARR (2030)' - : '1,200 customers × EUR 8,400 ACV = ~EUR 10M ARR (2030)'} + ? `${finalCustomers.toLocaleString('de-DE')} Kunden × ${acv.toLocaleString('de-DE')} EUR ACV = ~${(finalArr / 1_000_000).toFixed(1).replace('.', ',')} Mio. EUR ARR (2030)` + : `${finalCustomers.toLocaleString('en-US')} customers × EUR ${acv.toLocaleString('en-US')} ACV = ~EUR ${(finalArr / 1_000_000).toFixed(1)}M ARR (2030)`}

diff --git a/pitch-deck/components/slides/FinanzplanSlide.tsx b/pitch-deck/components/slides/FinanzplanSlide.tsx index 08d44d2..286b984 100644 --- a/pitch-deck/components/slides/FinanzplanSlide.tsx +++ b/pitch-deck/components/slides/FinanzplanSlide.tsx @@ -1,8 +1,10 @@ 'use client' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, 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' @@ -11,6 +13,7 @@ import { RefreshCw, Download, ChevronLeft, ChevronRight, BarChart3, Target } fro interface FinanzplanSlideProps { lang: Language + investorId?: string | null } interface SheetMeta { @@ -57,7 +60,7 @@ function formatCell(v: number | undefined): string { return Math.round(v).toLocaleString('de-DE', { maximumFractionDigits: 0 }) } -export default function FinanzplanSlide({ lang }: FinanzplanSlideProps) { +export default function FinanzplanSlide({ lang, investorId }: FinanzplanSlideProps) { const [sheets, setSheets] = useState([]) const [activeSheet, setActiveSheet] = useState('personalkosten') const [rows, setRows] = useState([]) @@ -66,6 +69,13 @@ export default function FinanzplanSlide({ lang }: FinanzplanSlideProps) { 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) + const annualKPIs = useMemo( + () => computeAnnualKPIs(fm.activeResults?.results || []), + [fm.activeResults], + ) + // Load sheet list useEffect(() => { fetch('/api/finanzplan') @@ -191,24 +201,25 @@ export default function FinanzplanSlide({ lang }: FinanzplanSlideProps) { {(() => { - // Compute KPIs from loaded data — we need liquidität and umsatz data - // These are approximate since we don't have all sheets loaded simultaneously + if (annualKPIs.length === 0) return ( + {de ? 'Finanzmodell wird geladen...' : 'Loading financial model...'} + ) const kpiRows = [ - { label: 'MRR (Dez)', values: [6100, 84450, 267950, 517650, 834750], unit: '€', bold: true }, - { label: 'ARR', values: [73200, 1013400, 3215400, 6211800, 10017000], unit: '€', bold: true }, - { label: de ? 'Kunden (Dez)' : 'Customers (Dec)', values: [14, 117, 370, 726, 1200], unit: '', bold: false }, - { label: 'ARPU (MRR/Kunden)', values: [436, 722, 724, 713, 696], unit: '€', bold: false }, - { label: de ? 'Mitarbeiter' : 'Employees', values: [5, 10, 17, 25, 35], unit: '', bold: false }, - { label: de ? 'Umsatz/Mitarbeiter' : 'Revenue/Employee', values: [14640, 101340, 189141, 248472, 286200], unit: '€', bold: false }, - { label: de ? 'Personalkosten' : 'Personnel Costs', values: [58768, 740968, 1353764, 2154301, 3129479], unit: '€', bold: false }, - { label: 'EBIT', values: [-95099, -566293, -4019, 1315689, 3144137], unit: '€', bold: true }, - { label: de ? 'EBIT-Marge' : 'EBIT Margin', values: [-130, -56, -1, 21, 31], unit: '%', bold: false }, - { label: de ? 'Steuern' : 'Taxes', values: [0, 0, 0, 182565, 882717], unit: '€', bold: false }, - { label: de ? 'Jahresüberschuss' : 'Net Income', values: [-95099, -566293, -4019, 1133124, 2261420], unit: '€', bold: true }, - { label: de ? 'Serverkosten/Kunde' : 'Server Cost/Customer', values: [100, 100, 100, 100, 100], unit: '€', bold: false }, - { label: de ? 'Bruttomarge' : 'Gross Margin', values: [100, 100, 92, 90, 88], unit: '%', bold: false }, - { label: 'Burn Rate (Dez)', values: [44734, 28364, 0, 0, 0], unit: '€/Mo', bold: false }, - { label: de ? 'Runway (Monate)' : 'Runway (months)', values: [19, 4, '∞', '∞', '∞'], unit: '', bold: false }, + { 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 }, ] return kpiRows.map((row, idx) => ( @@ -241,13 +252,11 @@ export default function FinanzplanSlide({ lang }: FinanzplanSlideProps) {

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

- {[ - { year: '2026', mrr: 6100, cust: 14, max_mrr: 834750, max_cust: 1200 }, - { year: '2027', mrr: 84450, cust: 117, max_mrr: 834750, max_cust: 1200 }, - { year: '2028', mrr: 267950, cust: 370, max_mrr: 834750, max_cust: 1200 }, - { year: '2029', mrr: 517650, cust: 726, max_mrr: 834750, max_cust: 1200 }, - { year: '2030', mrr: 834750, cust: 1200, max_mrr: 834750, max_cust: 1200 }, - ].map((d, idx) => ( + {(() => { + 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 })) + })().map((d, idx) => (
{/* MRR bar */} @@ -276,56 +285,50 @@ export default function FinanzplanSlide({ lang }: FinanzplanSlideProps) {

EBIT

- {[ - { year: '2026', val: -95099 }, - { year: '2027', val: -566293 }, - { year: '2028', val: -4019 }, - { year: '2029', val: 1315689 }, - { year: '2030', val: 3144137 }, - ].map((d, idx) => { - const maxAbs = 3144137 - const h = Math.abs(d.val) / maxAbs * 100 - return ( -
-
- {d.val >= 0 ? ( -
-
{Math.round(d.val/1000)}k
-
- ) : ( -
-
-
{Math.round(d.val/1000)}k
+ {(() => { + const maxAbs = Math.max(...annualKPIs.map(k => Math.abs(k.ebit)), 1) + return annualKPIs.map((k, idx) => { + const h = Math.abs(k.ebit) / maxAbs * 100 + return ( +
+
+ {k.ebit >= 0 ? ( +
+
{Math.round(k.ebit/1000)}k
-
- )} + ) : ( +
+
+
{Math.round(k.ebit/1000)}k
+
+
+ )} +
+ {k.year}
- {d.year} -
- ) - })} + ) + }) + })()}

{de ? 'Personalaufbau' : 'Headcount'}

- {[ - { year: '2026', val: 5 }, - { year: '2027', val: 10 }, - { year: '2028', val: 17 }, - { year: '2029', val: 25 }, - { year: '2030', val: 35 }, - ].map((d, idx) => ( -
-
-
-
{d.val}
+ {(() => { + const maxEmp = Math.max(...annualKPIs.map(k => k.employees), 1) + return annualKPIs.map(k => ({ year: String(k.year), val: k.employees })) + .map((d, idx) => ( +
+
+
+
{d.val}
+
+
+ {d.year}
-
- {d.year} -
- ))} + )) + })()}
diff --git a/pitch-deck/lib/finanzplan/annual-kpis.ts b/pitch-deck/lib/finanzplan/annual-kpis.ts new file mode 100644 index 0000000..1746285 --- /dev/null +++ b/pitch-deck/lib/finanzplan/annual-kpis.ts @@ -0,0 +1,89 @@ +import { FMResult } from '../types' + +export interface AnnualKPI { + year: number + mrr: number + arr: number + customers: number + arpu: number + employees: number + revenuePerEmployee: number + personnelCosts: number + totalRevenue: number + totalCosts: number + ebit: number + ebitMargin: number + taxes: number + netIncome: number + serverCostPerCustomer: number + grossMargin: number + burnRate: number + runway: number | null + cashBalance: number +} + +const TAX_RATE = 0.30 // ~30% Körperschaftsteuer + Gewerbesteuer + Soli + +/** + * Aggregates 60 monthly FMResult entries into 5 annual KPI rows (2026–2030). + * All values are derived — nothing is hardcoded. + */ +export function computeAnnualKPIs(results: FMResult[]): AnnualKPI[] { + if (!results || results.length === 0) return [] + + const years = [2026, 2027, 2028, 2029, 2030] + + return years.map(year => { + const yearResults = results.filter(r => r.year === year) + if (yearResults.length === 0) { + return emptyKPI(year) + } + + const dec = yearResults[yearResults.length - 1] // December snapshot + const totalRevenue = yearResults.reduce((s, r) => s + r.revenue_eur, 0) + const personnelCosts = yearResults.reduce((s, r) => s + r.personnel_eur, 0) + const totalCogs = yearResults.reduce((s, r) => s + r.cogs_eur, 0) + const totalInfra = yearResults.reduce((s, r) => s + r.infra_eur, 0) + const totalMarketing = yearResults.reduce((s, r) => s + r.marketing_eur, 0) + const totalCosts = yearResults.reduce((s, r) => s + r.total_costs_eur, 0) + + const ebit = totalRevenue - totalCosts + const ebitMargin = totalRevenue > 0 ? (ebit / totalRevenue) * 100 : 0 + const taxes = ebit > 0 ? Math.round(ebit * TAX_RATE) : 0 + const netIncome = ebit - taxes + const serverCost = dec.total_customers > 0 + ? Math.round((totalInfra / 12) / dec.total_customers) + : 0 + + return { + year, + mrr: Math.round(dec.mrr_eur), + arr: Math.round(dec.arr_eur), + customers: Math.round(dec.total_customers), + arpu: dec.total_customers > 0 ? Math.round(dec.mrr_eur / dec.total_customers) : 0, + employees: Math.round(dec.employees_count), + revenuePerEmployee: dec.employees_count > 0 ? Math.round(totalRevenue / dec.employees_count) : 0, + personnelCosts: Math.round(personnelCosts), + totalRevenue: Math.round(totalRevenue), + totalCosts: Math.round(totalCosts), + ebit: Math.round(ebit), + ebitMargin: Math.round(ebitMargin), + taxes, + netIncome: Math.round(netIncome), + serverCostPerCustomer: serverCost, + grossMargin: Math.round(dec.gross_margin_pct), + burnRate: Math.round(dec.burn_rate_eur), + runway: dec.runway_months > 999 ? null : Math.round(dec.runway_months), + cashBalance: Math.round(dec.cash_balance_eur), + } + }) +} + +function emptyKPI(year: number): AnnualKPI { + return { + year, mrr: 0, arr: 0, customers: 0, arpu: 0, employees: 0, + revenuePerEmployee: 0, personnelCosts: 0, totalRevenue: 0, totalCosts: 0, + ebit: 0, ebitMargin: 0, taxes: 0, netIncome: 0, + serverCostPerCustomer: 0, grossMargin: 0, burnRate: 0, runway: null, cashBalance: 0, + } +}