Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook) and split all 44 files exceeding 500 LOC into domain-focused modules: - consent-service (Go): models, handlers, services, database splits - backend-core (Python): security_api, rbac_api, pdf_service, auth splits - admin-core (TypeScript): 5 page.tsx + sidebar extractions - pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits - voice-service (Python): enhanced_task_orchestrator split Result: 0 violations, 36 exempted (pipeline, tests, pure-data files). Go build verified clean. No behavior changes — pure structural splits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
180 lines
5.8 KiB
TypeScript
180 lines
5.8 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
import { motion } from 'framer-motion'
|
|
import { Maximize2 } from 'lucide-react'
|
|
import { FMResult } from '@/lib/types'
|
|
import {
|
|
AnnualPLTableProps,
|
|
AnnualRow,
|
|
MonthlyRow,
|
|
AccountingStandard,
|
|
} from './AnnualPLTable.types'
|
|
import { AnnualTable, FullscreenOverlay } from './AnnualPLTable.parts'
|
|
|
|
export default function AnnualPLTable({ results, lang }: AnnualPLTableProps) {
|
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
const [expandedYear, setExpandedYear] = useState<number | null>(null)
|
|
const [standard, setStandard] = useState<AccountingStandard>('hgb')
|
|
const [portalRoot, setPortalRoot] = useState<HTMLElement | null>(null)
|
|
const de = lang === 'de'
|
|
|
|
// Portal mount point
|
|
useEffect(() => {
|
|
setPortalRoot(document.body)
|
|
}, [])
|
|
|
|
// Aggregate monthly results into annual
|
|
const annualMap = new Map<number, FMResult[]>()
|
|
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<number, MonthlyRow[]>()
|
|
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 = (
|
|
<AnnualTable
|
|
rows={rows}
|
|
lang={lang}
|
|
expandedYear={expandedYear}
|
|
onToggleYear={handleToggleYear}
|
|
monthlyData={monthlyData}
|
|
isFullscreen={isFullscreen}
|
|
standard={standard}
|
|
/>
|
|
)
|
|
|
|
return (
|
|
<>
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4 }}
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
{/* HGB / US GAAP Toggle (inline) */}
|
|
<div className="flex items-center bg-white/[0.04] border border-white/5 rounded-lg overflow-hidden">
|
|
<button
|
|
onClick={() => setStandard('hgb')}
|
|
className={`px-2 py-0.5 text-[10px] font-medium transition-all ${
|
|
standard === 'hgb'
|
|
? 'bg-indigo-500/20 text-indigo-300'
|
|
: 'text-white/30 hover:text-white/50'
|
|
}`}
|
|
>
|
|
HGB
|
|
</button>
|
|
<button
|
|
onClick={() => setStandard('usgaap')}
|
|
className={`px-2 py-0.5 text-[10px] font-medium transition-all ${
|
|
standard === 'usgaap'
|
|
? 'bg-indigo-500/20 text-indigo-300'
|
|
: 'text-white/30 hover:text-white/50'
|
|
}`}
|
|
>
|
|
US GAAP
|
|
</button>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsFullscreen(true)}
|
|
className="flex items-center gap-1.5 px-2 py-1 bg-white/[0.06] hover:bg-white/[0.1] border border-white/10 rounded-lg text-[10px] text-white/50 hover:text-white/80 transition-all"
|
|
title={de ? 'Vollbild' : 'Fullscreen'}
|
|
>
|
|
<Maximize2 className="w-3 h-3" />
|
|
{de ? 'Vollbild' : 'Fullscreen'}
|
|
</button>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
{tableContent}
|
|
</div>
|
|
<p className="text-[9px] text-white/20 mt-2 text-center">
|
|
{de
|
|
? 'Klicke auf ein Jahr fuer Monatsdetails'
|
|
: 'Click on a year for monthly details'}
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* Fullscreen via Portal — escapes parent stacking context */}
|
|
{isFullscreen && portalRoot && createPortal(
|
|
<FullscreenOverlay
|
|
onClose={() => setIsFullscreen(false)}
|
|
lang={lang}
|
|
standard={standard}
|
|
onStandardChange={setStandard}
|
|
>
|
|
<AnnualTable
|
|
rows={rows}
|
|
lang={lang}
|
|
expandedYear={expandedYear}
|
|
onToggleYear={handleToggleYear}
|
|
monthlyData={monthlyData}
|
|
isFullscreen={true}
|
|
standard={standard}
|
|
/>
|
|
</FullscreenOverlay>,
|
|
portalRoot
|
|
)}
|
|
</>
|
|
)
|
|
}
|