refactor: Consolidate standalone services into admin-v2, add new SDK modules
Remove standalone services (ai-compliance-sdk root, developer-portal, dsms-gateway, dsms-node, night-scheduler) and legacy compliance/dsgvo pages. Add new SDK pipeline modules (academy, document-crawler, dsb-portal, incidents, whistleblower, reporting, sso, multi-tenant, industry-templates). Add drafting engine, legal corpus files (AT/CH/DE), pitch-deck, blog and Förderantrag pages. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
232
pitch-deck/components/ui/AnnualPLTable.tsx
Normal file
232
pitch-deck/components/ui/AnnualPLTable.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { FMResult } from '@/lib/types'
|
||||
|
||||
export type AccountingStandard = 'hgb' | 'usgaap'
|
||||
|
||||
interface AnnualPLTableProps {
|
||||
results: FMResult[]
|
||||
lang: 'de' | 'en'
|
||||
standard: AccountingStandard
|
||||
}
|
||||
|
||||
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
|
||||
// Detail costs
|
||||
adminCosts: number
|
||||
officeCosts: number
|
||||
travelCosts: number
|
||||
softwareLicenses: number
|
||||
depreciation: number
|
||||
interestExpense: number
|
||||
taxes: number
|
||||
netIncome: number
|
||||
ebit: number
|
||||
ihk: number
|
||||
foundingCosts: number
|
||||
// US GAAP aggregates
|
||||
rAndD: number
|
||||
sga: 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')
|
||||
}
|
||||
|
||||
export default function AnnualPLTable({ results, lang, standard }: AnnualPLTableProps) {
|
||||
const de = lang === 'de'
|
||||
|
||||
// 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 adminCosts = months.reduce((s, m) => s + (m.admin_costs_eur || 0), 0)
|
||||
const officeCosts = months.reduce((s, m) => s + (m.office_costs_eur || 0), 0)
|
||||
const travelCosts = months.reduce((s, m) => s + (m.travel_costs_eur || 0), 0)
|
||||
const softwareLicenses = months.reduce((s, m) => s + (m.software_licenses_eur || 0), 0)
|
||||
const depreciation = months.reduce((s, m) => s + (m.depreciation_eur || 0), 0)
|
||||
const interestExpense = months.reduce((s, m) => s + (m.interest_expense_eur || 0), 0)
|
||||
const taxes = months.reduce((s, m) => s + (m.taxes_eur || 0), 0)
|
||||
const netIncome = months.reduce((s, m) => s + (m.net_income_eur || 0), 0)
|
||||
const ebit = months.reduce((s, m) => s + (m.ebit_eur || 0), 0)
|
||||
const ihk = months.reduce((s, m) => s + (m.ihk_eur || 0), 0)
|
||||
const foundingCosts = months.reduce((s, m) => s + (m.founding_costs_eur || 0), 0)
|
||||
|
||||
const totalOpex = personnel + marketing + infra + adminCosts + officeCosts + travelCosts + softwareLicenses + ihk + foundingCosts
|
||||
const ebitda = grossProfit - totalOpex
|
||||
const lastMonth = months[months.length - 1]
|
||||
|
||||
// US GAAP aggregates
|
||||
const rAndD = infra + softwareLicenses
|
||||
const sga = personnel + adminCosts + officeCosts + travelCosts + ihk + foundingCosts
|
||||
|
||||
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,
|
||||
adminCosts,
|
||||
officeCosts,
|
||||
travelCosts,
|
||||
softwareLicenses,
|
||||
depreciation,
|
||||
interestExpense,
|
||||
taxes,
|
||||
netIncome,
|
||||
ebit,
|
||||
ihk,
|
||||
foundingCosts,
|
||||
rAndD,
|
||||
sga,
|
||||
}
|
||||
})
|
||||
|
||||
type LineItem = {
|
||||
label: string
|
||||
getValue: (r: AnnualRow) => number
|
||||
isBold?: boolean
|
||||
isPercent?: boolean
|
||||
isSeparator?: boolean
|
||||
isNegative?: boolean
|
||||
isSubRow?: boolean
|
||||
}
|
||||
|
||||
const hgbLineItems: LineItem[] = [
|
||||
{ label: 'Umsatzerloese', getValue: r => r.revenue, isBold: true },
|
||||
{ label: '- Materialaufwand', getValue: r => r.cogs, isNegative: true },
|
||||
{ label: '- Personalaufwand', getValue: r => r.personnel, isNegative: true },
|
||||
{ label: '- Abschreibungen', getValue: r => r.depreciation, isNegative: true },
|
||||
{ label: '- Sonstige betr. Aufwendungen', getValue: r => r.marketing + r.adminCosts + r.officeCosts + r.travelCosts + r.softwareLicenses + r.ihk + r.foundingCosts + r.infra, isNegative: true, isSeparator: true },
|
||||
{ label: ' davon Marketing', getValue: r => r.marketing, isNegative: true, isSubRow: true },
|
||||
{ label: ' davon Steuerberater/Recht', getValue: r => r.adminCosts, isNegative: true, isSubRow: true },
|
||||
{ label: ' davon Buero/Telefon', getValue: r => r.officeCosts, isNegative: true, isSubRow: true },
|
||||
{ label: ' davon Software', getValue: r => r.softwareLicenses, isNegative: true, isSubRow: true },
|
||||
{ label: ' davon Reisekosten', getValue: r => r.travelCosts, isNegative: true, isSubRow: true },
|
||||
{ label: ' davon Infrastruktur', getValue: r => r.infra, isNegative: true, isSubRow: true },
|
||||
{ label: '= Betriebsergebnis (EBIT)', getValue: r => r.ebit, isBold: true, isSeparator: true },
|
||||
{ label: '- Zinsaufwand', getValue: r => r.interestExpense, isNegative: true },
|
||||
{ label: '= Ergebnis vor Steuern (EBT)', getValue: r => r.ebit - r.interestExpense, isBold: true, isSeparator: true },
|
||||
{ label: '- Steuern', getValue: r => r.taxes, isNegative: true },
|
||||
{ label: '= Jahresueberschuss', getValue: r => r.netIncome, isBold: true, isSeparator: true },
|
||||
{ label: 'Kunden (Jahresende)', getValue: r => r.customers },
|
||||
{ label: 'Mitarbeiter', getValue: r => r.employees },
|
||||
]
|
||||
|
||||
const usgaapLineItems: LineItem[] = [
|
||||
{ label: 'Revenue', getValue: r => r.revenue, isBold: true },
|
||||
{ label: '- Cost of Revenue (COGS)', getValue: r => r.cogs, isNegative: true },
|
||||
{ label: '= Gross Profit', getValue: r => r.grossProfit, isBold: true, isSeparator: true },
|
||||
{ label: ' Gross Margin', getValue: r => r.grossMarginPct, isPercent: true },
|
||||
{ label: '- Sales & Marketing', getValue: r => r.marketing, isNegative: true },
|
||||
{ label: '- Research & Development', getValue: r => r.rAndD, isNegative: true },
|
||||
{ label: '- General & Administrative', getValue: r => r.sga, isNegative: true },
|
||||
{ label: '- Depreciation & Amortization', getValue: r => r.depreciation, isNegative: true },
|
||||
{ label: '= Operating Income (EBIT)', getValue: r => r.ebit, isBold: true, isSeparator: true },
|
||||
{ label: ' Operating Margin', getValue: r => r.revenue > 0 ? (r.ebit / r.revenue) * 100 : 0, isPercent: true },
|
||||
{ label: '- Interest Expense', getValue: r => r.interestExpense, isNegative: true },
|
||||
{ label: '= Income Before Tax (EBT)', getValue: r => r.ebit - r.interestExpense, isBold: true, isSeparator: true },
|
||||
{ label: '- Income Tax', getValue: r => r.taxes, isNegative: true },
|
||||
{ label: '= Net Income', getValue: r => r.netIncome, isBold: true, isSeparator: true },
|
||||
{ label: 'Customers (Year End)', getValue: r => r.customers },
|
||||
{ label: 'Employees', getValue: r => r.employees },
|
||||
]
|
||||
|
||||
const lineItems = standard === 'hgb' ? hgbLineItems : usgaapLineItems
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="overflow-x-auto"
|
||||
>
|
||||
<table className="w-full text-[11px]">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10">
|
||||
<th className="text-left py-2 pr-4 text-white/40 font-medium min-w-[200px]">
|
||||
{standard === 'hgb'
|
||||
? 'GuV-Position (HGB)'
|
||||
: '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 min-w-[80px]">
|
||||
{r.year}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lineItems.map((item, idx) => (
|
||||
<tr
|
||||
key={idx}
|
||||
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.isSubRow ? 'text-[10px] text-white/30' : ''}
|
||||
`}>
|
||||
{item.label}
|
||||
</td>
|
||||
{rows.map(r => {
|
||||
const val = item.getValue(r)
|
||||
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.isSubRow ? 'text-[10px] text-white/25' : ''}
|
||||
${!item.isPercent && val < 0 ? 'text-red-400/80' : ''}
|
||||
${!item.isPercent && val > 0 && (item.label.includes('EBIT') || item.label.includes('Net Income') || item.label.includes('Jahresueberschuss') || item.label.includes('Betriebsergebnis')) ? 'text-emerald-400' : ''}
|
||||
${!item.isPercent && !item.isBold && !item.isSubRow ? 'text-white/60' : 'text-white'}
|
||||
`}
|
||||
>
|
||||
{item.isPercent
|
||||
? `${val.toFixed(1)}%`
|
||||
: (item.isNegative && val > 0 ? '-' : '') + fmt(Math.abs(val))
|
||||
}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user