Files
breakpilot-core/pitch-deck/components/slides/FinanzplanSlide.tsx
Benjamin Admin 92c86ec6ba [split-required] [guardrail-change] Enforce 500 LOC budget across all services
Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook)
and split all 44 files exceeding 500 LOC into domain-focused modules:

- consent-service (Go): models, handlers, services, database splits
- backend-core (Python): security_api, rbac_api, pdf_service, auth splits
- admin-core (TypeScript): 5 page.tsx + sidebar extractions
- pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits
- voice-service (Python): enhanced_task_orchestrator split

Result: 0 violations, 36 exempted (pipeline, tests, pure-data files).
Go build verified clean. No behavior changes — pure structural splits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 00:09:30 +02:00

241 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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<SheetMeta[]>([])
const [scenarios, setScenarios] = useState<FpScenario[]>([])
const [openCats, setOpenCats] = useState<Set<string>>(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<string>('')
const [activeSheet, setActiveSheet] = useState<string>('guv')
const [rows, setRows] = useState<SheetRow[]>([])
const [loading, setLoading] = useState(false)
const [yearOffset, setYearOffset] = useState(0) // 0=2026, 1=2027, ...
const [chartDetail, setChartDetail] = useState<string | null>(null)
const de = lang === 'de'
// KPIs loaded directly from fp_* tables (source of truth)
const [fpKPIs, setFpKPIs] = useState<Record<string, Record<string, number>>>({})
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<string, Record<string, number>> = {}
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 (
<div className="max-w-[95vw] mx-auto">
<FadeInView className="text-center mb-4">
<h2 className="text-3xl md:text-4xl font-bold mb-1">
<GradientText>{de ? 'Finanzplan' : 'Financial Plan'}</GradientText>
</h2>
<p className="text-sm text-white/40">{de ? '20262030 · Alle Werte in EUR · Monatliche Granularität' : '20262030 · All values in EUR · Monthly granularity'}</p>
</FadeInView>
{/* Tab Bar */}
<div className="flex items-center gap-1 mb-3 overflow-x-auto pb-1">
{sheets.map(s => (
<button
key={s.name}
onClick={() => setActiveSheet(s.name)}
className={`px-3 py-1.5 text-[10px] font-medium rounded-lg whitespace-nowrap transition-colors ${
activeSheet === s.name
? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30'
: 'text-white/40 hover:text-white/70 hover:bg-white/[0.05]'
}`}
>
{de ? s.label_de : s.label_en}
{s.rows > 0 && <span className="ml-1 text-[8px] opacity-50">({s.rows})</span>}
</button>
))}
{/* KPIs + Grafiken Tabs */}
<span className="text-white/10 mx-1">|</span>
{[
{ id: 'kpis', label: 'KPIs', icon: Target },
{ id: 'charts', label: de ? 'Grafiken' : 'Charts', icon: BarChart3 },
{ id: 'skr', label: 'Kontenrahmen (SKR04)', icon: BarChart3 },
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveSheet(tab.id)}
className={`px-3 py-1.5 text-[10px] font-medium rounded-lg whitespace-nowrap transition-colors flex items-center gap-1 ${
activeSheet === tab.id
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'
: 'text-white/40 hover:text-white/70 hover:bg-white/[0.05]'
}`}
>
<tab.icon className="w-3 h-3" />
{tab.label}
</button>
))}
</div>
{/* === KPIs Tab === */}
{activeSheet === 'kpis' && <KPIsTab fpKPIs={fpKPIs} de={de} />}
{/* === Charts Tab === */}
{activeSheet === 'charts' && (
<ChartsTab fpKPIs={fpKPIs} de={de} chartDetail={chartDetail} setChartDetail={setChartDetail} />
)}
{/* === Kontenrahmen SKR04 === */}
{activeSheet === 'skr' && <SKRTab de={de} />}
{/* Year Navigation — not for GuV, KPIs, Charts */}
{!['guv', 'kpis', 'charts', 'skr'].includes(activeSheet) && (
<div className="flex items-center justify-center gap-1 mb-2">
<button
onClick={() => setYearOffset(-1)}
className={`px-3 py-1 text-[10px] font-medium rounded-lg transition-colors ${yearOffset === -1 ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30' : 'text-white/40 hover:text-white/70 hover:bg-white/[0.05]'}`}
>
{de ? 'Alle Jahre' : 'All Years'}
</button>
{[2026, 2027, 2028, 2029, 2030].map((y, idx) => (
<button
key={y}
onClick={() => setYearOffset(idx)}
className={`px-3 py-1 text-[10px] font-medium rounded-lg transition-colors ${yearOffset === idx ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30' : 'text-white/40 hover:text-white/70 hover:bg-white/[0.05]'}`}
>
{y}
</button>
))}
</div>
)}
{/* Data Grid — not shown for KPIs and Charts */}
{!['kpis', 'charts', 'skr'].includes(activeSheet) && (
<GlassCard hover={false} className="p-2 overflow-x-auto">
{loading ? (
<div className="text-center py-8 text-white/30 text-sm">{de ? 'Lade...' : 'Loading...'}</div>
) : activeSheet === 'guv' ? (
<GuvTable rows={rows} de={de} />
) : (
<MonthlyGrid
rows={rows}
activeSheet={activeSheet}
de={de}
yearOffset={yearOffset}
openCats={openCats}
toggleCat={toggleCat}
/>
)}
</GlassCard>
)}
<ProjectionFooter lang={lang} />
</div>
)
}