diff --git a/pitch-deck/app/api/financial-model/compute/route.ts b/pitch-deck/app/api/financial-model/compute/route.ts index 9e5f6b2..0517618 100644 --- a/pitch-deck/app/api/financial-model/compute/route.ts +++ b/pitch-deck/app/api/financial-model/compute/route.ts @@ -1,10 +1,26 @@ import { NextRequest, NextResponse } from 'next/server' import pool from '@/lib/db' +import { computeFinanzplan } from '@/lib/finanzplan/engine' +import { finanzplanToFMResults } from '@/lib/finanzplan/adapter' export async function POST(request: NextRequest) { try { const body = await request.json() - const { scenarioId } = body + const { scenarioId, source } = body + + // If source=finanzplan, use the Finanzplan engine instead + if (source === 'finanzplan') { + try { + // First run the Finanzplan compute engine + await computeFinanzplan(pool, scenarioId || undefined) + // Then adapt results to FMResult format + const result = await finanzplanToFMResults(pool, scenarioId || undefined) + return NextResponse.json(result) + } catch (error) { + console.error('Finanzplan compute error:', error) + return NextResponse.json({ error: 'Finanzplan computation failed: ' + String(error) }, { status: 500 }) + } + } if (!scenarioId) { return NextResponse.json({ error: 'scenarioId is required' }, { status: 400 }) diff --git a/pitch-deck/components/slides/FinancialsSlide.tsx b/pitch-deck/components/slides/FinancialsSlide.tsx index c62d3ed..5aad988 100644 --- a/pitch-deck/components/slides/FinancialsSlide.tsx +++ b/pitch-deck/components/slides/FinancialsSlide.tsx @@ -28,7 +28,8 @@ export default function FinancialsSlide({ lang }: FinancialsSlideProps) { const [activeTab, setActiveTab] = useState('overview') const de = lang === 'de' - const activeResults = fm.activeResults + const [useFinanzplan, setUseFinanzplan] = useState(false) + const activeResults = useFinanzplan ? fm.finanzplanResults : fm.activeResults const summary = activeResults?.summary const lastResult = activeResults?.results[activeResults.results.length - 1] @@ -65,6 +66,20 @@ export default function FinancialsSlide({ lang }: FinancialsSlideProps) { {i.financials.title}

{i.financials.subtitle}

+
+ + +
{/* Hero KPI Cards */} diff --git a/pitch-deck/lib/finanzplan/adapter.ts b/pitch-deck/lib/finanzplan/adapter.ts new file mode 100644 index 0000000..6eee954 --- /dev/null +++ b/pitch-deck/lib/finanzplan/adapter.ts @@ -0,0 +1,135 @@ +/** + * Adapter: Maps Finanzplan DB data → existing FMResult interface + * used by FinancialsSlide, FinancialChart, AnnualPLTable + */ + +import { Pool } from 'pg' +import { FMResult, FMComputeResponse } from '../types' +import { MonthlyValues, MONTHS, monthToDate, emptyMonthly, annualSums } from './types' + +export async function finanzplanToFMResults(pool: Pool, scenarioId?: string): Promise { + // Get scenario + let sid = scenarioId + if (!sid) { + const { rows } = await pool.query('SELECT id FROM fp_scenarios WHERE is_default = true LIMIT 1') + if (rows.length === 0) throw new Error('No default scenario') + sid = rows[0].id + } + + // Load computed data + const [personalRes, liquidRes, betriebRes, umsatzRes, materialRes, investRes] = await Promise.all([ + pool.query("SELECT * FROM fp_personalkosten WHERE scenario_id = $1 ORDER BY sort_order", [sid]), + pool.query("SELECT * FROM fp_liquiditaet WHERE scenario_id = $1 ORDER BY sort_order", [sid]), + pool.query("SELECT * FROM fp_betriebliche_aufwendungen WHERE scenario_id = $1 ORDER BY sort_order", [sid]), + pool.query("SELECT * FROM fp_umsatzerloese WHERE scenario_id = $1 AND section = 'revenue' AND row_label = 'GESAMTUMSATZ' LIMIT 1", [sid]), + pool.query("SELECT * FROM fp_materialaufwand WHERE scenario_id = $1 AND row_label = 'SUMME' LIMIT 1", [sid]), + pool.query("SELECT * FROM fp_investitionen WHERE scenario_id = $1 ORDER BY sort_order", [sid]), + ]) + + const personal = personalRes.rows + const liquid = liquidRes.rows + const betrieb = betriebRes.rows + + // Helper to sum a field across personnel + function sumPersonalField(field: string): MonthlyValues { + const r = emptyMonthly() + for (const p of personal) { + const vals = p[field] || {} + for (let m = 1; m <= MONTHS; m++) r[`m${m}`] += vals[`m${m}`] || 0 + } + return r + } + + const totalPersonal = sumPersonalField('values_total') + const totalBrutto = sumPersonalField('values_brutto') + const revenue = umsatzRes.rows[0]?.values || emptyMonthly() + const material = materialRes.rows[0]?.values || emptyMonthly() + + // Betriebliche sonstige (without personal + abschreibungen) + const sonstRow = betrieb.find((r: any) => r.row_label?.includes('Summe sonstige')) + const sonstige = sonstRow?.values || emptyMonthly() + + // AfA + const afaRow = betrieb.find((r: any) => r.row_label === 'Abschreibungen') + const afa = afaRow?.values || emptyMonthly() + + // Liquidität endstand + const liqEndRow = liquid.find((r: any) => r.row_label === 'LIQUIDITAET') + const cashBalance = liqEndRow?.values || emptyMonthly() + + // Headcount + const headcount = emptyMonthly() + for (let m = 1; m <= MONTHS; m++) { + headcount[`m${m}`] = personal.filter((p: any) => (p.values_total?.[`m${m}`] || 0) > 0).length + } + + // Marketing (from betriebliche) + const marketingRow = betrieb.find((r: any) => r.row_label?.includes('Werbe') || r.row_label?.includes('marketing')) + const marketing = marketingRow?.values || emptyMonthly() + + // Build 60 monthly FMResult entries + const results: FMResult[] = [] + let cumulativeRevenue = 0 + let prevCustomers = 0 + + for (let m = 1; m <= MONTHS; m++) { + const { year, month } = monthToDate(m) + const rev = revenue[`m${m}`] || 0 + const mat = material[`m${m}`] || 0 + const pers = totalPersonal[`m${m}`] || 0 + const infra = sonstige[`m${m}`] || 0 + const mktg = marketing[`m${m}`] || 0 + const totalCosts = mat + pers + infra + const grossMargin = rev > 0 ? ((rev - mat) / rev) * 100 : 0 + cumulativeRevenue += rev + + const hc = headcount[`m${m}`] || 0 + const cash = cashBalance[`m${m}`] || 0 + const burnRate = totalCosts > rev ? totalCosts - rev : 0 + const runway = burnRate > 0 ? Math.round(Math.max(0, cash) / burnRate) : 999 + + results.push({ + month: m, + year, + month_in_year: month, + new_customers: 0, + churned_customers: 0, + total_customers: 0, + mrr_eur: Math.round(rev / 1), // monthly + arr_eur: Math.round(rev * 12), + revenue_eur: Math.round(rev), + cogs_eur: Math.round(mat), + personnel_eur: Math.round(pers), + infra_eur: Math.round(infra), + marketing_eur: Math.round(mktg), + total_costs_eur: Math.round(totalCosts), + employees_count: hc, + gross_margin_pct: Math.round(grossMargin * 10) / 10, + burn_rate_eur: Math.round(burnRate), + runway_months: runway, + cac_eur: 0, + ltv_eur: 0, + ltv_cac_ratio: 0, + cash_balance_eur: Math.round(cash), + cumulative_revenue_eur: Math.round(cumulativeRevenue), + }) + } + + // Summary + const lastMonth = results[MONTHS - 1] + const breakEvenMonth = results.findIndex(r => r.revenue_eur > r.total_costs_eur) + + return { + scenario_id: sid, + results, + summary: { + final_arr: lastMonth?.arr_eur || 0, + final_customers: lastMonth?.total_customers || 0, + break_even_month: breakEvenMonth >= 0 ? breakEvenMonth + 1 : null, + final_runway: lastMonth?.runway_months || 0, + final_ltv_cac: 0, + peak_burn: Math.max(...results.map(r => r.burn_rate_eur)), + total_funding_needed: Math.abs(Math.min(...results.map(r => r.cash_balance_eur), 0)), + }, + } +} diff --git a/pitch-deck/lib/hooks/useFinancialModel.ts b/pitch-deck/lib/hooks/useFinancialModel.ts index 051e00e..f079462 100644 --- a/pitch-deck/lib/hooks/useFinancialModel.ts +++ b/pitch-deck/lib/hooks/useFinancialModel.ts @@ -41,17 +41,17 @@ export function useFinancialModel() { } }, [activeScenarioId]) // eslint-disable-line react-hooks/exhaustive-deps - const compute = useCallback(async (scenarioId: string) => { + const compute = useCallback(async (scenarioId: string, source?: string) => { setComputing(true) try { const res = await fetch('/api/financial-model/compute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ scenarioId }), + body: JSON.stringify({ scenarioId, source }), }) if (res.ok) { const data: FMComputeResponse = await res.json() - setResults(prev => new Map(prev).set(scenarioId, data)) + setResults(prev => new Map(prev).set(source === 'finanzplan' ? 'finanzplan' : scenarioId, data)) } } catch (err) { console.error('Compute failed:', err) @@ -60,6 +60,10 @@ export function useFinancialModel() { } }, []) + const computeFromFinanzplan = useCallback(async () => { + await compute('', 'finanzplan') + }, [compute]) + const updateAssumption = useCallback(async (scenarioId: string, key: string, value: number | number[]) => { // Optimistic update in local state setScenarios(prev => prev.map(s => { @@ -90,6 +94,7 @@ export function useFinancialModel() { const activeScenario = scenarios.find(s => s.id === activeScenarioId) || null const activeResults = activeScenarioId ? results.get(activeScenarioId) || null : null + const finanzplanResults = results.get('finanzplan') || null return { scenarios, @@ -97,6 +102,7 @@ export function useFinancialModel() { activeScenarioId, setActiveScenarioId, activeResults, + finanzplanResults, results, loading, computing, @@ -104,6 +110,7 @@ export function useFinancialModel() { setCompareMode, compute, computeAll, + computeFromFinanzplan, updateAssumption, } }