Pool-Size von 5 auf 20 erhöht (Connection-Exhaustion bei parallelen Finanzplan-Queries + Compute + API-Calls) KPIs/Charts Tabs laden keine DB-Daten (virtual tabs, Daten sind hardcoded) → sofortiges Rendering Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
524 lines
25 KiB
TypeScript
524 lines
25 KiB
TypeScript
'use client'
|
||
|
||
import { useCallback, useEffect, useState } from 'react'
|
||
import { Language } from '@/lib/types'
|
||
import { t } from '@/lib/i18n'
|
||
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
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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 || '—'
|
||
}
|
||
|
||
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 })
|
||
}
|
||
|
||
export default function FinanzplanSlide({ lang }: FinanzplanSlideProps) {
|
||
const [sheets, setSheets] = useState<SheetMeta[]>([])
|
||
const [activeSheet, setActiveSheet] = useState<string>('personalkosten')
|
||
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'
|
||
|
||
// Load sheet list
|
||
useEffect(() => {
|
||
fetch('/api/finanzplan')
|
||
.then(r => r.json())
|
||
.then(data => setSheets(data.sheets || []))
|
||
.catch(() => {})
|
||
}, [])
|
||
|
||
// 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}`)
|
||
const data = await r.json()
|
||
setRows(data.rows || [])
|
||
} catch { /* ignore */ }
|
||
setLoading(false)
|
||
}, [])
|
||
|
||
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: '{}' })
|
||
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 ? '2026–2030 · Monatliche Granularitaet · Editierbar' : '2026–2030 · 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>
|
||
{(() => {
|
||
// Compute KPIs from loaded data — we need liquidität and umsatz data
|
||
// These are approximate since we don't have all sheets loaded simultaneously
|
||
const kpiRows = [
|
||
{ label: 'MRR (Dez)', values: [6100, 84450, 267950, 517650, 834750], unit: '€', bold: true },
|
||
{ label: 'ARR', values: [73200, 1013400, 3215400, 6211800, 10017000], unit: '€', bold: true },
|
||
{ label: de ? 'Kunden (Dez)' : 'Customers (Dec)', values: [14, 117, 370, 726, 1200], unit: '', bold: false },
|
||
{ label: 'ARPU (MRR/Kunden)', values: [436, 722, 724, 713, 696], unit: '€', bold: false },
|
||
{ label: de ? 'Mitarbeiter' : 'Employees', values: [5, 10, 17, 25, 35], unit: '', bold: false },
|
||
{ label: de ? 'Umsatz/Mitarbeiter' : 'Revenue/Employee', values: [14640, 101340, 189141, 248472, 286200], unit: '€', bold: false },
|
||
{ label: de ? 'Personalkosten' : 'Personnel Costs', values: [58768, 740968, 1353764, 2154301, 3129479], unit: '€', bold: false },
|
||
{ label: 'EBIT', values: [-95099, -566293, -4019, 1315689, 3144137], unit: '€', bold: true },
|
||
{ label: de ? 'EBIT-Marge' : 'EBIT Margin', values: [-130, -56, -1, 21, 31], unit: '%', bold: false },
|
||
{ label: de ? 'Steuern' : 'Taxes', values: [0, 0, 0, 182565, 882717], unit: '€', bold: false },
|
||
{ label: de ? 'Jahresüberschuss' : 'Net Income', values: [-95099, -566293, -4019, 1133124, 2261420], unit: '€', bold: true },
|
||
{ label: de ? 'Serverkosten/Kunde' : 'Server Cost/Customer', values: [100, 100, 100, 100, 100], unit: '€', bold: false },
|
||
{ label: de ? 'Bruttomarge' : 'Gross Margin', values: [100, 100, 92, 90, 88], unit: '%', bold: false },
|
||
{ label: 'Burn Rate (Dez)', values: [44734, 28364, 0, 0, 0], unit: '€/Mo', bold: false },
|
||
{ label: de ? 'Runway (Monate)' : 'Runway (months)', values: [19, 4, '∞', '∞', '∞'], unit: '', 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">
|
||
{[
|
||
{ year: '2026', mrr: 6100, cust: 14, max_mrr: 834750, max_cust: 1200 },
|
||
{ year: '2027', mrr: 84450, cust: 117, max_mrr: 834750, max_cust: 1200 },
|
||
{ year: '2028', mrr: 267950, cust: 370, max_mrr: 834750, max_cust: 1200 },
|
||
{ year: '2029', mrr: 517650, cust: 726, max_mrr: 834750, max_cust: 1200 },
|
||
{ year: '2030', mrr: 834750, cust: 1200, max_mrr: 834750, max_cust: 1200 },
|
||
].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-[7px] text-white/70 text-center -mt-3 whitespace-nowrap">
|
||
{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">
|
||
{[
|
||
{ year: '2026', val: -95099 },
|
||
{ year: '2027', val: -566293 },
|
||
{ year: '2028', val: -4019 },
|
||
{ year: '2029', val: 1315689 },
|
||
{ year: '2030', val: 3144137 },
|
||
].map((d, idx) => {
|
||
const maxAbs = 3144137
|
||
const h = Math.abs(d.val) / maxAbs * 100
|
||
return (
|
||
<div key={idx} className="flex flex-col items-center">
|
||
<div className="w-10 flex flex-col justify-end" style={{ height: '110px' }}>
|
||
{d.val >= 0 ? (
|
||
<div className="bg-emerald-500/60 rounded-t w-full" style={{ height: `${h}px` }}>
|
||
<div className="text-[7px] text-emerald-300 text-center -mt-3 whitespace-nowrap">{Math.round(d.val/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-[7px] text-red-300 text-center mt-1 whitespace-nowrap">{Math.round(d.val/1000)}k</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<span className="text-[10px] text-white/40 mt-1">{d.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">
|
||
{[
|
||
{ year: '2026', val: 5 },
|
||
{ year: '2027', val: 10 },
|
||
{ year: '2028', val: 17 },
|
||
{ year: '2029', val: 25 },
|
||
{ year: '2030', val: 35 },
|
||
].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 / 35) * 100}px` }}>
|
||
<div className="text-[8px] text-amber-300 text-center -mt-3 font-bold">{d.val}</div>
|
||
</div>
|
||
</div>
|
||
<span className="text-[10px] text-white/40 mt-1">{d.year}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</GlassCard>
|
||
</div>
|
||
</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('Jahresueberschuss') || 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'}`}>
|
||
{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>
|
||
{rows.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 isEditable = row.is_editable
|
||
// Balance rows show Dec value, flow rows show annual sum
|
||
const isBalanceRow = label.includes('Kontostand') || label === 'LIQUIDITÄT' || label === 'LIQUIDITAET'
|
||
|
||
let annual = 0
|
||
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={`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">{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', 'materialaufwand', 'betriebliche', 'investitionen', 'sonst_ertraege', 'umsatzerloese', 'kunden', 'kunden_summary'].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>
|
||
)}
|
||
|
||
<p className="text-center text-[9px] text-white/40 mt-2">
|
||
{de
|
||
? 'Doppelklick auf blaue Zellen zum Bearbeiten · Gründung: 01.08.2026'
|
||
: 'Double-click blue cells to edit · Founding: 01.08.2026'}
|
||
</p>
|
||
</div>
|
||
)
|
||
}
|