'use client' import { useState, useEffect } from 'react' import { createPortal } from 'react-dom' import { motion } from 'framer-motion' import { FMResult } from '@/lib/types' import { Maximize2, Minimize2, ChevronDown, ChevronRight } from 'lucide-react' interface AnnualPLTableProps { results: FMResult[] lang: 'de' | 'en' } type AccountingStandard = 'hgb' | 'usgaap' interface AnnualRow { year: number revenue: number cogs: number grossProfit: number grossMarginPct: number personnel: number marketing: number infra: number totalOpex: number ebitda: number ebitdaMarginPct: number customers: number employees: number } interface MonthlyRow { month: number monthInYear: number revenue: number cogs: number grossProfit: number grossMarginPct: number personnel: number marketing: number infra: number totalCosts: number ebitda: number ebitdaMarginPct: number customers: number employees: number mrr: number cashBalance: number } function fmt(v: number): string { if (Math.abs(v) >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M` if (Math.abs(v) >= 1_000) return `${(v / 1_000).toFixed(0)}k` return Math.round(v).toLocaleString('de-DE') } function fmtMonth(v: number): string { return Math.round(v).toLocaleString('de-DE') } const MONTH_NAMES_DE = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'] const MONTH_NAMES_EN = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] function getLineItems(lang: 'de' | 'en', standard: AccountingStandard) { const de = lang === 'de' const hgb = standard === 'hgb' return [ { label: hgb ? (de ? 'Umsatzerloese' : 'Revenue (Umsatzerloese)') : (de ? 'Revenue' : 'Revenue'), key: 'revenue' as keyof AnnualRow, monthKey: 'revenue' as keyof MonthlyRow, isBold: true, }, { label: hgb ? (de ? '- Herstellungskosten' : '- Cost of Production') : (de ? '- COGS' : '- Cost of Goods Sold'), key: 'cogs' as keyof AnnualRow, monthKey: 'cogs' as keyof MonthlyRow, isNegative: true, }, { label: hgb ? (de ? '= Rohertrag' : '= Gross Profit') : (de ? '= Gross Profit' : '= Gross Profit'), key: 'grossProfit' as keyof AnnualRow, monthKey: 'grossProfit' as keyof MonthlyRow, isBold: true, isSeparator: true, }, { label: hgb ? (de ? ' Rohertragsmarge' : ' Gross Margin') : (de ? ' Gross Margin' : ' Gross Margin'), key: 'grossMarginPct' as keyof AnnualRow, monthKey: 'grossMarginPct' as keyof MonthlyRow, isPercent: true, }, { label: hgb ? (de ? '- Personalaufwand' : '- Personnel Expenses') : (de ? '- Personnel' : '- Personnel'), key: 'personnel' as keyof AnnualRow, monthKey: 'personnel' as keyof MonthlyRow, isNegative: true, }, { label: hgb ? (de ? '- Vertrieb & Marketing' : '- Sales & Marketing') : (de ? '- Sales & Marketing' : '- Sales & Marketing'), key: 'marketing' as keyof AnnualRow, monthKey: 'marketing' as keyof MonthlyRow, isNegative: true, }, { label: hgb ? (de ? '- sonstige betriebl. Aufwendungen' : '- Other Operating Expenses') : (de ? '- Infrastructure' : '- Infrastructure'), key: 'infra' as keyof AnnualRow, monthKey: 'infra' as keyof MonthlyRow, isNegative: true, }, { label: hgb ? (de ? '= Betriebsaufwand gesamt' : '= Total Operating Expenses') : (de ? '= Total OpEx' : '= Total OpEx'), key: 'totalOpex' as keyof AnnualRow, monthKey: 'totalCosts' as keyof MonthlyRow, isBold: true, isSeparator: true, isNegative: true, }, { label: hgb ? (de ? 'Betriebsergebnis (EBITDA)' : 'Operating Result (EBITDA)') : 'EBITDA', key: 'ebitda' as keyof AnnualRow, monthKey: 'ebitda' as keyof MonthlyRow, isBold: true, isSeparator: true, }, { label: hgb ? (de ? ' EBITDA-Marge' : ' EBITDA Margin') : (de ? ' EBITDA Margin' : ' EBITDA Margin'), key: 'ebitdaMarginPct' as keyof AnnualRow, monthKey: 'ebitdaMarginPct' as keyof MonthlyRow, isPercent: true, }, { label: hgb ? (de ? 'Kunden (Stichtag)' : 'Customers (Reporting Date)') : (de ? 'Kunden (Jahresende)' : 'Customers (Year End)'), key: 'customers' as keyof AnnualRow, monthKey: 'customers' as keyof MonthlyRow, }, { label: hgb ? (de ? 'Mitarbeiter (VZAe)' : 'Employees (FTE)') : (de ? 'Mitarbeiter' : 'Employees'), key: 'employees' as keyof AnnualRow, monthKey: 'employees' as keyof MonthlyRow, }, ] } function AnnualTable({ rows, lang, expandedYear, onToggleYear, monthlyData, isFullscreen, standard, }: { rows: AnnualRow[] lang: 'de' | 'en' expandedYear: number | null onToggleYear: (year: number) => void monthlyData: Map isFullscreen: boolean standard: AccountingStandard }) { const de = lang === 'de' const monthNames = de ? MONTH_NAMES_DE : MONTH_NAMES_EN const lineItems = getLineItems(lang, standard) const monthlyExtraItems: { label: string; key: keyof MonthlyRow; isBold?: boolean }[] = isFullscreen ? [ { label: 'MRR', key: 'mrr', isBold: true }, { label: de ? 'Cash-Bestand' : 'Cash Balance', key: 'cashBalance', isBold: true }, ] : [] const textSize = isFullscreen ? 'text-xs' : 'text-[11px]' const minColWidth = isFullscreen ? 'min-w-[70px]' : 'min-w-[80px]' return ( {rows.map(r => ( ))} {lineItems.map((item) => ( {rows.map(r => { const val = r[item.key] as number return ( ) })} ))} {/* Monthly Drill-Down */} {expandedYear && monthlyData.has(expandedYear) && ( {monthlyData.get(expandedYear)!.map(m => ( ))} {lineItems.map((item) => { const mKey = item.monthKey if (!mKey) return null return ( {monthlyData.get(expandedYear)!.map(m => { const val = m[mKey] as number return ( ) })} ) })} {monthlyExtraItems.map((item) => ( {monthlyData.get(expandedYear)!.map(m => { const val = m[item.key] as number return ( ) })} ))} )}
{standard === 'hgb' ? (de ? 'GuV-Position (HGB)' : 'P&L Line Item (HGB)') : (de ? 'GuV-Position (US GAAP)' : 'P&L Line Item (US GAAP)') } onToggleYear(r.year)} title={de ? 'Klicken fuer Monatsdetails' : 'Click for monthly details'} > {expandedYear === r.year ? ( ) : ( )} {r.year}
{item.label} 0 && item.key === 'ebitda' ? 'text-emerald-400' : ''} ${!item.isPercent && !item.isBold ? 'text-white/60' : 'text-white'} `} > {item.isPercent ? `${val.toFixed(1)}%` : (item.isNegative && val > 0 ? '-' : '') + fmt(Math.abs(val)) }

{de ? `Monatsdetails ${expandedYear}` : `Monthly Details ${expandedYear}`}

{de ? 'Monat' : 'Month'} {monthNames[m.monthInYear - 1]}
{item.label} 0 && item.key === 'ebitda' ? 'text-emerald-400/70' : ''} ${!item.isPercent ? 'text-white/40' : ''} `} > {item.isPercent ? `${val.toFixed(0)}%` : (item.isNegative && val > 0 ? '-' : '') + fmtMonth(Math.abs(val)) }
{item.label} {fmtMonth(Math.round(val))}
) } function FullscreenOverlay({ children, onClose, lang, standard, onStandardChange, }: { children: React.ReactNode onClose: () => void lang: 'de' | 'en' standard: AccountingStandard onStandardChange: (s: AccountingStandard) => void }) { const de = lang === 'de' // ESC to close useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) }, [onClose]) return (

{de ? 'Gewinn- und Verlustrechnung' : 'Profit & Loss Statement'}

{de ? 'Klicke auf ein Jahr um die Monatsdetails zu sehen · ESC zum Schliessen' : 'Click on a year to see monthly details · ESC to close'}

{/* HGB / US GAAP Toggle */}
{children}
) } export default function AnnualPLTable({ results, lang }: AnnualPLTableProps) { const [isFullscreen, setIsFullscreen] = useState(false) const [expandedYear, setExpandedYear] = useState(null) const [standard, setStandard] = useState('hgb') const [portalRoot, setPortalRoot] = useState(null) const de = lang === 'de' // Portal mount point useEffect(() => { setPortalRoot(document.body) }, []) // Aggregate monthly results into annual const annualMap = new Map() for (const r of results) { if (!annualMap.has(r.year)) annualMap.set(r.year, []) annualMap.get(r.year)!.push(r) } const rows: AnnualRow[] = Array.from(annualMap.entries()).map(([year, months]) => { const revenue = months.reduce((s, m) => s + m.revenue_eur, 0) const cogs = months.reduce((s, m) => s + m.cogs_eur, 0) const grossProfit = revenue - cogs const personnel = months.reduce((s, m) => s + m.personnel_eur, 0) const marketing = months.reduce((s, m) => s + m.marketing_eur, 0) const infra = months.reduce((s, m) => s + m.infra_eur, 0) const totalOpex = personnel + marketing + infra const ebitda = grossProfit - totalOpex const lastMonth = months[months.length - 1] return { year, revenue, cogs, grossProfit, grossMarginPct: revenue > 0 ? (grossProfit / revenue) * 100 : 0, personnel, marketing, infra, totalOpex, ebitda, ebitdaMarginPct: revenue > 0 ? (ebitda / revenue) * 100 : 0, customers: lastMonth.total_customers, employees: lastMonth.employees_count, } }) // Build monthly data for drill-down const monthlyData = new Map() for (const [year, months] of annualMap.entries()) { monthlyData.set(year, months.map(m => { const grossProfit = m.revenue_eur - m.cogs_eur const totalCosts = m.personnel_eur + m.marketing_eur + m.infra_eur const ebitda = grossProfit - totalCosts return { month: m.month, monthInYear: m.month_in_year, revenue: m.revenue_eur, cogs: m.cogs_eur, grossProfit, grossMarginPct: m.revenue_eur > 0 ? (grossProfit / m.revenue_eur) * 100 : 0, personnel: m.personnel_eur, marketing: m.marketing_eur, infra: m.infra_eur, totalCosts, ebitda, ebitdaMarginPct: m.revenue_eur > 0 ? (ebitda / m.revenue_eur) * 100 : 0, customers: m.total_customers, employees: m.employees_count, mrr: m.mrr_eur, cashBalance: m.cash_balance_eur, } })) } const handleToggleYear = (year: number) => { setExpandedYear(prev => prev === year ? null : year) } const tableContent = ( ) return ( <>
{/* HGB / US GAAP Toggle (inline) */}
{tableContent}

{de ? 'Klicke auf ein Jahr fuer Monatsdetails' : 'Click on a year for monthly details'}

{/* Fullscreen via Portal — escapes parent stacking context */} {isFullscreen && portalRoot && createPortal( setIsFullscreen(false)} lang={lang} standard={standard} onStandardChange={setStandard} > , portalRoot )} ) }