- Mock-Daten und Szenario-Toggle entfernt - Lädt automatisch Finanzplan-Daten beim Öffnen - KPIs: ARR 2030, Mitarbeiter 2030, Break-Even Jahr, Cash Ende - Übersicht: Revenue vs. Costs Chart + Waterfall + Cashflow - GuV: Direkt aus fp_guv DB-Tabelle (keine Mocks) - Cashflow: AnnualCashflowChart mit 1M InitialFunding - Keine Slider/Szenario-Sidebar mehr (nicht relevant) - Umlaute korrigiert (Übersicht statt Uebersicht) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
259 lines
10 KiB
TypeScript
259 lines
10 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { Language, FMComputeResponse } from '@/lib/types'
|
|
import { t } from '@/lib/i18n'
|
|
import GradientText from '../ui/GradientText'
|
|
import FadeInView from '../ui/FadeInView'
|
|
import FinancialChart from '../ui/FinancialChart'
|
|
import KPICard from '../ui/KPICard'
|
|
import WaterfallChart from '../ui/WaterfallChart'
|
|
import AnnualCashflowChart from '../ui/AnnualCashflowChart'
|
|
|
|
type FinTab = 'overview' | 'guv' | 'cashflow'
|
|
|
|
interface FinancialsSlideProps {
|
|
lang: Language
|
|
}
|
|
|
|
export default function FinancialsSlide({ lang }: FinancialsSlideProps) {
|
|
const i = t(lang)
|
|
const [activeTab, setActiveTab] = useState<FinTab>('overview')
|
|
const [data, setData] = useState<FMComputeResponse | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [guv, setGuv] = useState<any[]>([])
|
|
const de = lang === 'de'
|
|
|
|
// Auto-load Finanzplan data
|
|
useEffect(() => {
|
|
async function load() {
|
|
try {
|
|
// Compute Finanzplan and get FMResult format
|
|
const res = await fetch('/api/financial-model/compute', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ source: 'finanzplan' }),
|
|
})
|
|
if (res.ok) {
|
|
const d = await res.json()
|
|
setData(d)
|
|
}
|
|
// Load GuV separately
|
|
const guvRes = await fetch('/api/finanzplan/guv')
|
|
if (guvRes.ok) {
|
|
const guvData = await guvRes.json()
|
|
setGuv(guvData.rows || [])
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load Finanzplan:', e)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
}, [])
|
|
|
|
const summary = data?.summary
|
|
const results = data?.results || []
|
|
const lastResult = results.length > 0 ? results[results.length - 1] : null
|
|
|
|
// Find break-even month (first month where revenue > costs)
|
|
const breakEvenMonth = results.findIndex(r => r.revenue_eur > r.total_costs_eur && r.revenue_eur > 0)
|
|
const breakEvenYear = breakEvenMonth >= 0 ? results[breakEvenMonth]?.year : null
|
|
|
|
const tabs: { id: FinTab; label: string }[] = [
|
|
{ id: 'overview', label: de ? 'Übersicht' : 'Overview' },
|
|
{ id: 'guv', label: de ? 'GuV (Jahres)' : 'P&L (Annual)' },
|
|
{ id: 'cashflow', label: de ? 'Cashflow' : 'Cash Flow' },
|
|
]
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="w-8 h-8 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto px-4">
|
|
<FadeInView className="text-center mb-3">
|
|
<h2 className="text-3xl md:text-4xl font-bold mb-1">
|
|
<GradientText>{i.financials.title}</GradientText>
|
|
</h2>
|
|
<p className="text-sm text-white/50 max-w-2xl mx-auto">{i.financials.subtitle}</p>
|
|
</FadeInView>
|
|
|
|
{/* Hero KPI Cards */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-3">
|
|
<KPICard
|
|
label="ARR 2030"
|
|
value={lastResult ? Math.round(lastResult.arr_eur / 1_000_000 * 10) / 10 : 0}
|
|
suffix=" Mio."
|
|
decimals={1}
|
|
trend="up"
|
|
color="#6366f1"
|
|
delay={0.1}
|
|
subLabel="EUR"
|
|
/>
|
|
<KPICard
|
|
label={de ? 'Mitarbeiter 2030' : 'Employees 2030'}
|
|
value={lastResult?.employees_count || 0}
|
|
trend="up"
|
|
color="#22c55e"
|
|
delay={0.15}
|
|
/>
|
|
<KPICard
|
|
label="Break-Even"
|
|
value={breakEvenYear || 0}
|
|
trend={breakEvenMonth >= 0 ? 'up' : 'neutral'}
|
|
color="#eab308"
|
|
delay={0.2}
|
|
subLabel={breakEvenMonth >= 0 ? `${de ? 'Monat' : 'Month'} ${breakEvenMonth + 1}` : ''}
|
|
/>
|
|
<KPICard
|
|
label={de ? 'Cash Ende 2030' : 'Cash End 2030'}
|
|
value={lastResult ? Math.round(lastResult.cash_balance_eur / 1_000_000 * 10) / 10 : 0}
|
|
suffix=" Mio."
|
|
decimals={1}
|
|
trend={(lastResult?.cash_balance_eur || 0) > 0 ? 'up' : 'down'}
|
|
color="#a855f7"
|
|
delay={0.25}
|
|
subLabel="EUR"
|
|
/>
|
|
</div>
|
|
|
|
{/* Tab Navigation */}
|
|
<div className="flex items-center gap-1 mb-3">
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`px-3 py-1.5 rounded-lg text-xs transition-all
|
|
${activeTab === tab.id
|
|
? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30'
|
|
: 'bg-white/[0.04] text-white/40 border border-transparent hover:text-white/60 hover:bg-white/[0.06]'
|
|
}`}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* TAB: Overview — monatlicher Chart */}
|
|
{activeTab === 'overview' && (
|
|
<div className="space-y-3">
|
|
<FadeInView delay={0.1}>
|
|
<div className="bg-white/[0.05] backdrop-blur-xl border border-white/10 rounded-2xl p-3">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<p className="text-xs text-white/40">
|
|
{de ? 'Umsatz vs. Kosten (60 Monate)' : 'Revenue vs. Costs (60 months)'}
|
|
</p>
|
|
<div className="flex items-center gap-3 text-[9px]">
|
|
<span className="flex items-center gap-1"><span className="w-2 h-0.5 bg-indigo-500 inline-block" /> {de ? 'Umsatz' : 'Revenue'}</span>
|
|
<span className="flex items-center gap-1"><span className="w-2 h-0.5 bg-red-400 inline-block" /> {de ? 'Kosten' : 'Costs'}</span>
|
|
<span className="flex items-center gap-1"><span className="w-2 h-0.5 bg-emerald-500 inline-block" /> Cash</span>
|
|
</div>
|
|
</div>
|
|
<FinancialChart
|
|
activeResults={data}
|
|
compareResults={new Map()}
|
|
compareMode={false}
|
|
scenarioColors={{}}
|
|
lang={lang}
|
|
/>
|
|
</div>
|
|
</FadeInView>
|
|
|
|
<div className="grid md:grid-cols-2 gap-3">
|
|
<FadeInView delay={0.2}>
|
|
<div className="bg-white/[0.05] backdrop-blur-xl border border-white/10 rounded-2xl p-3">
|
|
<p className="text-xs text-white/40 mb-2">
|
|
{de ? 'Cash-Flow (Quartal)' : 'Cash Flow (Quarterly)'}
|
|
</p>
|
|
{data && <WaterfallChart results={data.results} lang={lang} />}
|
|
</div>
|
|
</FadeInView>
|
|
|
|
<FadeInView delay={0.25}>
|
|
<div className="bg-white/[0.05] backdrop-blur-xl border border-white/10 rounded-2xl p-3">
|
|
<p className="text-xs text-white/40 mb-2">
|
|
{de ? 'Jährlicher Cashflow' : 'Annual Cash Flow'}
|
|
</p>
|
|
{data && (
|
|
<AnnualCashflowChart
|
|
results={data.results}
|
|
initialFunding={1000000}
|
|
lang={lang}
|
|
/>
|
|
)}
|
|
</div>
|
|
</FadeInView>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB: GuV — aus Finanzplan DB */}
|
|
{activeTab === 'guv' && (
|
|
<FadeInView delay={0.1}>
|
|
<div className="bg-white/[0.05] backdrop-blur-xl border border-white/10 rounded-2xl p-4">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<p className="text-xs text-white/40">
|
|
{de ? 'Gewinn- und Verlustrechnung (5 Jahre)' : 'Profit & Loss Statement (5 Years)'}
|
|
</p>
|
|
<p className="text-[9px] text-white/20">{de ? 'Alle Werte in EUR' : 'All values in EUR'}</p>
|
|
</div>
|
|
<table className="w-full text-[11px]">
|
|
<thead>
|
|
<tr className="border-b border-white/10">
|
|
<th className="text-left py-2 px-2 text-white/40 font-medium min-w-[200px]">{de ? 'Position' : 'Item'}</th>
|
|
{[2026, 2027, 2028, 2029, 2030].map(y => (
|
|
<th key={y} className="text-right py-2 px-3 text-white/40 font-medium">{y}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{guv.map((row: any) => {
|
|
const label = row.row_label || ''
|
|
const values = row.values || {}
|
|
const isBold = 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] ${isBold ? 'bg-white/[0.03]' : ''}`}>
|
|
<td className={`py-1.5 px-2 ${isBold ? 'font-bold text-white/80' : 'text-white/60'}`}>{label}</td>
|
|
{[2026, 2027, 2028, 2029, 2030].map(y => {
|
|
const v = Math.round(values[`y${y}`] || 0)
|
|
return (
|
|
<td key={y} className={`text-right py-1.5 px-3 ${v < 0 ? 'text-red-400' : v > 0 ? (isBold ? 'text-white/80' : 'text-white/50') : 'text-white/15'} ${isBold ? 'font-bold' : ''}`}>
|
|
{v === 0 ? '—' : v.toLocaleString('de-DE')}
|
|
</td>
|
|
)
|
|
})}
|
|
</tr>
|
|
)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</FadeInView>
|
|
)}
|
|
|
|
{/* TAB: Cashflow */}
|
|
{activeTab === 'cashflow' && data && (
|
|
<FadeInView delay={0.1}>
|
|
<div className="bg-white/[0.05] backdrop-blur-xl border border-white/10 rounded-2xl p-4">
|
|
<p className="text-xs text-white/40 mb-3">
|
|
{de ? 'Jährlicher Cashflow & Liquiditätsentwicklung' : 'Annual Cash Flow & Liquidity Development'}
|
|
</p>
|
|
<AnnualCashflowChart
|
|
results={data.results}
|
|
initialFunding={1000000}
|
|
lang={lang}
|
|
/>
|
|
</div>
|
|
</FadeInView>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|