Files
breakpilot-core/pitch-deck/components/slides/FinanzplanSlide.tsx
Benjamin Admin fa6b0a241d
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m29s
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 42s
CI / test-python-voice (push) Successful in 33s
CI / test-bqas (push) Successful in 34s
fix: move chartDetail useState to component top level (hooks rule)
2026-04-22 00:00:54 +02:00

911 lines
53 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',
'Serverkosten Cloud (F)': '2.000 EUR Basis (SysEleven) + (Kunden - 10) × 250 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 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
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 burnRate = liquiditaet < 0 ? Math.round(Math.abs(ebit / 12)) : 0
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
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') {
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 },
].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', 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: 'ARPU (MRR/Kunden)', values: years.map(y => v(y, 'arpu')), 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: '€', 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 — der wichtigste KPI für SaaS-Unternehmen. Zeigt die planbaren monatlichen Einnahmen aus laufenden Kundenverträgen. Wachsender MRR = wachsendes Geschäft.' : 'Monthly recurring revenue — the most important KPI for SaaS companies. Shows predictable monthly income from active customer contracts. Growing MRR = growing business.' },
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: 'NRR (Net Revenue Retention)', desc: de ? 'Umsatzwachstum durch Bestandskunden — zeigt ob bestehende Kunden mehr ausgeben als sie kündigen. NRR > 100% bedeutet: das Geschäft wächst auch ohne Neukunden.' : 'Revenue growth from existing customers — shows if current customers spend more than they churn. NRR > 100% means: the business grows even without new customers.' },
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 key = chartDetail === 'cash' ? 'liquiditaet' : chartDetail === 'revcost' ? 'revenue' : 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: 'NRR', 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>
)
})()}
{/* Year Navigation — not for GuV, KPIs, Charts */}
{!['guv', 'kpis', 'charts'].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'].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 }
})
return computedRows
})().filter(row => {
// For betriebliche: hide detail rows if their category is collapsed
if (activeSheet !== 'betriebliche') return true
const cat = row.category as string || ''
const label = getLabel(row)
// Always show: sum rows, Personalkosten, Abschreibungen, category="summe"
if (row.is_sum_row || cat === 'summe' || cat === 'personal' || cat === 'abschreibungen') return true
if (label === 'Personalkosten' || label === 'Abschreibungen') return true
// Detail rows: only show if category is open
return openCats.has(cat)
}).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 isBalanceRow = label.includes('Kontostand') || label === 'LIQUIDITÄT' || label === 'LIQUIDITAET'
|| label.includes('Bestandskunden') || (activeSheet === 'kunden' && row.row_label === 'Bestandskunden')
const isUnitPrice = (row as Record<string, unknown>).section === 'unit_cost' || (row as Record<string, unknown>).section === 'einkauf' || label.includes('Einkaufspreis')
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>
)
}