fix(pitch-deck): replace all hardcoded financial numbers with computed values
Some checks failed
Build pitch-deck / build-push-deploy (push) Failing after 40s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 37s
CI / test-python-voice (push) Successful in 36s
CI / test-bqas (push) Successful in 33s

All financial data now flows from the same compute engine (useFinancialModel).
No more hardcoded numbers in any slide — all values are derived from the
finanzplan database, ensuring consistency across all pitch deck versions.

- FinanzplanSlide: KPI table + charts now use computeAnnualKPIs() from FMResult[]
- BusinessModelSlide: bottom-up calc (customers × ACV = ARR) from compute engine
- AssumptionsSlide: Base case from compute, Bear/Bull scaled from Base
- New helper: lib/finanzplan/annual-kpis.ts for 60-month → 5-year aggregation
- PitchDeck: passes investorId to all financial slides for version-aware data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-16 08:48:37 +02:00
parent aed428312f
commit 06f868abeb
5 changed files with 247 additions and 94 deletions

View File

@@ -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<SheetMeta[]>([])
const [activeSheet, setActiveSheet] = useState<string>('personalkosten')
const [rows, setRows] = useState<SheetRow[]>([])
@@ -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) {
</thead>
<tbody>
{(() => {
// 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 (
<tr><td colSpan={6} className="text-center py-4 text-white/30">{de ? 'Finanzmodell wird geladen...' : 'Loading financial model...'}</td></tr>
)
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) => (
<tr key={idx} className={`border-b border-white/[0.03] ${row.bold ? 'bg-white/[0.03]' : ''}`}>
@@ -241,13 +252,11 @@ export default function FinanzplanSlide({ lang }: FinanzplanSlideProps) {
<GlassCard hover={false} className="p-4">
<h3 className="text-xs font-bold text-indigo-400 uppercase tracking-wider mb-3">{de ? 'MRR & Kundenentwicklung' : 'MRR & Customer Growth'}</h3>
<div className="grid grid-cols-5 gap-1 items-end h-48">
{[
{ 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) => (
<div key={idx} className="flex flex-col items-center gap-1">
<div className="flex items-end gap-1 w-full justify-center" style={{ height: '160px' }}>
{/* MRR bar */}
@@ -276,56 +285,50 @@ export default function FinanzplanSlide({ lang }: FinanzplanSlideProps) {
<GlassCard hover={false} className="p-4">
<h3 className="text-xs font-bold text-purple-400 uppercase tracking-wider mb-3">EBIT</h3>
<div className="grid grid-cols-5 gap-1 items-end h-36">
{[
{ 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 (
<div key={idx} className="flex flex-col items-center">
<div className="w-10 flex flex-col justify-end" style={{ height: '110px' }}>
{d.val >= 0 ? (
<div className="bg-emerald-500/60 rounded-t w-full" style={{ height: `${h}px` }}>
<div className="text-[7px] text-emerald-300 text-center -mt-3 whitespace-nowrap">{Math.round(d.val/1000)}k</div>
</div>
) : (
<div className="flex flex-col justify-end h-full">
<div className="bg-red-500/60 rounded-b w-full" style={{ height: `${h}px` }}>
<div className="text-[7px] text-red-300 text-center mt-1 whitespace-nowrap">{Math.round(d.val/1000)}k</div>
{(() => {
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 (
<div key={idx} className="flex flex-col items-center">
<div className="w-10 flex flex-col justify-end" style={{ height: '110px' }}>
{k.ebit >= 0 ? (
<div className="bg-emerald-500/60 rounded-t w-full" style={{ height: `${h}px` }}>
<div className="text-[7px] text-emerald-300 text-center -mt-3 whitespace-nowrap">{Math.round(k.ebit/1000)}k</div>
</div>
</div>
)}
) : (
<div className="flex flex-col justify-end h-full">
<div className="bg-red-500/60 rounded-b w-full" style={{ height: `${h}px` }}>
<div className="text-[7px] text-red-300 text-center mt-1 whitespace-nowrap">{Math.round(k.ebit/1000)}k</div>
</div>
</div>
)}
</div>
<span className="text-[10px] text-white/40 mt-1">{k.year}</span>
</div>
<span className="text-[10px] text-white/40 mt-1">{d.year}</span>
</div>
)
})}
)
})
})()}
</div>
</GlassCard>
<GlassCard hover={false} className="p-4">
<h3 className="text-xs font-bold text-amber-400 uppercase tracking-wider mb-3">{de ? 'Personalaufbau' : 'Headcount'}</h3>
<div className="grid grid-cols-5 gap-1 items-end h-36">
{[
{ year: '2026', val: 5 },
{ year: '2027', val: 10 },
{ year: '2028', val: 17 },
{ year: '2029', val: 25 },
{ year: '2030', val: 35 },
].map((d, idx) => (
<div key={idx} className="flex flex-col items-center">
<div className="w-10 flex flex-col justify-end" style={{ height: '110px' }}>
<div className="bg-amber-500/60 rounded-t w-full" style={{ height: `${(d.val / 35) * 100}px` }}>
<div className="text-[8px] text-amber-300 text-center -mt-3 font-bold">{d.val}</div>
{(() => {
const maxEmp = Math.max(...annualKPIs.map(k => k.employees), 1)
return annualKPIs.map(k => ({ year: String(k.year), val: k.employees }))
.map((d, idx) => (
<div key={idx} className="flex flex-col items-center">
<div className="w-10 flex flex-col justify-end" style={{ height: '110px' }}>
<div className="bg-amber-500/60 rounded-t w-full" style={{ height: `${(d.val / maxEmp) * 100}px` }}>
<div className="text-[8px] text-amber-300 text-center -mt-3 font-bold">{d.val}</div>
</div>
</div>
<span className="text-[10px] text-white/40 mt-1">{d.year}</span>
</div>
</div>
<span className="text-[10px] text-white/40 mt-1">{d.year}</span>
</div>
))}
))
})()}
</div>
</GlassCard>
</div>