Files
breakpilot-core/pitch-deck/lib/hooks/useFpKPIs.ts
Benjamin Admin 7be1a296c6
Some checks failed
Build pitch-deck / build-push-deploy (push) Successful in 1m11s
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 39s
CI / test-python-voice (push) Successful in 33s
CI / test-bqas (push) Has been cancelled
feat(pitch-deck): NRR + Payback formula-based from fp_* data
- NRR: Revenue year N / Revenue year N-1 × 100 (no more "target > 120%")
- Payback: CAC / monthly gross profit (no more "target < 3 months")
- Both computed in useFpKPIs hook from fp_guv data
- BusinessModelSlide shows computed values with "(berechnet)" label

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 17:47:11 +02:00

110 lines
4.9 KiB
TypeScript

'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<string, number>
values_total?: Record<string, number>
}
/**
* 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<Record<string, FpAnnualKPIs>>({})
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<string, FpAnnualKPIs> = {}
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])
const last = kpis.y2030
return { kpis, loading, last }
}