Files
breakpilot-core/pitch-deck/components/slides/FinanzplanSlide.tsx
Benjamin Admin ec7326cfe1
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m6s
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 31s
CI / test-python-voice (push) Successful in 30s
CI / test-bqas (push) Successful in 32s
feat(pitch-deck): live-compute sums for Liquidität + Kunden + Umsatz tabs
Extended live-compute to ALL tabs:
- Liquidität: "Summe ERTRÄGE" = sum of einzahlung rows,
  "Summe AUSZAHLUNGEN" = sum of auszahlung rows
- Kunden: GESAMT rows = sum of tier detail rows
- Umsatz: GESAMTUMSATZ = sum of all revenue rows
- Materialaufwand: SUMME = sum of cost rows

ÜBERSCHUSS rows kept from DB (complex multi-step formula).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 10:26:23 +02:00

754 lines
38 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)': 'Enterprise-Kunden × 100 EUR/Mon',
'Internet/Mobilfunk (F)': 'Headcount gesamt × 50 EUR/Mon',
'Serverkosten Cloud (F)': 'Bestandskunden × 50 EUR + 300 EUR Basis',
'Berufsgenossenschaft (F)': '2,77% der Brutto-Lohnsumme (VBG IT)',
'Allgemeine Marketingkosten (F)': '10% vom Monatsumsatz',
'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 || {}
}
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 [selectedScenarioId, setSelectedScenarioId] = useState<string>('')
const [activeSheet, setActiveSheet] = useState<string>('guv')
const [rows, setRows] = useState<SheetRow[]>([])
const [loading, setLoading] = useState(false)
const [computing, setComputing] = useState(false)
const [yearOffset, setYearOffset] = useState(0) // 0=2026, 1=2027, ...
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
kpis[yk] = { revenue, ebit, personal, netIncome, steuern, liquiditaet, customers, headcount, mrr, arr, arpu, revPerEmp, ebitMargin, burnRate }
}
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])
// Compute
const handleCompute = async () => {
setComputing(true)
try {
await fetch('/api/finanzplan/compute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scenarioId: selectedScenarioId || undefined }) })
await loadSheet(activeSheet)
} catch { /* ignore */ }
setComputing(false)
}
// Cell edit
const handleCellEdit = async (rowId: number, monthKey: string, newValue: string) => {
const numVal = parseFloat(newValue.replace(/[^\d.-]/g, ''))
if (isNaN(numVal)) return
try {
await fetch(`/api/finanzplan/${activeSheet}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rowId, updates: { [monthKey]: numVal } }),
})
await loadSheet(activeSheet)
} catch { /* ignore */ }
}
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 · Monatliche Granularitaet · Editierbar' : '20262030 · Monthly Granularity · Editable'}</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 className="flex-1" />
<button
onClick={handleCompute}
disabled={computing}
className="flex items-center gap-1 px-3 py-1.5 text-[10px] bg-emerald-500/20 text-emerald-300 rounded-lg hover:bg-emerald-500/30 transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-3 h-3 ${computing ? 'animate-spin' : ''}`} />
{de ? 'Berechnen' : 'Compute'}
</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' && (
<div className="space-y-4">
{/* MRR + Kunden Chart */}
<GlassCard hover={false} className="p-4">
<h3 className="text-xs font-bold text-indigo-400 uppercase tracking-wider mb-3">{de ? 'MRR & Kundenentwicklung' : 'MRR & Customer Growth'}</h3>
<div className="grid grid-cols-5 gap-1 items-end h-48">
{(() => {
const years = ['y2026','y2027','y2028','y2029','y2030']
const data = years.map(y => ({ year: y.slice(1), mrr: fpKPIs[y]?.mrr || 0, cust: fpKPIs[y]?.customers || 0 }))
const maxMrr = Math.max(...data.map(d => d.mrr), 1)
const maxCust = Math.max(...data.map(d => d.cust), 1)
return data.map(d => ({ ...d, max_mrr: maxMrr, max_cust: maxCust }))
})().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: '160px' }}>
{/* MRR bar */}
<div className="w-8 bg-indigo-500/60 rounded-t transition-all" style={{ height: `${(d.mrr / d.max_mrr) * 150}px` }}>
<div className="text-[11px] text-white/80 text-center -mt-4 whitespace-nowrap font-semibold">
{d.mrr >= 100000 ? `${Math.round(d.mrr/1000)}k` : d.mrr.toLocaleString('de-DE')}
</div>
</div>
{/* Kunden bar */}
<div className="w-8 bg-emerald-500/60 rounded-t transition-all" style={{ height: `${(d.cust / d.max_cust) * 150}px` }}>
<div className="text-[7px] text-white/70 text-center -mt-3">{d.cust}</div>
</div>
</div>
<span className="text-[10px] text-white/40">{d.year}</span>
</div>
))}
</div>
<div className="flex justify-center gap-6 mt-2 text-[10px]">
<span className="flex items-center gap-1"><span className="w-3 h-2 bg-indigo-500/60 rounded inline-block" /> MRR ()</span>
<span className="flex items-center gap-1"><span className="w-3 h-2 bg-emerald-500/60 rounded inline-block" /> {de ? 'Kunden' : 'Customers'}</span>
</div>
</GlassCard>
{/* EBIT + Cash Chart */}
<div className="grid md:grid-cols-2 gap-4">
<GlassCard hover={false} className="p-4">
<h3 className="text-xs font-bold text-purple-400 uppercase tracking-wider mb-3">EBIT</h3>
<div className="grid grid-cols-5 gap-1 items-end h-36">
{(() => {
const years = ['y2026','y2027','y2028','y2029','y2030']
const data = years.map(y => ({ year: y.slice(1), ebit: fpKPIs[y]?.ebit || 0 }))
const maxAbs = Math.max(...data.map(d => Math.abs(d.ebit)), 1)
return data.map((k, idx) => {
const h = Math.abs(k.ebit) / maxAbs * 100
return (
<div key={idx} className="flex flex-col items-center">
<div className="w-10 flex flex-col justify-end" style={{ height: '110px' }}>
{k.ebit >= 0 ? (
<div className="bg-emerald-500/60 rounded-t w-full" style={{ height: `${h}px` }}>
<div className="text-[11px] text-emerald-300 text-center -mt-4 whitespace-nowrap font-semibold">{Math.round(k.ebit/1000)}k</div>
</div>
) : (
<div className="flex flex-col justify-end h-full">
<div className="bg-red-500/60 rounded-b w-full" style={{ height: `${h}px` }}>
<div className="text-[11px] text-red-300 text-center mt-1 whitespace-nowrap font-semibold">{Math.round(k.ebit/1000)}k</div>
</div>
</div>
)}
</div>
<span className="text-[10px] text-white/40 mt-1">{k.year}</span>
</div>
)
})
})()}
</div>
</GlassCard>
<GlassCard hover={false} className="p-4">
<h3 className="text-xs font-bold text-amber-400 uppercase tracking-wider mb-3">{de ? 'Personalaufbau' : 'Headcount'}</h3>
<div className="grid grid-cols-5 gap-1 items-end h-36">
{(() => {
const years = ['y2026','y2027','y2028','y2029','y2030']
const data = years.map(y => ({ year: y.slice(1), val: fpKPIs[y]?.headcount || 0 }))
const maxEmp = Math.max(...data.map(d => d.val), 1)
return data.map((d, idx) => (
<div key={idx} className="flex flex-col items-center">
<div className="w-10 flex flex-col justify-end" style={{ height: '110px' }}>
<div className="bg-amber-500/60 rounded-t w-full" style={{ height: `${(d.val / maxEmp) * 100}px` }}>
<div className="text-[11px] text-amber-300 text-center -mt-4 font-bold">{d.val}</div>
</div>
</div>
<span className="text-[10px] text-white/40 mt-1">{d.year}</span>
</div>
))
})()}
</div>
</GlassCard>
</div>
{/* Liquidität Chart */}
<GlassCard hover={false} className="p-4">
<h3 className="text-xs font-bold text-cyan-400 uppercase tracking-wider mb-3">{de ? 'Liquidität (Jahresende)' : 'Cash Position (Year-End)'}</h3>
<div className="grid grid-cols-5 gap-1 items-end h-36">
{(() => {
const years = ['y2026','y2027','y2028','y2029','y2030']
const data = years.map(y => ({ year: y.slice(1), val: fpKPIs[y]?.liquiditaet || 0 }))
const maxAbs = Math.max(...data.map(d => Math.abs(d.val)), 1)
return data.map((d, idx) => {
const h = Math.abs(d.val) / maxAbs * 100
const label = Math.abs(d.val) >= 1000000 ? `${(d.val/1000000).toFixed(1)}M` : `${Math.round(d.val/1000)}k`
return (
<div key={idx} className="flex flex-col items-center">
<div className="w-10 flex flex-col justify-end" style={{ height: '110px' }}>
<div className={`${d.val >= 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-[11px] ${d.val >= 0 ? 'text-cyan-300 -mt-4' : 'text-red-300 mt-1'} text-center whitespace-nowrap font-semibold`}>{label}</div>
</div>
</div>
<span className="text-[10px] text-white/40 mt-1">{d.year}</span>
</div>
)
})
})()}
</div>
</GlassCard>
{/* Umsatz vs. Kosten Chart */}
<GlassCard hover={false} className="p-4">
<h3 className="text-xs font-bold text-indigo-400 uppercase tracking-wider mb-3">{de ? 'Umsatz vs. Gesamtkosten' : 'Revenue vs. Total Costs'}</h3>
<div className="grid grid-cols-5 gap-1 items-end h-36">
{(() => {
const years = ['y2026','y2027','y2028','y2029','y2030']
const data = years.map(y => ({
year: y.slice(1),
rev: fpKPIs[y]?.revenue || 0,
costs: (fpKPIs[y]?.personal || 0) + Math.abs(fpKPIs[y]?.ebit || 0) + (fpKPIs[y]?.revenue || 0) - (fpKPIs[y]?.ebit || 0) - (fpKPIs[y]?.revenue || 0),
}))
// Simpler: costs = revenue - ebit
const data2 = years.map(y => ({
year: y.slice(1),
rev: fpKPIs[y]?.revenue || 0,
costs: (fpKPIs[y]?.revenue || 0) - (fpKPIs[y]?.ebit || 0),
}))
const maxVal = Math.max(...data2.map(d => Math.max(d.rev, d.costs)), 1)
return data2.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: '100px' }}>
<div className="w-6 bg-indigo-500/60 rounded-t" style={{ height: `${(d.rev / maxVal) * 90}px` }}>
<div className="text-[10px] text-indigo-300 text-center -mt-3 whitespace-nowrap font-semibold">{d.rev >= 1000000 ? `${(d.rev/1000000).toFixed(1)}M` : `${Math.round(d.rev/1000)}k`}</div>
</div>
<div className="w-6 bg-red-500/40 rounded-t" style={{ height: `${(d.costs / maxVal) * 90}px` }}>
<div className="text-[10px] text-red-300 text-center -mt-3 whitespace-nowrap font-semibold">{d.costs >= 1000000 ? `${(d.costs/1000000).toFixed(1)}M` : `${Math.round(d.costs/1000)}k`}</div>
</div>
</div>
<span className="text-[10px] text-white/40">{d.year}</span>
</div>
))
})()}
</div>
<div className="flex justify-center gap-6 mt-2 text-[10px]">
<span className="flex items-center gap-1"><span className="w-3 h-2 bg-indigo-500/60 rounded inline-block" /> {de ? 'Umsatz' : 'Revenue'}</span>
<span className="flex items-center gap-1"><span className="w-3 h-2 bg-red-500/40 rounded inline-block" /> {de ? 'Kosten' : 'Costs'}</span>
</div>
</GlassCard>
</div>
)}
{/* Year Navigation — not for GuV, KPIs, Charts (annual views) */}
{!['guv', 'kpis', 'charts'].includes(activeSheet) && (
<div className="flex items-center justify-center gap-4 mb-2">
<button onClick={() => setYearOffset(Math.max(0, yearOffset - 1))} disabled={yearOffset === 0}
className="p-1 text-white/40 hover:text-white/70 disabled:opacity-20">
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-sm font-bold text-white">{currentYear}</span>
<button onClick={() => setYearOffset(Math.min(4, yearOffset + 1))} disabled={yearOffset === 4}
className="p-1 text-white/40 hover:text-white/70 disabled:opacity-20">
<ChevronRight className="w-4 h-4" />
</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 isSumRow = row.is_sum_row || label.includes('EBIT') || label.includes('Summe') || label.includes('Rohergebnis') || label.includes('Gesamtleistung') || label.includes('Jahresüberschuss') || label.includes('Ergebnis')
return (
<tr key={row.id} className={`border-b border-white/[0.03] ${isSumRow ? 'bg-white/[0.03]' : ''} hover:bg-white/[0.02]`}>
<td className={`py-1.5 px-2 sticky left-0 bg-slate-900/90 backdrop-blur ${isSumRow ? 'font-bold 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 ? (isSumRow ? 'text-white/80' : 'text-white/50') : 'text-white/15'} ${isSumRow ? 'font-bold' : ''}`}>
{v === 0 ? '—' : Math.round(v).toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
) : (
/* === Monthly 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>
<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')) {
sourceRows = rows.filter(r => !r.is_sum_row && !getLabel(r).includes('Summe') && !getLabel(r).includes('SUMME') && (r.category as string) !== '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
}
// === Kunden/Umsatz GESAMT rows ===
else if (label.includes('GESAMT') || label.includes('Bestandskunden gesamt') || label.includes('GESAMTUMSATZ') || label.includes('SUMME Material')) {
// Sum all non-sum rows
sourceRows = rows.filter(r => {
const rLabel = getLabel(r)
return !rLabel.includes('GESAMT') && !rLabel.includes('Summe') && !rLabel.includes('SUMME') && !rLabel.includes('Bestandskunden gesamt') && !rLabel.includes('GESAMTUMSATZ')
})
}
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
})().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 = row.is_editable
// Balance rows show Dec value, flow rows show annual sum
const isBalanceRow = label.includes('Kontostand') || label === 'LIQUIDITÄT' || label === 'LIQUIDITAET'
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'}`}>
<div className="flex items-center gap-1">
{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>}
{row.section && <span className="text-white/50 ml-1">[{row.section}]</span>}
</div>
</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', 'betriebliche', 'investitionen', 'sonst_ertraege'].includes(activeSheet) && rows.length > 0 && (() => {
// Berechne Summe über alle Zeilen die keine Summenzeilen sind
const sumValues: Record<string, number> = {}
let sumAnnual = 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')
})
for (let idx = 0; idx < 12; idx++) {
const mKey = `m${monthStart + idx}`
let colSum = 0
for (const row of nonSumRows) {
const v = getValues(row)
colSum += v[mKey] || 0
}
sumValues[mKey] = colSum
sumAnnual += colSum
}
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>
<td className={`text-right py-1.5 px-2 font-bold text-xs ${sumAnnual < 0 ? 'text-red-400' : 'text-white/80'}`}>
{['kunden', 'kunden_summary'].includes(activeSheet)
? formatCell(sumValues[`m${monthEnd}`] || 0)
: formatCell(sumAnnual)
}
</td>
{Array.from({ length: 12 }, (_, idx) => {
const mKey = `m${monthStart + idx}`
const v = sumValues[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>
)
}