'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 nrr: number // Net Revenue Retention % paybackMonths: number // CAC Payback Period in months } 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 // NRR: compare Dec revenue of this year vs Dec revenue of prior year // NRR = (MRR_Dec_thisYear / MRR_Dec_lastYear) * 100 const prevMk = `m${(y - 2026) * 12}` // December of prior year (m0 for 2026 = no prior) const prevRevenue = y > 2026 ? (findGuv('Umsatzerlöse')?.values?.[`y${y - 1}`] || 0) : 0 const nrr = prevRevenue > 0 ? Math.round((revenue / prevRevenue) * 100) : 0 // Payback: Marketing costs / monthly gross profit per new customer // Simplified: annual marketing spend / (new customers * monthly ARPU) const sonstAufw = findGuv('Sonst. betriebl. Aufwend')?.values?.[yk] || 0 // Marketing ~ 10% of sonstige (rough estimate from our formula) const marketingSpend = Math.round(revenue * 0.10) const prevCustomers = y > 2026 ? (kundenGesamt?.values?.[`m${(y - 2026) * 12}`] || 0) : 0 const newCustomers = Math.max(customers - prevCustomers, 1) const cac = newCustomers > 0 ? Math.round(marketingSpend / newCustomers) : 0 const monthlyGrossProfit = arpu > 0 ? arpu * (grossMargin / 100) : 0 const paybackMonths = monthlyGrossProfit > 0 ? Math.round(cac / monthlyGrossProfit) : 0 result[yk] = { revenue, ebit, personal, netIncome, steuern, liquiditaet, customers, headcount, mrr, arr, arpu, revPerEmp, ebitMargin, grossMargin, nrr, paybackMonths } } setKpis(result) } catch { /* ignore */ } setLoading(false) } load() }, [isWandeldarlehen]) // Use of Funds: compute spending breakdown m8-m24 (funding period) const [useOfFunds, setUseOfFunds] = useState>([]) useEffect(() => { async function loadUoF() { try { const param = isWandeldarlehen ? '?scenarioId=c0000000-0000-0000-0000-000000000200' : '' const [persRes, betriebRes, investRes] = await Promise.all([ fetch(`/api/finanzplan/personalkosten${param}`, { cache: 'no-store' }), fetch(`/api/finanzplan/betriebliche${param}`, { cache: 'no-store' }), fetch(`/api/finanzplan/investitionen${param}`, { cache: 'no-store' }), ]) const [pers, betrieb, invest] = await Promise.all([persRes.json(), betriebRes.json(), investRes.json()]) // Sum spending m8-m24 const sumRange = (rows: SheetRow[], field: string, m1: number, m2: number) => { let total = 0 for (const row of (rows || [])) { const vals = (row as Record)[field] as Record || row.values || {} for (let m = m1; m <= m2; m++) total += vals[`m${m}`] || 0 } return total } const personalTotal = sumRange(pers.rows || [], 'values_total', 8, 24) // Marketing rows from betriebliche const betriebRows: SheetRow[] = betrieb.rows || [] const marketingRows = betriebRows.filter((r: SheetRow) => (r as Record).category === 'marketing' && !(r as Record).is_sum_row ) const marketingTotal = sumRange(marketingRows, 'values', 8, 24) // All other betriebliche (excl. personal, marketing, abschreibungen, sum rows) const otherBetrieb = betriebRows.filter((r: SheetRow) => { const any = r as Record return any.category !== 'marketing' && any.category !== 'personal' && any.category !== 'abschreibungen' && !any.is_sum_row && !(r.row_label || '').includes('Summe') && !(r.row_label || '').includes('SUMME') }) const operationsTotal = sumRange(otherBetrieb, 'values', 8, 24) // Investitionen const investTotal = sumRange(invest.rows || [], 'values_invest', 8, 24) const grandTotal = personalTotal + marketingTotal + operationsTotal + investTotal if (grandTotal <= 0) return const pctPersonal = Math.round((personalTotal / grandTotal) * 100) const pctMarketing = Math.round((marketingTotal / grandTotal) * 100) const pctOps = Math.round((operationsTotal / grandTotal) * 100) const pctInvest = Math.round((investTotal / grandTotal) * 100) // Adjust rounding to 100% const pctReserve = 100 - pctPersonal - pctMarketing - pctOps - pctInvest setUseOfFunds([ { category: 'engineering', label_de: 'Engineering & Personal', label_en: 'Engineering & Personnel', percentage: pctPersonal }, { category: 'sales', label_de: 'Vertrieb & Marketing', label_en: 'Sales & Marketing', percentage: pctMarketing }, { category: 'operations', label_de: 'Betrieb & Infrastruktur', label_en: 'Operations & Infrastructure', percentage: pctOps }, { category: 'hardware', label_de: 'Hardware & Ausstattung', label_en: 'Hardware & Equipment', percentage: pctInvest }, ...(pctReserve > 0 ? [{ category: 'reserve', label_de: 'Reserve', label_en: 'Reserve', percentage: pctReserve }] : []), ].filter(f => f.percentage > 0)) } catch { /* ignore */ } } loadUoF() }, [isWandeldarlehen]) const last = kpis.y2030 return { kpis, loading, last, useOfFunds } }