Files
breakpilot-core/pitch-deck/components/slides/FinanzplanSlide.tsx
Benjamin Admin 4265f5175a
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m25s
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 37s
CI / test-python-voice (push) Successful in 33s
CI / test-bqas (push) Successful in 36s
fix(pitch-deck): betriebliche accordion header-first, umsatz labels, annual display
- Betriebliche: category header (sum_row) now renders BEFORE detail rows
- Umsatzerlöse: renamed to Preis/Monat, Anzahl Kunden, Umsatz per tier
- Engine: tier matching via parentheses extraction (handles renamed labels)
- Annual column: quantity=Dec value, price=Dec value (not cumulated)

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

1062 lines
66 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 { t } from '@/lib/i18n'
import ProjectionFooter from '../ui/ProjectionFooter'
import GradientText from '../ui/GradientText'
import FadeInView from '../ui/FadeInView'
import GlassCard from '../ui/GlassCard'
import { RefreshCw, Download, ChevronLeft, ChevronRight, BarChart3, Target } from 'lucide-react'
interface FinanzplanSlideProps {
lang: Language
investorId?: string | null
preferredScenarioId?: string | null
isWandeldarlehen?: boolean
}
interface SheetMeta {
name: string
label_de: string
label_en: string
rows: number
}
interface SheetRow {
id: number
row_label?: string
person_name?: string
item_name?: string
category?: string
section?: string
is_editable?: boolean
is_sum_row?: boolean
values?: Record<string, number>
values_total?: Record<string, number>
values_brutto?: Record<string, number>
brutto_monthly?: number
position?: string
start_date?: string
purchase_amount?: number
[key: string]: unknown
}
const MONTH_LABELS = [
'Jan', 'Feb', 'Mrz', 'Apr', 'Mai', 'Jun',
'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez',
]
function getLabel(row: SheetRow): string {
return row.row_label || row.person_name || row.item_name || '—'
}
const FORMULA_TOOLTIPS: Record<string, string> = {
'Fort-/Weiterbildungskosten (F)': 'Mitarbeiter (ohne Gründer) × 300 EUR/Mon',
'Fahrzeugkosten (F)': 'Mitarbeiter (ohne Gründer) × 200 EUR/Mon',
'KFZ-Steuern (F)': 'Mitarbeiter (ohne Gründer) × 25 EUR/Mon',
'KFZ-Versicherung (F)': 'Mitarbeiter (ohne Gründer) × 150 EUR/Mon',
'Reisekosten (F)': 'Headcount gesamt × 75 EUR/Mon',
'Bewirtungskosten (F)': 'Bestandskunden × 50 EUR/Mon',
'Internet/Mobilfunk (F)': 'Headcount gesamt × 50 EUR/Mon',
'Cloud-Hosting (SysEleven/Hetzner)': '1.500 EUR Basis + (Kunden - 10) × 100 EUR (erste 10 inkl.)',
'Berufsgenossenschaft (F)': '0,5% der Brutto-Lohnsumme (VBG IT/Büro)',
'Allgemeine Marketingkosten (F)': '8% vom Umsatz (2026-2028), 10% ab 2029',
'Gewerbesteuer (F)': '12,25% vom Gewinn (Messzahl 3,5% × Hebesatz 350%, nur bei Gewinn)',
'Personalkosten': 'Summe aus Tab Personalkosten',
'Abschreibungen': 'Summe AfA aus Tab Investitionen',
}
function LabelWithTooltip({ label }: { label: string }) {
const tooltip = FORMULA_TOOLTIPS[label]
if (!tooltip) return <span>{label}</span>
return (
<span className="group relative cursor-help">
{label}
<span className="invisible group-hover:visible absolute left-0 top-full mt-1 z-50 bg-slate-800 border border-white/10 text-[9px] text-white/70 px-2 py-1 rounded shadow-lg whitespace-nowrap">
{tooltip}
</span>
</span>
)
}
function getValues(row: SheetRow): Record<string, number> {
return row.values || row.values_total || row.values_brutto || (row as Record<string, unknown>).values_invest as Record<string, number> || {}
}
function formatCell(v: number | undefined): string {
if (v === undefined || v === null) return ''
if (v === 0) return '—'
return Math.round(v).toLocaleString('de-DE', { maximumFractionDigits: 0 })
}
interface FpScenario { id: string; name: string; is_default: 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 [openSKR, setOpenSKR] = useState<Set<string>>(new Set(['4', '6', '7']))
const toggleSKR = (k: string) => setOpenSKR(prev => { const n = new Set(prev); n.has(k) ? n.delete(k) : n.add(k); return n })
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])
const currentYear = 2026 + yearOffset
const monthStart = yearOffset * 12 + 1
const monthEnd = monthStart + 11
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' && (
<GlassCard hover={false} className="p-4">
<h3 className="text-sm font-bold text-emerald-400 uppercase tracking-wider mb-4">{de ? 'Wichtige Kennzahlen (pro Jahr)' : 'Key Metrics (per year)'}</h3>
<table className="w-full text-xs">
<thead>
<tr className="border-b border-white/10">
<th className="text-left py-2 px-2 text-white/60 font-medium">KPI</th>
{[2026, 2027, 2028, 2029, 2030].map(y => (
<th key={y} className="text-right py-2 px-3 text-white/60 font-medium">{y}</th>
))}
</tr>
</thead>
<tbody>
{(() => {
const years = ['y2026', 'y2027', 'y2028', 'y2029', 'y2030']
if (!fpKPIs['y2026']) return (
<tr><td colSpan={6} className="text-center py-4 text-white/30">{de ? 'Finanzplan wird geladen...' : 'Loading financial plan...'}</td></tr>
)
const v = (yk: string, key: string) => fpKPIs[yk]?.[key] || 0
const kpiRows = [
{ label: 'MRR (Dez)', values: years.map(y => v(y, 'mrr')), unit: '€', bold: true },
{ label: 'ARR (Dez × 12)', values: years.map(y => v(y, 'arr')), unit: '€', bold: true },
{ label: de ? 'Kunden (Dez)' : 'Customers (Dec)', values: years.map(y => v(y, 'customers')), unit: '', bold: false },
{ label: de ? 'ACV (Umsatz/Kunden)' : 'ACV (Revenue/Customers)', values: years.map(y => v(y, 'arpu')), unit: '€', bold: false },
{ label: 'Gross Margin', values: years.map(y => v(y, 'grossMargin')), unit: '%', bold: false },
{ label: de ? 'Umsatzwachstum (YoY)' : 'Revenue Growth (YoY)', values: years.map(y => v(y, 'nrr')), unit: '%', bold: false },
{ label: de ? 'Mitarbeiter' : 'Employees', values: years.map(y => v(y, 'headcount')), unit: '', bold: false },
{ label: de ? 'Umsatz/Mitarbeiter' : 'Revenue/Employee', values: years.map(y => v(y, 'revPerEmp')), unit: '€', bold: false },
{ label: de ? 'Personalkosten' : 'Personnel Costs', values: years.map(y => v(y, 'personal')), unit: '<27><>', bold: false },
{ label: 'EBIT', values: years.map(y => v(y, 'ebit')), unit: '€', bold: true },
{ label: de ? 'EBIT-Marge' : 'EBIT Margin', values: years.map(y => v(y, 'ebitMargin')), unit: '%', bold: false },
{ label: de ? 'Steuern' : 'Taxes', values: years.map(y => v(y, 'steuern')), unit: '€', bold: false },
{ label: de ? 'Jahresüberschuss' : 'Net Income', values: years.map(y => v(y, 'netIncome')), unit: '€', bold: true },
{ label: de ? 'Liquidität (Dez)' : 'Cash (Dec)', values: years.map(y => v(y, 'liquiditaet')), unit: '€', bold: true },
{ label: 'Burn Rate', values: years.map(y => v(y, 'burnRate')), unit: '€/Mo', bold: false },
]
return kpiRows.map((row, idx) => (
<tr key={idx} className={`border-b border-white/[0.03] ${row.bold ? 'bg-white/[0.03]' : ''}`}>
<td className={`py-1.5 px-2 ${row.bold ? 'font-bold text-white/80' : 'text-white/60'}`}>{row.label}</td>
{row.values.map((v, i) => {
const num = typeof v === 'number' ? v : 0
const display = typeof v === 'string' ? v : (
row.unit === '%' ? `${v}%` :
row.unit === '€/Mo' ? formatCell(num) + '/Mo' :
formatCell(num)
)
return (
<td key={i} className={`text-right py-1.5 px-3 font-mono ${num < 0 ? 'text-red-400' : row.bold ? 'text-white/80 font-bold' : 'text-white/50'}`}>
{display}
</td>
)
})}
</tr>
))
})()}
</tbody>
</table>
</GlassCard>
)}
{/* === Charts Tab === */}
{activeSheet === 'charts' && (() => {
const years = ['y2026','y2027','y2028','y2029','y2030']
const fmtK = (v: number) => Math.abs(v) >= 1000000 ? `${(v/1000000).toFixed(1)}M` : `${Math.round(v/1000)}k`
const fmtV = (v: number) => v.toLocaleString('de-DE')
const chartDetails: Record<string, { title: string; desc: string }> = {
mrr: { title: 'MRR (Monthly Recurring Revenue)', desc: de ? 'Monatlich wiederkehrender Umsatz im Dezember des jeweiligen Jahres — der wichtigste KPI für SaaS-Unternehmen. Zeigt den tatsächlichen monatlichen Run-Rate, nicht den Jahresdurchschnitt. ARR = MRR × 12.' : 'Monthly recurring revenue in December of each year — the most important SaaS KPI. Shows the actual monthly run rate, not the annual average. ARR = MRR × 12.' },
ebit: { title: 'EBIT (Earnings Before Interest & Taxes)', desc: de ? 'Operatives Ergebnis vor Zinsen und Steuern — zeigt die tatsächliche Profitabilität des Geschäftsbetriebs. Positiver EBIT = das Geschäftsmodell funktioniert.' : 'Operating profit before interest and taxes — shows actual profitability of operations. Positive EBIT = the business model works.' },
headcount: { title: de ? 'Personalaufbau' : 'Headcount', desc: de ? 'Geplanter Teamaufbau über 5 Jahre. Wir starten lean mit 2 Gründern und wachsen bedarfsorientiert. Jede Einstellung ist an einen konkreten Meilenstein geknüpft.' : 'Planned team growth over 5 years. We start lean with 2 founders and grow based on demand. Each hire is tied to a concrete milestone.' },
cash: { title: de ? 'Liquidität (Jahresende)' : 'Cash Position (Year-End)', desc: de ? 'Kontostand am Ende jedes Jahres. Zeigt ob genug Geld für den laufenden Betrieb vorhanden ist. Die Liquiditätskurve berücksichtigt alle Einnahmen, Ausgaben, Investitionen und Finanzierungen.' : 'Bank balance at end of each year. Shows if enough cash is available for operations. The liquidity curve accounts for all revenue, expenses, investments and financing.' },
revcost: { title: de ? 'Umsatz vs. Gesamtkosten' : 'Revenue vs. Total Costs', desc: de ? 'Vergleich zwischen Einnahmen und Ausgaben. Der Schnittpunkt zeigt den Break-Even — ab dann verdient das Unternehmen mehr als es ausgibt.' : 'Comparison between income and expenses. The intersection shows break-even — from then on, the company earns more than it spends.' },
acv: { title: 'ACV (Average Contract Value)', desc: de ? 'Durchschnittlicher Vertragswert pro Kunde und Jahr. Steigender ACV bedeutet: Kunden kaufen mehr Module oder wechseln in höhere Tiers.' : 'Average contract value per customer per year. Rising ACV means: customers buy more modules or upgrade to higher tiers.' },
grossMargin: { title: 'Gross Margin', desc: de ? 'Rohertragsmarge — wieviel Prozent vom Umsatz nach Abzug der direkten Kosten (Cloud-Infrastruktur, Lizenzen) übrig bleibt. Bei SaaS typisch 70-90%.' : 'How much revenue remains after direct costs (cloud infrastructure, licenses). Typical for SaaS: 70-90%.' },
nrr: { title: de ? 'Umsatzwachstum (YoY)' : 'Revenue Growth (YoY)', desc: de ? 'Jahresvergleich des Gesamtumsatzes — zeigt die Wachstumsgeschwindigkeit. In der Frühphase primär durch Neukundengewinnung getrieben, später zunehmend durch Expansion bestehender Kunden (Upselling in höhere Tiers).' : 'Year-over-year total revenue comparison — shows growth velocity. In early stages driven primarily by new customer acquisition, later increasingly by expansion of existing customers (upselling to higher tiers).' },
ebitMargin: { title: 'EBIT Margin', desc: de ? 'Operatives Ergebnis in Prozent vom Umsatz. Zeigt die Effizienz des Geschäftsmodells. Ziel für SaaS: 20-30% in der Reifephase.' : 'Operating result as percentage of revenue. Shows business model efficiency. Target for SaaS: 20-30% at maturity.' },
}
const detailInfo = chartDetail ? chartDetails[chartDetail] : null
return (
<div className="space-y-4">
{/* Detail overlay */}
{detailInfo && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" onClick={() => setChartDetail(null)}>
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
<div className="relative bg-slate-900/95 border border-white/10 rounded-2xl p-6 max-w-lg w-full" onClick={e => e.stopPropagation()}>
<button onClick={() => setChartDetail(null)} className="absolute top-4 right-4 text-white/40 hover:text-white/80 text-lg"></button>
<h3 className="text-xl font-bold text-white mb-3">{detailInfo.title}</h3>
<p className="text-sm text-white/60 leading-relaxed">{detailInfo.desc}</p>
{chartDetail && fpKPIs.y2026 && (
<div className="mt-4 pt-4 border-t border-white/10">
<div className="grid grid-cols-5 gap-2 text-center">
{[2026,2027,2028,2029,2030].map(y => {
const keyMap: Record<string, string> = { cash: 'liquiditaet', revcost: 'revenue', acv: 'arpu' }
const key = keyMap[chartDetail!] || chartDetail
const v = fpKPIs[`y${y}`]?.[key as keyof typeof fpKPIs['y2026']] as number || 0
return (
<div key={y}>
<div className="text-[10px] text-white/40">{y}</div>
<div className="text-sm font-bold text-white">{typeof v === 'number' && Math.abs(v) >= 1000 ? fmtK(v) : `${v}`}</div>
</div>
)
})}
</div>
</div>
)}
</div>
</div>
)}
{/* MRR + Kunden Chart */}
<GlassCard hover={false} className="p-4 cursor-pointer" onClick={() => setChartDetail('mrr')}>
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-bold text-indigo-400 uppercase tracking-wider">{de ? 'MRR & Kundenentwicklung' : 'MRR & Customer Growth'}</h3>
<span className="text-[9px] text-white/30">{de ? 'Klicken für Details' : 'Click for details'}</span>
</div>
<div className="flex">
{/* Y-axis labels */}
<div className="flex flex-col justify-between h-40 pr-2 text-[9px] text-white/30 text-right" style={{ width: 45 }}>
{(() => { const m = Math.max(...years.map(y => fpKPIs[y]?.mrr || 0), 1); return [m, Math.round(m/2), 0].map((v,i) => <span key={i}>{fmtK(v)} </span>) })()}
</div>
<div className="flex-1 grid grid-cols-5 gap-1 items-end h-40 border-l border-b border-white/10 pl-2 pb-1">
{years.map((y, idx) => {
const d = { mrr: fpKPIs[y]?.mrr || 0, cust: fpKPIs[y]?.customers || 0 }
const maxMrr = Math.max(...years.map(k => fpKPIs[k]?.mrr || 0), 1)
const maxCust = Math.max(...years.map(k => fpKPIs[k]?.customers || 0), 1)
return (
<div key={idx} className="flex flex-col items-center gap-1">
<div className="flex items-end gap-1 w-full justify-center" style={{ height: '130px' }}>
<div className="w-8 bg-indigo-500/60 rounded-t transition-all" style={{ height: `${(d.mrr / maxMrr) * 120}px` }}>
<div className="text-[10px] text-white/80 text-center -mt-3.5 whitespace-nowrap font-semibold">{fmtK(d.mrr)}</div>
</div>
<div className="w-8 bg-emerald-500/60 rounded-t transition-all" style={{ height: `${(d.cust / maxCust) * 120}px` }}>
<div className="text-[10px] text-white/80 text-center -mt-3.5 whitespace-nowrap font-semibold">{d.cust}</div>
</div>
</div>
<span className="text-[10px] text-white/50 font-medium">{y.slice(1)}</span>
</div>
)
})}
</div>
</div>
<div className="flex justify-center gap-6 mt-2 text-[10px]">
<span className="flex items-center gap-1.5"><span className="w-3 h-2 bg-indigo-500/60 rounded inline-block" /> MRR (/Mon)</span>
<span className="flex items-center gap-1.5"><span className="w-3 h-2 bg-emerald-500/60 rounded inline-block" /> {de ? 'Bestandskunden (Dez)' : 'Customers (Dec)'}</span>
</div>
</GlassCard>
{/* EBIT + Headcount */}
<div className="grid md:grid-cols-2 gap-4">
<GlassCard hover={false} className="p-4 cursor-pointer" onClick={() => setChartDetail('ebit')}>
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-bold text-purple-400 uppercase tracking-wider">EBIT</h3>
<span className="text-[9px] text-white/30">/Jahr</span>
</div>
<div className="flex">
<div className="flex flex-col justify-between h-28 pr-2 text-[9px] text-white/30 text-right" style={{ width: 40 }}>
{(() => { const vals = years.map(y => fpKPIs[y]?.ebit || 0); const mx = Math.max(...vals.map(Math.abs), 1); return [fmtK(mx), '0', fmtK(-mx)].map((v,i) => <span key={i}>{v}</span>) })()}
</div>
<div className="flex-1 grid grid-cols-5 gap-1 items-end h-28 border-l border-white/10 pl-2">
{years.map((y, idx) => {
const v = fpKPIs[y]?.ebit || 0
const maxAbs = Math.max(...years.map(k => Math.abs(fpKPIs[k]?.ebit || 0)), 1)
const h = Math.abs(v) / maxAbs * 90
return (
<div key={idx} className="flex flex-col items-center">
<div className="w-10 flex flex-col justify-end" style={{ height: '100px' }}>
<div className={`${v >= 0 ? 'bg-emerald-500/60 rounded-t' : 'bg-red-500/60 rounded-b'} w-full`} style={{ height: `${h}px` }}>
<div className={`text-[10px] ${v >= 0 ? 'text-emerald-300' : 'text-red-300'} text-center -mt-3.5 whitespace-nowrap font-semibold`}>{fmtK(v)}</div>
</div>
</div>
<span className="text-[10px] text-white/50 mt-1 font-medium">{y.slice(1)}</span>
</div>
)
})}
</div>
</div>
</GlassCard>
<GlassCard hover={false} className="p-4 cursor-pointer" onClick={() => setChartDetail('headcount')}>
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-bold text-amber-400 uppercase tracking-wider">{de ? 'Personalaufbau' : 'Headcount'}</h3>
<span className="text-[9px] text-white/30">{de ? 'Mitarbeiter (Dez)' : 'Employees (Dec)'}</span>
</div>
<div className="flex">
<div className="flex flex-col justify-between h-28 pr-2 text-[9px] text-white/30 text-right" style={{ width: 25 }}>
{(() => { const m = Math.max(...years.map(y => fpKPIs[y]?.headcount || 0), 1); return [m, Math.round(m/2), 0].map((v,i) => <span key={i}>{v}</span>) })()}
</div>
<div className="flex-1 grid grid-cols-5 gap-1 items-end h-28 border-l border-b border-white/10 pl-2 pb-1">
{years.map((y, idx) => {
const v = fpKPIs[y]?.headcount || 0
const mx = Math.max(...years.map(k => fpKPIs[k]?.headcount || 0), 1)
return (
<div key={idx} className="flex flex-col items-center">
<div className="w-10 flex flex-col justify-end" style={{ height: '90px' }}>
<div className="bg-amber-500/60 rounded-t w-full" style={{ height: `${(v / mx) * 80}px` }}>
<div className="text-[11px] text-amber-300 text-center -mt-3.5 font-bold">{v}</div>
</div>
</div>
<span className="text-[10px] text-white/50 mt-1 font-medium">{y.slice(1)}</span>
</div>
)
})}
</div>
</div>
</GlassCard>
</div>
{/* Liquidität + Revenue vs Costs */}
<div className="grid md:grid-cols-2 gap-4">
<GlassCard hover={false} className="p-4 cursor-pointer" onClick={() => setChartDetail('cash')}>
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-bold text-cyan-400 uppercase tracking-wider">{de ? 'Liquidität' : 'Cash Position'}</h3>
<span className="text-[9px] text-white/30">{de ? 'Jahresende' : 'Year-End'}</span>
</div>
<div className="flex">
<div className="flex flex-col justify-between h-28 pr-2 text-[9px] text-white/30 text-right" style={{ width: 40 }}>
{(() => { const m = Math.max(...years.map(y => Math.abs(fpKPIs[y]?.liquiditaet || 0)), 1); return [fmtK(m), fmtK(m/2), '0'].map((v,i) => <span key={i}>{v}</span>) })()}
</div>
<div className="flex-1 grid grid-cols-5 gap-1 items-end h-28 border-l border-b border-white/10 pl-2 pb-1">
{years.map((y, idx) => {
const v = fpKPIs[y]?.liquiditaet || 0
const mx = Math.max(...years.map(k => Math.abs(fpKPIs[k]?.liquiditaet || 0)), 1)
const h = Math.abs(v) / mx * 90
return (
<div key={idx} className="flex flex-col items-center">
<div className="w-10 flex flex-col justify-end" style={{ height: '90px' }}>
<div className={`${v >= 0 ? 'bg-cyan-500/60 rounded-t' : 'bg-red-500/60 rounded-b'} w-full`} style={{ height: `${Math.max(h, 4)}px` }}>
<div className={`text-[10px] ${v >= 0 ? 'text-cyan-300' : 'text-red-300'} text-center -mt-3.5 whitespace-nowrap font-semibold`}>{fmtK(v)}</div>
</div>
</div>
<span className="text-[10px] text-white/50 mt-1 font-medium">{y.slice(1)}</span>
</div>
)
})}
</div>
</div>
</GlassCard>
<GlassCard hover={false} className="p-4 cursor-pointer" onClick={() => setChartDetail('revcost')}>
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-bold text-indigo-400 uppercase tracking-wider">{de ? 'Umsatz vs. Kosten' : 'Revenue vs. Costs'}</h3>
<span className="text-[9px] text-white/30">/Jahr</span>
</div>
<div className="flex">
<div className="flex flex-col justify-between h-28 pr-2 text-[9px] text-white/30 text-right" style={{ width: 40 }}>
{(() => {
const d = years.map(y => ({ rev: fpKPIs[y]?.revenue || 0, costs: (fpKPIs[y]?.revenue || 0) - (fpKPIs[y]?.ebit || 0) }))
const mx = Math.max(...d.map(x => Math.max(x.rev, x.costs)), 1)
return [fmtK(mx), fmtK(mx/2), '0'].map((v,i) => <span key={i}>{v}</span>)
})()}
</div>
<div className="flex-1 grid grid-cols-5 gap-1 items-end h-28 border-l border-b border-white/10 pl-2 pb-1">
{(() => {
const data = years.map(y => ({ year: y.slice(1), rev: fpKPIs[y]?.revenue || 0, costs: (fpKPIs[y]?.revenue || 0) - (fpKPIs[y]?.ebit || 0) }))
const mx = Math.max(...data.map(d => Math.max(d.rev, d.costs)), 1)
return data.map((d, idx) => (
<div key={idx} className="flex flex-col items-center gap-1">
<div className="flex items-end gap-1 w-full justify-center" style={{ height: '90px' }}>
<div className="w-6 bg-indigo-500/60 rounded-t" style={{ height: `${(d.rev / mx) * 80}px` }}>
<div className="text-[9px] text-indigo-300 text-center -mt-3 whitespace-nowrap font-semibold">{fmtK(d.rev)}</div>
</div>
<div className="w-6 bg-red-500/40 rounded-t" style={{ height: `${(d.costs / mx) * 80}px` }}>
<div className="text-[9px] text-red-300 text-center -mt-3 whitespace-nowrap font-semibold">{fmtK(d.costs)}</div>
</div>
</div>
<span className="text-[10px] text-white/50 font-medium">{d.year}</span>
</div>
))
})()}
</div>
</div>
<div className="flex justify-center gap-4 mt-2 text-[9px]">
<span className="flex items-center gap-1"><span className="w-3 h-1.5 bg-indigo-500/60 rounded inline-block" /> {de ? 'Umsatz' : 'Revenue'}</span>
<span className="flex items-center gap-1"><span className="w-3 h-1.5 bg-red-500/40 rounded inline-block" /> {de ? 'Kosten' : 'Costs'}</span>
</div>
</GlassCard>
</div>
{/* Unit Economics — clickable cards */}
<GlassCard hover={false} className="p-4">
<h3 className="text-xs font-bold text-purple-400 uppercase tracking-wider mb-3">Unit Economics (20262030)</h3>
<div className="grid md:grid-cols-4 gap-3">
{[
{ label: 'ACV', key: 'arpu', unit: '€', color: 'text-indigo-300', bg: 'bg-indigo-500/60', detail: 'acv' },
{ label: 'Gross Margin', key: 'grossMargin', unit: '%', color: 'text-emerald-300', bg: 'bg-emerald-500/60', detail: 'grossMargin' },
{ label: de ? 'Wachstum' : 'Growth', key: 'nrr', unit: '%', color: 'text-purple-300', bg: 'bg-purple-500/60', detail: 'nrr' },
{ label: 'EBIT Margin', key: 'ebitMargin', unit: '%', color: 'text-amber-300', bg: 'bg-amber-500/60', detail: 'ebitMargin' },
].map((metric, mIdx) => (
<div key={mIdx} className="text-center cursor-pointer hover:bg-white/[0.03] rounded-lg p-2 transition-colors" onClick={() => setChartDetail(metric.detail)}>
<p className={`text-[10px] font-bold ${metric.color} uppercase tracking-wider mb-2`}>{metric.label}</p>
<div className="flex items-end justify-center gap-1 h-16">
{[2026,2027,2028,2029,2030].map((y, idx) => {
const val = fpKPIs[`y${y}`]?.[metric.key as keyof typeof fpKPIs['y2026']] as number || 0
const maxVal = Math.max(...[2026,2027,2028,2029,2030].map(yr => Math.abs(fpKPIs[`y${yr}`]?.[metric.key as keyof typeof fpKPIs['y2026']] as number || 0)), 1)
const h = Math.abs(val) / maxVal * 50
return (
<div key={idx} className="flex flex-col items-center">
<div className={`w-4 ${val >= 0 ? metric.bg + ' rounded-t' : 'bg-red-500/60 rounded-b'}`} style={{ height: `${Math.max(h, 2)}px` }} />
<span className="text-[8px] text-white/30 mt-0.5">{String(y).slice(2)}</span>
</div>
)
})}
</div>
<p className={`text-sm font-bold ${metric.color} mt-1`}>
{(() => {
const v = fpKPIs.y2030?.[metric.key as keyof typeof fpKPIs['y2026']] as number || 0
return metric.unit === '€' ? `${fmtV(v)}${metric.unit}` : `${v}${metric.unit}`
})()}
</p>
<p className="text-[8px] text-white/25">2030</p>
</div>
))}
</div>
</GlassCard>
</div>
)
})()}
{/* === Kontenrahmen SKR04 === */}
{activeSheet === 'skr' && (() => {
const skr04: { klasse: string; title: string; color: string; accounts: { nr: string; name: string; used?: boolean }[] }[] = [
{ klasse: '0', title: de ? 'Anlagevermögen' : 'Fixed Assets', color: 'text-slate-400', accounts: [
{ nr: '0200', name: de ? 'Technische Anlagen & Maschinen' : 'Technical Equipment', used: false },
{ nr: '0400', name: de ? 'Betriebs- und Geschäftsausstattung' : 'Office & Business Equipment', used: true },
{ nr: '0420', name: de ? 'EDV-Hardware' : 'IT Hardware', used: true },
{ nr: '0440', name: de ? 'Geringwertige Wirtschaftsgüter (GWG)' : 'Low-Value Assets (GWG)', used: true },
{ nr: '0480', name: de ? 'Immaterielle Vermögensgegenstände' : 'Intangible Assets', used: true },
]},
{ klasse: '1', title: de ? 'Umlaufvermögen' : 'Current Assets', color: 'text-blue-400', accounts: [
{ nr: '1200', name: de ? 'Bank (Geschäftskonto)' : 'Bank (Business Account)', used: true },
{ nr: '1210', name: de ? 'Bank (Festgeld/Rücklagen)' : 'Bank (Fixed Deposit)', used: false },
{ nr: '1400', name: de ? 'Forderungen aus L&L' : 'Accounts Receivable', used: true },
{ nr: '1590', name: de ? 'Durchlaufende Posten' : 'Transit Items', used: false },
]},
{ klasse: '2', title: de ? 'Eigenkapital & Verbindlichkeiten' : 'Equity & Liabilities', color: 'text-purple-400', accounts: [
{ nr: '2000', name: de ? 'Gezeichnetes Kapital (Stammkapital)' : 'Share Capital', used: true },
{ nr: '2010', name: de ? 'Kapitalrücklage (Wandeldarlehen)' : 'Capital Reserve (Convertible Loan)', used: true },
{ nr: '2100', name: de ? 'Gewinnvortrag / Verlustvortrag' : 'Retained Earnings / Losses', used: true },
{ nr: '2900', name: de ? 'Jahresüberschuss / -fehlbetrag' : 'Net Income / Loss', used: true },
]},
{ klasse: '3', title: de ? 'Rückstellungen & Verbindlichkeiten' : 'Provisions & Payables', color: 'text-indigo-400', accounts: [
{ nr: '3070', name: de ? 'Verbindlichkeiten ggü. Kreditinstituten' : 'Bank Liabilities', used: true },
{ nr: '3100', name: de ? 'Verbindlichkeiten aus L&L' : 'Accounts Payable', used: true },
{ nr: '3150', name: de ? 'Darlehen (L-Bank Pre-Seed)' : 'Loan (L-Bank Pre-Seed)', used: true },
{ nr: '3500', name: de ? 'Umsatzsteuer-Verbindlichkeit' : 'VAT Payable', used: true },
{ nr: '3520', name: de ? 'Lohnsteuer-Verbindlichkeit' : 'Payroll Tax Payable', used: true },
{ nr: '3550', name: de ? 'Sozialversicherungs-Verbindlichkeit' : 'Social Security Payable', used: true },
{ nr: '3900', name: de ? 'Rückstellungen (Jahresabschluss etc.)' : 'Provisions (Closing etc.)', used: true },
]},
{ klasse: '4', title: de ? 'Betriebliche Erträge' : 'Operating Revenue', color: 'text-emerald-400', accounts: [
{ nr: '4400', name: de ? 'Erlöse Software-Lizenzen (SaaS)' : 'Software License Revenue (SaaS)', used: true },
{ nr: '4410', name: de ? 'Erlöse Beratung & Service' : 'Consulting & Service Revenue', used: true },
{ nr: '4440', name: de ? 'Erlöse Hardware (Mac Mini/Studio)' : 'Hardware Revenue', used: false },
{ nr: '4500', name: de ? 'Sonstige betriebliche Erträge' : 'Other Operating Revenue', used: true },
{ nr: '4510', name: de ? 'Fördergelder / Grants' : 'Grants / Subsidies', used: true },
{ nr: '4520', name: de ? 'Forschungszulage (§ 27a EStG)' : 'Research Tax Credit', used: true },
]},
{ klasse: '5', title: de ? 'Materialaufwand / COGS' : 'Material Costs / COGS', color: 'text-orange-400', accounts: [
{ nr: '5400', name: de ? 'Cloud-Hosting (SysEleven/Hetzner)' : 'Cloud Hosting', used: true },
{ nr: '5410', name: de ? 'LLM-Inferenzkosten' : 'LLM Inference Costs', used: true },
{ nr: '5420', name: de ? '3rd Party API (Tavily etc.)' : '3rd Party API', used: true },
{ nr: '5430', name: de ? 'Datenbank-Hosting (PostgreSQL/Qdrant)' : 'Database Hosting', used: true },
{ nr: '5440', name: de ? 'CDN / Storage / Monitoring' : 'CDN / Storage / Monitoring', used: true },
]},
{ klasse: '6', title: de ? 'Personalaufwand' : 'Personnel Costs', color: 'text-cyan-400', accounts: [
{ nr: '6000', name: de ? 'Löhne und Gehälter' : 'Wages and Salaries', used: true },
{ nr: '6010', name: de ? 'Geschäftsführer-Gehälter' : 'Managing Director Salaries', used: true },
{ nr: '6100', name: de ? 'Soziale Abgaben (AG-Anteil)' : 'Social Security (Employer)', used: true },
{ nr: '6110', name: de ? 'Berufsgenossenschaft (VBG)' : 'Professional Association (VBG)', used: true },
{ nr: '6130', name: de ? 'Vermögenswirksame Leistungen' : 'Capital-Forming Benefits', used: false },
{ nr: '6170', name: de ? 'Freiwillige Sozialleistungen' : 'Voluntary Social Benefits', used: false },
]},
{ klasse: '7', title: de ? 'Sonstige betriebliche Aufwendungen' : 'Other Operating Expenses', color: 'text-amber-400', accounts: [
{ nr: '7000', name: de ? 'Raumkosten / Miete' : 'Rent / Room Costs', used: true },
{ nr: '7100', name: de ? 'Versicherungen (D&O, Cyber, Haftpflicht)' : 'Insurance (D&O, Cyber, Liability)', used: true },
{ nr: '7200', name: de ? 'Fahrzeugkosten / KFZ' : 'Vehicle Costs', used: true },
{ nr: '7300', name: de ? 'Werbe- und Marketingkosten' : 'Marketing Costs', used: true },
{ nr: '7310', name: de ? 'Teilnahme an Messen' : 'Trade Fair Participation', used: true },
{ nr: '7320', name: de ? 'Bewirtungskosten' : 'Entertainment Costs', used: true },
{ nr: '7400', name: de ? 'Reisekosten' : 'Travel Costs', used: true },
{ nr: '7500', name: de ? 'Internet / Mobilfunk' : 'Internet / Mobile', used: true },
{ nr: '7510', name: de ? 'Serverkosten / Cloud (→ Klasse 5)' : 'Server / Cloud (→ Class 5)', used: false },
{ nr: '7600', name: de ? 'Rechts-/Beratungskosten' : 'Legal / Advisory Costs', used: true },
{ nr: '7610', name: de ? 'Buchführungskosten' : 'Bookkeeping Costs', used: true },
{ nr: '7620', name: de ? 'Jahresabschlusskosten' : 'Annual Closing Costs', used: true },
{ nr: '7630', name: de ? 'Ext. Datenschutzbeauftragter' : 'Ext. Data Protection Officer', used: true },
{ nr: '7640', name: de ? 'Zertifizierung (ISO 27001 / BSI C5)' : 'Certification (ISO 27001 / BSI C5)', used: true },
{ nr: '7650', name: de ? 'Recruiting / Stellenanzeigen' : 'Recruiting / Job Ads', used: true },
{ nr: '7680', name: de ? 'IHK / Kammerbeiträge' : 'Chamber of Commerce Fees', used: true },
{ nr: '7690', name: de ? 'Rundfunkbeitrag' : 'Broadcasting Fee', used: true },
{ nr: '7700', name: de ? 'Abschreibungen (AfA)' : 'Depreciation', used: true },
{ nr: '7750', name: de ? 'Fort- und Weiterbildung' : 'Training & Development', used: true },
{ nr: '7800', name: de ? 'Bankgebühren / Nebenkosten Geldverkehr' : 'Bank Fees', used: true },
{ nr: '7900', name: de ? 'Schutzrechte / Lizenzkosten' : 'IP Rights / License Costs', used: true },
]},
{ klasse: '8', title: de ? 'Finanzerträge & -aufwendungen' : 'Financial Income & Expenses', color: 'text-red-400', accounts: [
{ nr: '8100', name: de ? 'Zinserträge' : 'Interest Income', used: false },
{ nr: '8200', name: de ? 'Zinsaufwendungen' : 'Interest Expenses', used: true },
{ nr: '8210', name: de ? 'Zinsen L-Bank Wandeldarlehen (8%)' : 'Interest L-Bank Convertible (8%)', used: true },
]},
{ klasse: '9', title: de ? 'Steuern & Jahresabschluss' : 'Taxes & Closing', color: 'text-rose-400', accounts: [
{ nr: '9000', name: de ? 'Gewerbesteuer' : 'Trade Tax', used: true },
{ nr: '9100', name: de ? 'Körperschaftsteuer + Soli' : 'Corporate Tax + Surcharge', used: true },
{ nr: '9200', name: de ? 'Umsatzsteuer (Zahllast)' : 'VAT (Payable)', used: true },
{ nr: '9300', name: de ? 'Lohnsteuer' : 'Payroll Tax', used: true },
]},
]
return (
<GlassCard hover={false} className="p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-bold text-white/80">{de ? 'Kontenrahmen SKR04 — Breakpilot COMPLAI GmbH' : 'Chart of Accounts SKR04 — Breakpilot COMPLAI GmbH'}</h3>
<div className="flex items-center gap-3 text-[9px]">
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-emerald-400 inline-block" /> {de ? 'Aktiv genutzt' : 'Actively used'}</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-white/15 inline-block" /> {de ? 'Geplant / nicht aktiv' : 'Planned / inactive'}</span>
</div>
</div>
<div className="space-y-1">
{skr04.map(k => (
<div key={k.klasse}>
<button onClick={() => toggleSKR(k.klasse)} className="w-full flex items-center gap-2 py-1.5 px-2 rounded hover:bg-white/[0.03] transition-colors text-left">
<span className="text-[10px] text-white/30 w-3">{openSKR.has(k.klasse) ? '▾' : '▸'}</span>
<span className={`text-xs font-bold ${k.color}`}>Klasse {k.klasse}</span>
<span className="text-xs text-white/60"> {k.title}</span>
<span className="text-[9px] text-white/25 ml-auto">{k.accounts.filter(a => a.used).length}/{k.accounts.length}</span>
</button>
{openSKR.has(k.klasse) && (
<div className="ml-7 mb-2 space-y-0.5">
{k.accounts.map(a => (
<div key={a.nr} className={`flex items-center gap-2 py-0.5 px-2 rounded text-[11px] ${a.used ? 'text-white/70' : 'text-white/25'}`}>
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${a.used ? 'bg-emerald-400' : 'bg-white/15'}`} />
<span className="font-mono text-white/30 w-10">{a.nr}</span>
<span>{a.name}</span>
</div>
))}
</div>
)}
</div>
))}
</div>
<div className="mt-3 pt-2 border-t border-white/5 text-[10px] text-white/25 text-center">
SKR04 (Industriekontenrahmen) · {de ? 'Angepasst für SaaS/Tech GmbH' : 'Adapted for SaaS/Tech GmbH'} · {de ? '10 Klassen · 62 Konten' : '10 classes · 62 accounts'}
</div>
</GlassCard>
)
})()}
{/* 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' ? (
/* === GuV: Annual table (y2026-y2030) === */
<table className="w-full text-[10px]">
<thead>
<tr className="border-b border-white/10">
<th className="text-left py-1.5 px-2 text-white/60 font-medium sticky left-0 bg-slate-900/90 backdrop-blur min-w-[220px]">
{de ? 'GuV-Position' : 'P&L Item'}
</th>
{[2026, 2027, 2028, 2029, 2030].map(y => (
<th key={y} className="text-right py-1.5 px-3 text-white/60 font-medium min-w-[100px]">{y}</th>
))}
</tr>
</thead>
<tbody>
{rows.map(row => {
const values = getValues(row)
const label = getLabel(row)
const isMajorSum = label === 'EBIT' || label.includes('Rohergebnis') || label.includes('Jahresüberschuss') || label.includes('Ergebnis nach Steuern')
const isMinorSum = row.is_sum_row || label.includes('Summe') || label.includes('Gesamtleistung') || label.includes('Steuern gesamt')
const isSumRow = isMajorSum || isMinorSum
return (
<tr key={row.id} className={`${isMajorSum ? 'border-t-2 border-t-white/20 border-b border-b-white/[0.05] bg-white/[0.05]' : isMinorSum ? 'border-t border-t-white/10 border-b border-b-white/[0.03] bg-white/[0.03]' : 'border-b border-white/[0.03]'} hover:bg-white/[0.02]`}>
<td className={`py-1.5 px-2 sticky left-0 bg-slate-900/90 backdrop-blur ${isMajorSum ? 'font-bold text-white text-xs' : isMinorSum ? 'font-semibold text-white/80' : 'text-white/60'}`}>
<LabelWithTooltip label={label} />
</td>
{[2026, 2027, 2028, 2029, 2030].map(y => {
const v = values[`y${y}`] || 0
return (
<td key={y} className={`text-right py-1.5 px-3 ${v < 0 ? 'text-red-400' : v > 0 ? (isMajorSum ? 'text-white' : isSumRow ? 'text-white/80' : 'text-white/50') : 'text-white/15'} ${isMajorSum ? 'font-bold text-xs' : isSumRow ? 'font-semibold' : ''}`}>
{v === 0 ? '—' : Math.round(v).toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
) : (
/* === Monthly/Annual Grid (all other sheets) === */
<table className="w-full text-[10px]">
<thead>
<tr className="border-b border-white/10">
<th className="text-left py-1.5 px-2 text-white/60 font-medium sticky left-0 bg-slate-900/90 backdrop-blur min-w-[160px]">
{de ? 'Position' : 'Item'}
</th>
{yearOffset === -1 ? (
// All years view
[2026, 2027, 2028, 2029, 2030].map(y => (
<th key={y} className="text-right py-1.5 px-3 text-white/60 font-medium min-w-[80px]">{y}</th>
))
) : (
<>
<th className="text-right py-1.5 px-2 text-white/60 font-medium min-w-[70px]">
{currentYear}
</th>
{MONTH_LABELS.map((label, idx) => (
<th key={idx} className="text-right py-1.5 px-1.5 text-white/50 font-normal min-w-[55px]">
{label}
</th>
))}
</>
)}
</tr>
</thead>
<tbody>
{(() => {
// Live-compute sum rows from detail rows (like Excel formulas)
const computedRows = rows.map(row => {
const label = getLabel(row)
const cat = row.category as string || ''
const rowType = (row as Record<string, unknown>).row_type as string || ''
const isSumLabel = row.is_sum_row || label.includes('Summe') || label.includes('SUMME') || label.includes('ÜBERSCHUSS') || label.includes('UEBERSCHUSS')
if (!isSumLabel) return row
let sourceRows: SheetRow[] = []
// === Betriebliche Aufwendungen: category-based sums ===
if (cat && cat !== 'summe') {
sourceRows = rows.filter(r => (r.category as string) === cat && !r.is_sum_row && getLabel(r) !== label)
} else if (label.includes('Summe sonstige')) {
sourceRows = rows.filter(r => {
const rCat = r.category as string || ''
const rLabel = getLabel(r)
return !r.is_sum_row && rCat !== 'personal' && rCat !== 'abschreibungen' && rCat !== 'summe' &&
!rLabel.includes('Personalkosten') && !rLabel.includes('Abschreibungen') && !rLabel.includes('Summe') && !rLabel.includes('SUMME')
})
} else if (label.includes('SUMME Betriebliche')) {
// Include ALL rows: personal + abschreibungen + all detail rows (but not other sum rows)
sourceRows = rows.filter(r => {
const rCat = r.category as string || ''
const rLabel = getLabel(r)
// Include Personalkosten and Abschreibungen (they have is_sum_row=true but are real data)
if (rLabel === 'Personalkosten' || rLabel === 'Abschreibungen') return true
// Exclude sum rows and category "summe"
return !r.is_sum_row && rCat !== 'summe' && !rLabel.includes('Summe') && !rLabel.includes('SUMME')
})
}
// === Liquidität: row_type-based sums ===
else if (label.includes('Summe') && label.includes('ERTR')) {
sourceRows = rows.filter(r => (r as Record<string, unknown>).row_type === 'einzahlung' && !getLabel(r).includes('Summe'))
} else if (label.includes('Summe') && label.includes('AUSZAHL')) {
sourceRows = rows.filter(r => (r as Record<string, unknown>).row_type === 'auszahlung' && !getLabel(r).includes('Summe'))
}
// === Liquidität: ÜBERSCHUSS = Erträge - Auszahlungen ===
else if (label.includes('ÜBERSCHUSS') || label.includes('UEBERSCHUSS')) {
// These are complex formulas — keep DB values for now
return row
}
// === Umsatzerlöse: GESAMTUMSATZ = sum of revenue rows only ===
else if (label.includes('GESAMTUMSATZ')) {
sourceRows = rows.filter(r => {
const sec = (r as Record<string, unknown>).section as string || ''
return sec === 'revenue' && !getLabel(r).includes('GESAMTUMSATZ')
})
}
// === Materialaufwand: SUMME = sum of cost rows only ===
else if (label.includes('SUMME Material') || (activeSheet === 'materialaufwand' && label === 'SUMME')) {
sourceRows = rows.filter(r => {
const sec = (r as Record<string, unknown>).section as string || ''
return sec === 'cost' && getLabel(r) !== 'SUMME'
})
}
// === Kunden GESAMT rows — trust DB values (engine computed) ===
else if (label.includes('GESAMT') || label.includes('Bestandskunden gesamt')) {
return row
}
if (sourceRows.length === 0) return row
const computed: Record<string, number> = {}
for (let m = 1; m <= 60; m++) {
const key = `m${m}`
computed[key] = Math.round(sourceRows.reduce((sum, r) => sum + (getValues(r)[key] || 0), 0))
}
return { ...row, values: computed, values_total: computed }
})
// For betriebliche: reorder so category header (sum_row) comes BEFORE detail rows
if (activeSheet === 'betriebliche') {
const grouped: SheetRow[] = []
const cats = new Map<string, { header: SheetRow | null; details: SheetRow[] }>()
// Collect by category
for (const row of computedRows) {
const cat = row.category as string || ''
if (!cats.has(cat)) cats.set(cat, { header: null, details: [] })
const g = cats.get(cat)!
if (row.is_sum_row) g.header = row
else g.details.push(row)
}
// Output: header first, then details (if open)
for (const [cat, g] of cats) {
if (g.header) grouped.push(g.header)
if (openCats.has(cat) || cat === 'summe' || cat === 'personal' || cat === 'abschreibungen') {
grouped.push(...g.details)
}
}
return grouped
}
return computedRows
})().map(row => {
const values = getValues(row)
const label = getLabel(row)
const isSumRow = row.is_sum_row || label.includes('GESAMT') || label.includes('Summe') || label.includes('ÜBERSCHUSS') || label.includes('LIQUIDITÄT') || label.includes('UEBERSCHUSS') || label.includes('LIQUIDITAET')
const isTotalRow = label.includes('GESAMT') || label.includes('Bestandskunden gesamt') || label.includes('GESAMTUMSATZ') || label.includes('SUMME')
const isEditable = false // read-only for investors
const cat = row.category as string || ''
// Make category sum rows clickable (accordion)
const isCatHeader = activeSheet === 'betriebliche' && row.is_sum_row && cat !== 'summe' && cat !== 'personal' && cat !== 'abschreibungen'
const isCatOpen = openCats.has(cat)
// Balance rows show Dec value, flow rows show annual sum
const section = (row as Record<string, unknown>).section as string || ''
const isBalanceRow = label.includes('Kontostand') || label === 'LIQUIDITÄT' || label === 'LIQUIDITAET'
|| label.includes('Bestandskunden') || (activeSheet === 'kunden' && row.row_label === 'Bestandskunden')
|| label.includes('Anzahl Kunden') || section === 'quantity'
const isUnitPrice = section === 'unit_cost' || section === 'einkauf' || label.includes('Einkaufspreis')
|| section === 'price' || label.includes('Preis/Monat')
let annual = 0
if (isUnitPrice) {
// Unit prices: show the price, not a sum
annual = values[`m${monthEnd}`] || values[`m${monthStart}`] || 0
} else if (isBalanceRow) {
// Point-in-time: show last month (December) value
annual = values[`m${monthEnd}`] || 0
} else {
// Flow: sum all 12 months
for (let m = monthStart; m <= monthEnd; m++) annual += values[`m${m}`] || 0
}
return (
<tr
key={row.id}
className={`${isTotalRow ? 'border-t-2 border-t-white/20 border-b border-b-white/[0.03]' : 'border-b border-white/[0.03]'} ${isSumRow ? 'bg-white/[0.03]' : ''} hover:bg-white/[0.02]`}
>
<td className={`py-1 px-2 sticky left-0 bg-slate-900/90 backdrop-blur ${isSumRow ? 'font-bold text-white/80' : 'text-white/60'} ${isCatHeader ? 'cursor-pointer select-none' : ''}`}
onClick={isCatHeader ? () => toggleCat(cat) : undefined}
>
<div className="flex items-center gap-1">
{isCatHeader && <span className="text-[10px] text-indigo-400 w-3 shrink-0">{isCatOpen ? '▾' : '▸'}</span>}
{isEditable && <span className="w-1 h-1 rounded-full bg-indigo-400 flex-shrink-0" />}
<span className="truncate"><LabelWithTooltip label={label} /></span>
{row.position && <span className="text-white/50 ml-1">({row.position})</span>}
</div>
</td>
{yearOffset === -1 ? (
// All years view: show annual values per year
[2026, 2027, 2028, 2029, 2030].map(y => {
const yStart = (y - 2026) * 12 + 1
const yEnd = yStart + 11
let yVal = 0
if (isUnitPrice) {
yVal = values[`m${yEnd}`] || 0
} else if (isBalanceRow) {
yVal = values[`m${yEnd}`] || 0
} else {
for (let m = yStart; m <= yEnd; m++) yVal += values[`m${m}`] || 0
}
return (
<td key={y} className={`text-right py-1 px-3 ${yVal < 0 ? 'text-red-400' : yVal > 0 ? (isSumRow ? 'text-white/80' : 'text-white/50') : 'text-white/15'} ${isSumRow ? 'font-bold' : ''}`}>
{formatCell(Math.round(yVal))}
</td>
)
})
) : (
<>
<td className={`text-right py-1 px-2 font-medium ${annual < 0 ? 'text-red-400' : isSumRow ? 'text-white/80' : 'text-white/50'}`}>
{formatCell(annual)}
</td>
{Array.from({ length: 12 }, (_, idx) => {
const mKey = `m${monthStart + idx}`
const v = values[mKey] || 0
return (
<td
key={idx}
className={`text-right py-1 px-1.5 ${
v < 0 ? 'text-red-400/70' : v > 0 ? (isSumRow ? 'text-white/70' : 'text-white/50') : 'text-white/15'
} ${isEditable ? 'cursor-pointer hover:bg-indigo-500/10' : ''}`}
onDoubleClick={() => {
if (!isEditable) return
const input = prompt(`${label}${MONTH_LABELS[idx]} ${currentYear}`, String(v))
if (input !== null) handleCellEdit(row.id, mKey, input)
}}
>
{formatCell(v)}
</td>
)
})}
</>
)}
</tr>
)
})}
</tbody>
{/* Summenzeile für relevante Sheets */}
{['personalkosten', 'investitionen'].includes(activeSheet) && rows.length > 0 && (() => {
const nonSumRows = rows.filter(r => {
const l = getLabel(r)
return !(r.is_sum_row || l.includes('GESAMT') || l.includes('Summe') || l.includes('Gesamtkosten') || l === 'SUMME')
})
return (
<tfoot>
<tr className="border-t-2 border-white/20 bg-white/[0.05]">
<td className="py-1.5 px-2 sticky left-0 bg-slate-900/90 backdrop-blur font-bold text-white/80 text-xs">
{de ? 'SUMME' : 'TOTAL'}
</td>
{yearOffset === -1 ? (
[2026, 2027, 2028, 2029, 2030].map(y => {
const yStart = (y - 2026) * 12 + 1
const yEnd = yStart + 11
let yVal = 0
for (let m = yStart; m <= yEnd; m++) {
for (const row of nonSumRows) yVal += getValues(row)[`m${m}`] || 0
}
return (
<td key={y} className={`text-right py-1.5 px-3 font-bold text-xs ${yVal < 0 ? 'text-red-400' : 'text-white/80'}`}>
{formatCell(Math.round(yVal))}
</td>
)
})
) : (
<>
{(() => {
let sumAnnual = 0
for (let m = monthStart; m <= monthEnd; m++) {
for (const row of nonSumRows) sumAnnual += getValues(row)[`m${m}`] || 0
}
return (
<td className={`text-right py-1.5 px-2 font-bold text-xs ${sumAnnual < 0 ? 'text-red-400' : 'text-white/80'}`}>
{formatCell(sumAnnual)}
</td>
)
})()}
{Array.from({ length: 12 }, (_, idx) => {
const mKey = `m${monthStart + idx}`
let v = 0
for (const row of nonSumRows) v += getValues(row)[mKey] || 0
return (
<td key={idx} className={`text-right py-1.5 px-1.5 font-bold text-xs ${v < 0 ? 'text-red-400' : v > 0 ? 'text-white/70' : 'text-white/15'}`}>
{formatCell(v)}
</td>
)
})}
</>
)}
</tr>
</tfoot>
)
})()}
</table>
)}
</GlassCard>
)}
<ProjectionFooter lang={lang} />
</div>
)
}