'use client' import { useCallback, useEffect, useState } from 'react' import { Language } from '@/lib/types' import ProjectionFooter from '../ui/ProjectionFooter' import GradientText from '../ui/GradientText' import FadeInView from '../ui/FadeInView' import GlassCard from '../ui/GlassCard' import { BarChart3, Target } from 'lucide-react' import KPIsTab from './FinanzplanSlide.kpis' import ChartsTab from './FinanzplanSlide.charts' import SKRTab from './FinanzplanSlide.skr' import { SheetMeta, SheetRow, FpScenario, } from './FinanzplanSlide.helpers' import { GuvTable, MonthlyGrid } from './FinanzplanSlide.datagrid' interface FinanzplanSlideProps { lang: Language investorId?: string | null preferredScenarioId?: string | null isWandeldarlehen?: boolean } export default function FinanzplanSlide({ lang, investorId, preferredScenarioId, isWandeldarlehen }: FinanzplanSlideProps) { const [sheets, setSheets] = useState([]) const [scenarios, setScenarios] = useState([]) const [openCats, setOpenCats] = useState>(new Set()) const toggleCat = (cat: string) => setOpenCats(prev => { const n = new Set(prev); n.has(cat) ? n.delete(cat) : n.add(cat); return n }) const [selectedScenarioId, setSelectedScenarioId] = useState('') const [activeSheet, setActiveSheet] = useState('guv') const [rows, setRows] = useState([]) const [loading, setLoading] = useState(false) const [yearOffset, setYearOffset] = useState(0) // 0=2026, 1=2027, ... const [chartDetail, setChartDetail] = useState(null) const de = lang === 'de' // KPIs loaded directly from fp_* tables (source of truth) const [fpKPIs, setFpKPIs] = useState>>({}) useEffect(() => { async function loadKPIs() { const param = selectedScenarioId ? `?scenarioId=${selectedScenarioId}` : '' try { 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 = guv.rows || [] const liqRows = liq.rows || [] const persRows = pers.rows || [] const kundenRows = kunden.rows || [] const findGuv = (label: string) => guvRows.find((r: SheetRow) => (r.row_label || '').includes(label)) const findLiq = (label: string) => liqRows.find((r: SheetRow) => (r.row_label || '').includes(label)) const kundenGesamt = kundenRows.find((r: SheetRow) => r.row_label === 'Bestandskunden gesamt') const years = [2026, 2027, 2028, 2029, 2030] const kpis: Record> = {} for (const y of years) { const yk = `y${y}` const m12 = (y - 2026) * 12 + 12 // December of each year const mk = `m${m12}` 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: SheetRow) => ((r.values_total || r.values)?.[mk] || 0) > 0).length // MRR = December monthly revenue from Liquidität (not annual average) const liqUmsatz = findLiq('Umsatzerlöse') const mrr = Math.round(liqUmsatz?.values?.[mk] || 0) const arr = mrr * 12 // ARR = December MRR × 12 (annualized run-rate) const arpu = customers > 0 ? Math.round(revenue / customers) : 0 // ACV = annual revenue / customers const revPerEmp = headcount > 0 ? Math.round(revenue / headcount) : 0 const ebitMargin = revenue > 0 ? Math.round((ebit / revenue) * 100) : 0 const burnRate = ebit < 0 ? Math.round(Math.abs(ebit / 12)) : 0 // show when EBIT negative, not cash const material = findGuv('Summe Materialaufwand')?.values?.[yk] || 0 const grossMargin = revenue > 0 ? Math.round(((revenue - material) / revenue) * 100) : 0 const prevRevenue = y > 2026 ? (findGuv('Umsatzerlöse')?.values?.[`y${y - 1}`] || 0) : 0 const nrr = prevRevenue > 0 ? Math.round((revenue / prevRevenue) * 100) : 0 // Revenue Growth (proxy for NRR) kpis[yk] = { revenue, ebit, personal, netIncome, steuern, liquiditaet, customers, headcount, mrr, arr, arpu, revPerEmp, ebitMargin, burnRate, grossMargin, nrr } } setFpKPIs(kpis) } catch { /* ignore */ } } loadKPIs() }, [selectedScenarioId]) // Load sheet list + scenarios useEffect(() => { fetch('/api/finanzplan', { cache: 'no-store' }) .then(r => r.json()) .then(data => { setSheets(data.sheets || []) const scens: FpScenario[] = data.scenarios || [] setScenarios(scens) // Pick scenario: Wandeldarlehen version → WD scenario, otherwise default if (!selectedScenarioId) { const wdScenario = isWandeldarlehen ? scens.find(s => s.name.toLowerCase().includes('wandeldarlehen') && !s.name.toLowerCase().includes('bear') && !s.name.toLowerCase().includes('bull')) : null const def = wdScenario ?? scens.find(s => s.is_default) ?? scens[0] if (def) setSelectedScenarioId(def.id) } }) .catch(() => {}) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const scenarioParam = selectedScenarioId ? `?scenarioId=${selectedScenarioId}` : '' // Load sheet data const loadSheet = useCallback(async (name: string) => { if (name === 'kpis' || name === 'charts' || name === 'skr') { setRows([]) setLoading(false) return } setLoading(true) try { const r = await fetch(`/api/finanzplan/${name}${scenarioParam}`, { cache: 'no-store' }) const data = await r.json() setRows(data.rows || []) } catch { /* ignore */ } setLoading(false) }, [scenarioParam]) useEffect(() => { loadSheet(activeSheet) }, [activeSheet, loadSheet]) return (

{de ? 'Finanzplan' : 'Financial Plan'}

{de ? '2026–2030 · Alle Werte in EUR · Monatliche Granularität' : '2026–2030 · All values in EUR · Monthly granularity'}

{/* Tab Bar */}
{sheets.map(s => ( ))} {/* KPIs + Grafiken Tabs */} | {[ { id: 'kpis', label: 'KPIs', icon: Target }, { id: 'charts', label: de ? 'Grafiken' : 'Charts', icon: BarChart3 }, { id: 'skr', label: 'Kontenrahmen (SKR04)', icon: BarChart3 }, ].map(tab => ( ))}
{/* === KPIs Tab === */} {activeSheet === 'kpis' && } {/* === Charts Tab === */} {activeSheet === 'charts' && ( )} {/* === Kontenrahmen SKR04 === */} {activeSheet === 'skr' && } {/* Year Navigation — not for GuV, KPIs, Charts */} {!['guv', 'kpis', 'charts', 'skr'].includes(activeSheet) && (
{[2026, 2027, 2028, 2029, 2030].map((y, idx) => ( ))}
)} {/* Data Grid — not shown for KPIs and Charts */} {!['kpis', 'charts', 'skr'].includes(activeSheet) && ( {loading ? (
{de ? 'Lade...' : 'Loading...'}
) : activeSheet === 'guv' ? ( ) : ( )}
)}
) }