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>
260 lines
9.3 KiB
TypeScript
260 lines
9.3 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect } from 'react'
|
|
import { ChevronDown, ChevronRight, Minimize2 } from 'lucide-react'
|
|
import {
|
|
AnnualRow,
|
|
MonthlyRow,
|
|
AccountingStandard,
|
|
fmt,
|
|
fmtMonth,
|
|
getLineItems,
|
|
MONTH_NAMES_DE,
|
|
MONTH_NAMES_EN,
|
|
} from './AnnualPLTable.types'
|
|
|
|
interface AnnualTableProps {
|
|
rows: AnnualRow[]
|
|
lang: 'de' | 'en'
|
|
expandedYear: number | null
|
|
onToggleYear: (year: number) => void
|
|
monthlyData: Map<number, MonthlyRow[]>
|
|
isFullscreen: boolean
|
|
standard: AccountingStandard
|
|
}
|
|
|
|
export function AnnualTable({
|
|
rows,
|
|
lang,
|
|
expandedYear,
|
|
onToggleYear,
|
|
monthlyData,
|
|
isFullscreen,
|
|
standard,
|
|
}: AnnualTableProps) {
|
|
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 (
|
|
<table className={`w-full ${textSize}`}>
|
|
<thead>
|
|
<tr className="border-b border-white/10">
|
|
<th className={`text-left py-2 pr-4 text-white/40 font-medium ${isFullscreen ? 'min-w-[220px]' : 'min-w-[180px]'}`}>
|
|
{standard === 'hgb'
|
|
? (de ? 'GuV-Position (HGB)' : 'P&L Line Item (HGB)')
|
|
: (de ? 'GuV-Position (US GAAP)' : 'P&L Line Item (US GAAP)')
|
|
}
|
|
</th>
|
|
{rows.map(r => (
|
|
<th
|
|
key={r.year}
|
|
className={`text-right py-2 px-2 text-white/50 font-semibold ${minColWidth} cursor-pointer hover:text-indigo-400 transition-colors`}
|
|
onClick={() => onToggleYear(r.year)}
|
|
title={de ? 'Klicken fuer Monatsdetails' : 'Click for monthly details'}
|
|
>
|
|
<span className="inline-flex items-center gap-1">
|
|
{expandedYear === r.year ? (
|
|
<ChevronDown className="w-3 h-3" />
|
|
) : (
|
|
<ChevronRight className="w-3 h-3 opacity-30" />
|
|
)}
|
|
{r.year}
|
|
</span>
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{lineItems.map((item) => (
|
|
<tr
|
|
key={item.key}
|
|
className={item.isSeparator ? 'border-t border-white/10' : ''}
|
|
>
|
|
<td className={`py-1.5 pr-4 ${item.isBold ? 'text-white font-semibold' : 'text-white/50'} ${item.isPercent ? 'italic text-white/30' : ''}`}>
|
|
{item.label}
|
|
</td>
|
|
{rows.map(r => {
|
|
const val = r[item.key] as number
|
|
return (
|
|
<td
|
|
key={r.year}
|
|
className={`text-right py-1.5 px-2 font-mono
|
|
${item.isBold ? 'font-semibold' : ''}
|
|
${item.isPercent ? 'text-white/30 italic' : ''}
|
|
${!item.isPercent && val < 0 ? 'text-red-400/80' : ''}
|
|
${!item.isPercent && val > 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))
|
|
}
|
|
</td>
|
|
)
|
|
})}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
|
|
{/* Monthly Drill-Down */}
|
|
{expandedYear && monthlyData.has(expandedYear) && (
|
|
<tbody>
|
|
<tr>
|
|
<td colSpan={rows.length + 1} className="pt-4 pb-1">
|
|
<div className="border-t border-indigo-500/30 pt-3">
|
|
<p className="text-xs font-semibold text-indigo-400 mb-2">
|
|
{de ? `Monatsdetails ${expandedYear}` : `Monthly Details ${expandedYear}`}
|
|
</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr className="border-b border-white/10">
|
|
<td className="text-left py-1 pr-4 text-white/40 font-medium text-[10px]">
|
|
{de ? 'Monat' : 'Month'}
|
|
</td>
|
|
{monthlyData.get(expandedYear)!.map(m => (
|
|
<td key={m.monthInYear} className="text-right py-1 px-1 text-white/40 font-medium text-[10px]">
|
|
{monthNames[m.monthInYear - 1]}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
{lineItems.map((item) => {
|
|
const mKey = item.monthKey
|
|
if (!mKey) return null
|
|
return (
|
|
<tr key={`monthly-${item.key}`} className={item.isSeparator ? 'border-t border-white/5' : ''}>
|
|
<td className={`py-1 pr-4 text-[10px] ${item.isBold ? 'text-white/70 font-medium' : 'text-white/30'} ${item.isPercent ? 'italic' : ''}`}>
|
|
{item.label}
|
|
</td>
|
|
{monthlyData.get(expandedYear)!.map(m => {
|
|
const val = m[mKey] as number
|
|
return (
|
|
<td
|
|
key={m.monthInYear}
|
|
className={`text-right py-1 px-1 font-mono text-[10px]
|
|
${item.isPercent ? 'text-white/20 italic' : ''}
|
|
${!item.isPercent && val < 0 ? 'text-red-400/60' : ''}
|
|
${!item.isPercent && val > 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))
|
|
}
|
|
</td>
|
|
)
|
|
})}
|
|
</tr>
|
|
)
|
|
})}
|
|
{monthlyExtraItems.map((item) => (
|
|
<tr key={`monthly-extra-${item.key}`} className="border-t border-white/5">
|
|
<td className="py-1 pr-4 text-[10px] text-indigo-300/70 font-medium">{item.label}</td>
|
|
{monthlyData.get(expandedYear)!.map(m => {
|
|
const val = m[item.key] as number
|
|
return (
|
|
<td key={m.monthInYear} className="text-right py-1 px-1 font-mono text-[10px] text-indigo-300/50">
|
|
{fmtMonth(Math.round(val))}
|
|
</td>
|
|
)
|
|
})}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
)}
|
|
</table>
|
|
)
|
|
}
|
|
|
|
interface FullscreenOverlayProps {
|
|
children: React.ReactNode
|
|
onClose: () => void
|
|
lang: 'de' | 'en'
|
|
standard: AccountingStandard
|
|
onStandardChange: (s: AccountingStandard) => void
|
|
}
|
|
|
|
export function FullscreenOverlay({
|
|
children,
|
|
onClose,
|
|
lang,
|
|
standard,
|
|
onStandardChange,
|
|
}: FullscreenOverlayProps) {
|
|
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 (
|
|
<div className="fixed inset-0 z-[9999] bg-slate-950/98 backdrop-blur-xl overflow-auto p-6">
|
|
<div className="max-w-[1400px] mx-auto">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h2 className="text-xl font-bold text-white">
|
|
{de ? 'Gewinn- und Verlustrechnung' : 'Profit & Loss Statement'}
|
|
</h2>
|
|
<p className="text-xs text-white/40">
|
|
{de
|
|
? 'Klicke auf ein Jahr um die Monatsdetails zu sehen · ESC zum Schliessen'
|
|
: 'Click on a year to see monthly details · ESC to close'}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{/* HGB / US GAAP Toggle */}
|
|
<div className="flex items-center bg-white/[0.06] border border-white/10 rounded-xl overflow-hidden">
|
|
<button
|
|
onClick={() => onStandardChange('hgb')}
|
|
className={`px-3 py-1.5 text-xs font-medium transition-all ${
|
|
standard === 'hgb'
|
|
? 'bg-indigo-500/20 text-indigo-300'
|
|
: 'text-white/40 hover:text-white/60'
|
|
}`}
|
|
>
|
|
HGB
|
|
</button>
|
|
<button
|
|
onClick={() => onStandardChange('usgaap')}
|
|
className={`px-3 py-1.5 text-xs font-medium transition-all ${
|
|
standard === 'usgaap'
|
|
? 'bg-indigo-500/20 text-indigo-300'
|
|
: 'text-white/40 hover:text-white/60'
|
|
}`}
|
|
>
|
|
US GAAP
|
|
</button>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="flex items-center gap-2 px-3 py-2 bg-white/[0.08] hover:bg-white/[0.12] border border-white/10 rounded-xl text-sm text-white/70 hover:text-white transition-all"
|
|
>
|
|
<Minimize2 className="w-4 h-4" />
|
|
{de ? 'Schliessen' : 'Close'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="bg-white/[0.03] border border-white/10 rounded-2xl p-6 overflow-x-auto">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|