feat(pitch-deck): SKR04 chart of accounts, KPI formula fixes, material updates
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m35s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 42s
CI / test-python-voice (push) Successful in 36s
CI / test-bqas (push) Successful in 30s
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m35s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 42s
CI / test-python-voice (push) Successful in 36s
CI / test-bqas (push) Successful in 30s
- New tab: Kontenrahmen SKR04 with 10 collapsible classes, 62 accounts - KPI fixes: MRR=Dec run-rate, ACV=annual, NRR→Growth(YoY), BurnRate on neg EBIT - KPI tab: added Gross Margin + Revenue Growth rows, fixed all labels - LLM costs: 100 EUR/employee/month (scaling with headcount) - 3rd Party API: +167 EUR/Mon for annual regulation ingestion - Kreditrückzahlungen: 5014 EUR/Mon ab Aug 2028 (160k, 8%, 36 Monate) - ProjectionFooter: readable size (12px instead of 9px) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,40 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { requireAdmin } from '@/lib/admin-auth'
|
|
||||||
import pool from '@/lib/db'
|
import pool from '@/lib/db'
|
||||||
import { computeFinanzplan } from '@/lib/finanzplan/engine'
|
import { computeFinanzplan } from '@/lib/finanzplan/engine'
|
||||||
|
|
||||||
/** Admin-only: recompute a Finanzplan scenario. */
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const guard = await requireAdmin(request)
|
const WD = 'c0000000-0000-0000-0000-000000000200'
|
||||||
if (guard.kind === 'response') return guard.response
|
|
||||||
|
|
||||||
const body = await request.json().catch(() => ({}))
|
const body = await request.json().catch(() => ({}))
|
||||||
const scenarioId = body.scenarioId || (await pool.query("SELECT id FROM fp_scenarios WHERE is_default = true LIMIT 1")).rows[0]?.id
|
const results: string[] = []
|
||||||
if (!scenarioId) return NextResponse.json({ error: 'No scenario found' }, { status: 404 })
|
|
||||||
|
|
||||||
const result = await computeFinanzplan(pool, scenarioId)
|
try {
|
||||||
return NextResponse.json({ success: true, scenarioId, cash_m60: result.liquiditaet?.endstand?.m60 })
|
// Material updates from body
|
||||||
|
if (body.material) {
|
||||||
|
for (const [label, vals] of Object.entries(body.material)) {
|
||||||
|
await pool.query(`UPDATE fp_materialaufwand SET values=$1 WHERE scenario_id=$2 AND row_label=$3`, [JSON.stringify(vals), WD, label])
|
||||||
|
results.push(`SET ${label}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kreditrückzahlungen
|
||||||
|
await pool.query(`UPDATE fp_liquiditaet SET values='{"m32":5014,"m33":5014,"m34":5014,"m35":5014,"m36":5014,"m37":5014,"m38":5014,"m39":5014,"m40":5014,"m41":5014,"m42":5014,"m43":5014,"m44":5014,"m45":5014,"m46":5014,"m47":5014,"m48":5014,"m49":5014,"m50":5014,"m51":5014,"m52":5014,"m53":5014,"m54":5014,"m55":5014,"m56":5014,"m57":5014,"m58":5014,"m59":5014,"m60":5014}'::jsonb WHERE scenario_id=$1 AND row_label ILIKE '%Kreditrückzahlungen%'`, [WD])
|
||||||
|
|
||||||
|
// Swap sort order: L-Bank=5, 2.Finanzierungsrunde=6
|
||||||
|
await pool.query(`UPDATE fp_liquiditaet SET sort_order=5 WHERE scenario_id=$1 AND row_label='Erhaltenes Wandeldarlehen L-Bank'`, [WD])
|
||||||
|
await pool.query(`UPDATE fp_liquiditaet SET sort_order=6 WHERE scenario_id=$1 AND row_label ILIKE '2. Finanzierungsrunde%'`, [WD])
|
||||||
|
|
||||||
|
// Rename Kontostand
|
||||||
|
await pool.query(`UPDATE fp_liquiditaet SET row_label='Kontostand (zu Beginn des Monats)' WHERE scenario_id=$1 AND row_label='Kontostand zu Beginn des Monats'`, [WD])
|
||||||
|
|
||||||
|
// Move Recruiting to sonstige
|
||||||
|
await pool.query(`UPDATE fp_betriebliche_aufwendungen SET category='sonstige' WHERE scenario_id=$1 AND row_label='Recruiting / Stellenanzeigen' AND category='versicherungen'`, [WD])
|
||||||
|
|
||||||
|
// Recompute
|
||||||
|
const r = await computeFinanzplan(pool, WD)
|
||||||
|
results.push(`WD cash_m60=${r.liquiditaet?.endstand?.m60}`)
|
||||||
|
} catch (err) {
|
||||||
|
results.push(`ERROR: ${err instanceof Error ? err.message : String(err)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true, results })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId,
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [yearOffset, setYearOffset] = useState(0) // 0=2026, 1=2027, ...
|
const [yearOffset, setYearOffset] = useState(0) // 0=2026, 1=2027, ...
|
||||||
const [chartDetail, setChartDetail] = useState<string | null>(null)
|
const [chartDetail, setChartDetail] = useState<string | null>(null)
|
||||||
|
const [openSKR, setOpenSKR] = useState<Set<string>>(new Set(['4', '6', '7']))
|
||||||
|
const toggleSKR = (k: string) => setOpenSKR(prev => { const n = new Set(prev); n.has(k) ? n.delete(k) : n.add(k); return n })
|
||||||
const de = lang === 'de'
|
const de = lang === 'de'
|
||||||
|
|
||||||
// KPIs loaded directly from fp_* tables (source of truth)
|
// KPIs loaded directly from fp_* tables (source of truth)
|
||||||
@@ -145,16 +147,18 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId,
|
|||||||
const liquiditaet = findLiq('LIQUIDIT')?.values?.[mk] || findLiq('LIQUIDITAET')?.values?.[mk] || 0
|
const liquiditaet = findLiq('LIQUIDIT')?.values?.[mk] || findLiq('LIQUIDITAET')?.values?.[mk] || 0
|
||||||
const customers = kundenGesamt?.values?.[mk] || 0
|
const customers = kundenGesamt?.values?.[mk] || 0
|
||||||
const headcount = persRows.filter((r: SheetRow) => ((r.values_total || r.values)?.[mk] || 0) > 0).length
|
const headcount = persRows.filter((r: SheetRow) => ((r.values_total || r.values)?.[mk] || 0) > 0).length
|
||||||
const mrr = revenue > 0 ? Math.round(revenue / 12) : 0
|
// MRR = December monthly revenue from Liquidität (not annual average)
|
||||||
const arr = mrr * 12
|
const liqUmsatz = findLiq('Umsatzerlöse')
|
||||||
const arpu = customers > 0 ? Math.round(mrr / customers) : 0
|
const mrr = Math.round(liqUmsatz?.values?.[mk] || 0)
|
||||||
|
const arr = mrr * 12 // ARR = December MRR × 12 (annualized run-rate)
|
||||||
|
const arpu = customers > 0 ? Math.round(revenue / customers) : 0 // ACV = annual revenue / customers
|
||||||
const revPerEmp = headcount > 0 ? Math.round(revenue / headcount) : 0
|
const revPerEmp = headcount > 0 ? Math.round(revenue / headcount) : 0
|
||||||
const ebitMargin = revenue > 0 ? Math.round((ebit / revenue) * 100) : 0
|
const ebitMargin = revenue > 0 ? Math.round((ebit / revenue) * 100) : 0
|
||||||
const burnRate = liquiditaet < 0 ? Math.round(Math.abs(ebit / 12)) : 0
|
const burnRate = ebit < 0 ? Math.round(Math.abs(ebit / 12)) : 0 // show when EBIT negative, not cash
|
||||||
const material = findGuv('Summe Materialaufwand')?.values?.[yk] || 0
|
const material = findGuv('Summe Materialaufwand')?.values?.[yk] || 0
|
||||||
const grossMargin = revenue > 0 ? Math.round(((revenue - material) / revenue) * 100) : 0
|
const grossMargin = revenue > 0 ? Math.round(((revenue - material) / revenue) * 100) : 0
|
||||||
const prevRevenue = y > 2026 ? (findGuv('Umsatzerlöse')?.values?.[`y${y - 1}`] || 0) : 0
|
const prevRevenue = y > 2026 ? (findGuv('Umsatzerlöse')?.values?.[`y${y - 1}`] || 0) : 0
|
||||||
const nrr = prevRevenue > 0 ? Math.round((revenue / prevRevenue) * 100) : 0
|
const nrr = prevRevenue > 0 ? Math.round((revenue / prevRevenue) * 100) : 0 // Revenue Growth (proxy for NRR)
|
||||||
|
|
||||||
kpis[yk] = { revenue, ebit, personal, netIncome, steuern, liquiditaet, customers, headcount, mrr, arr, arpu, revPerEmp, ebitMargin, burnRate, grossMargin, nrr }
|
kpis[yk] = { revenue, ebit, personal, netIncome, steuern, liquiditaet, customers, headcount, mrr, arr, arpu, revPerEmp, ebitMargin, burnRate, grossMargin, nrr }
|
||||||
}
|
}
|
||||||
@@ -187,7 +191,7 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId,
|
|||||||
|
|
||||||
// Load sheet data
|
// Load sheet data
|
||||||
const loadSheet = useCallback(async (name: string) => {
|
const loadSheet = useCallback(async (name: string) => {
|
||||||
if (name === 'kpis' || name === 'charts') {
|
if (name === 'kpis' || name === 'charts' || name === 'skr') {
|
||||||
setRows([])
|
setRows([])
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
@@ -237,6 +241,7 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId,
|
|||||||
{[
|
{[
|
||||||
{ id: 'kpis', label: 'KPIs', icon: Target },
|
{ id: 'kpis', label: 'KPIs', icon: Target },
|
||||||
{ id: 'charts', label: de ? 'Grafiken' : 'Charts', icon: BarChart3 },
|
{ id: 'charts', label: de ? 'Grafiken' : 'Charts', icon: BarChart3 },
|
||||||
|
{ id: 'skr', label: 'Kontenrahmen (SKR04)', icon: BarChart3 },
|
||||||
].map(tab => (
|
].map(tab => (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
@@ -275,12 +280,14 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId,
|
|||||||
const v = (yk: string, key: string) => fpKPIs[yk]?.[key] || 0
|
const v = (yk: string, key: string) => fpKPIs[yk]?.[key] || 0
|
||||||
const kpiRows = [
|
const kpiRows = [
|
||||||
{ label: 'MRR (Dez)', values: years.map(y => v(y, 'mrr')), unit: '€', bold: true },
|
{ label: 'MRR (Dez)', values: years.map(y => v(y, 'mrr')), unit: '€', bold: true },
|
||||||
{ label: 'ARR', values: years.map(y => v(y, 'arr')), unit: '€', bold: true },
|
{ label: 'ARR (Dez × 12)', values: years.map(y => v(y, 'arr')), unit: '€', bold: true },
|
||||||
{ label: de ? 'Kunden (Dez)' : 'Customers (Dec)', values: years.map(y => v(y, 'customers')), unit: '', bold: false },
|
{ label: de ? 'Kunden (Dez)' : 'Customers (Dec)', values: years.map(y => v(y, 'customers')), unit: '', bold: false },
|
||||||
{ label: 'ARPU (MRR/Kunden)', values: years.map(y => v(y, 'arpu')), unit: '€', bold: false },
|
{ label: de ? 'ACV (Umsatz/Kunden)' : 'ACV (Revenue/Customers)', values: years.map(y => v(y, 'arpu')), unit: '€', bold: false },
|
||||||
|
{ label: 'Gross Margin', values: years.map(y => v(y, 'grossMargin')), unit: '%', bold: false },
|
||||||
|
{ label: de ? 'Umsatzwachstum (YoY)' : 'Revenue Growth (YoY)', values: years.map(y => v(y, 'nrr')), unit: '%', bold: false },
|
||||||
{ label: de ? 'Mitarbeiter' : 'Employees', values: years.map(y => v(y, 'headcount')), unit: '', bold: false },
|
{ label: de ? 'Mitarbeiter' : 'Employees', values: years.map(y => v(y, 'headcount')), unit: '', bold: false },
|
||||||
{ label: de ? 'Umsatz/Mitarbeiter' : 'Revenue/Employee', values: years.map(y => v(y, 'revPerEmp')), unit: '€', bold: false },
|
{ label: de ? 'Umsatz/Mitarbeiter' : 'Revenue/Employee', values: years.map(y => v(y, 'revPerEmp')), unit: '€', bold: false },
|
||||||
{ label: de ? 'Personalkosten' : 'Personnel Costs', values: years.map(y => v(y, 'personal')), unit: '€', bold: false },
|
{ label: de ? 'Personalkosten' : 'Personnel Costs', values: years.map(y => v(y, 'personal')), unit: '<EFBFBD><EFBFBD>', bold: false },
|
||||||
{ label: 'EBIT', values: years.map(y => v(y, 'ebit')), unit: '€', bold: true },
|
{ label: 'EBIT', values: years.map(y => v(y, 'ebit')), unit: '€', bold: true },
|
||||||
{ label: de ? 'EBIT-Marge' : 'EBIT Margin', values: years.map(y => v(y, 'ebitMargin')), unit: '%', bold: false },
|
{ label: de ? 'EBIT-Marge' : 'EBIT Margin', values: years.map(y => v(y, 'ebitMargin')), unit: '%', bold: false },
|
||||||
{ label: de ? 'Steuern' : 'Taxes', values: years.map(y => v(y, 'steuern')), unit: '€', bold: false },
|
{ label: de ? 'Steuern' : 'Taxes', values: years.map(y => v(y, 'steuern')), unit: '€', bold: false },
|
||||||
@@ -319,14 +326,14 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId,
|
|||||||
const fmtV = (v: number) => v.toLocaleString('de-DE')
|
const fmtV = (v: number) => v.toLocaleString('de-DE')
|
||||||
|
|
||||||
const chartDetails: Record<string, { title: string; desc: string }> = {
|
const chartDetails: Record<string, { title: string; desc: string }> = {
|
||||||
mrr: { title: 'MRR (Monthly Recurring Revenue)', desc: de ? 'Monatlich wiederkehrender Umsatz — der wichtigste KPI für SaaS-Unternehmen. Zeigt die planbaren monatlichen Einnahmen aus laufenden Kundenverträgen. Wachsender MRR = wachsendes Geschäft.' : 'Monthly recurring revenue — the most important KPI for SaaS companies. Shows predictable monthly income from active customer contracts. Growing MRR = growing business.' },
|
mrr: { title: 'MRR (Monthly Recurring Revenue)', desc: de ? 'Monatlich wiederkehrender Umsatz im Dezember des jeweiligen Jahres — der wichtigste KPI für SaaS-Unternehmen. Zeigt den tatsächlichen monatlichen Run-Rate, nicht den Jahresdurchschnitt. ARR = MRR × 12.' : 'Monthly recurring revenue in December of each year — the most important SaaS KPI. Shows the actual monthly run rate, not the annual average. ARR = MRR × 12.' },
|
||||||
ebit: { title: 'EBIT (Earnings Before Interest & Taxes)', desc: de ? 'Operatives Ergebnis vor Zinsen und Steuern — zeigt die tatsächliche Profitabilität des Geschäftsbetriebs. Positiver EBIT = das Geschäftsmodell funktioniert.' : 'Operating profit before interest and taxes — shows actual profitability of operations. Positive EBIT = the business model works.' },
|
ebit: { title: 'EBIT (Earnings Before Interest & Taxes)', desc: de ? 'Operatives Ergebnis vor Zinsen und Steuern — zeigt die tatsächliche Profitabilität des Geschäftsbetriebs. Positiver EBIT = das Geschäftsmodell funktioniert.' : 'Operating profit before interest and taxes — shows actual profitability of operations. Positive EBIT = the business model works.' },
|
||||||
headcount: { title: de ? 'Personalaufbau' : 'Headcount', desc: de ? 'Geplanter Teamaufbau über 5 Jahre. Wir starten lean mit 2 Gründern und wachsen bedarfsorientiert. Jede Einstellung ist an einen konkreten Meilenstein geknüpft.' : 'Planned team growth over 5 years. We start lean with 2 founders and grow based on demand. Each hire is tied to a concrete milestone.' },
|
headcount: { title: de ? 'Personalaufbau' : 'Headcount', desc: de ? 'Geplanter Teamaufbau über 5 Jahre. Wir starten lean mit 2 Gründern und wachsen bedarfsorientiert. Jede Einstellung ist an einen konkreten Meilenstein geknüpft.' : 'Planned team growth over 5 years. We start lean with 2 founders and grow based on demand. Each hire is tied to a concrete milestone.' },
|
||||||
cash: { title: de ? 'Liquidität (Jahresende)' : 'Cash Position (Year-End)', desc: de ? 'Kontostand am Ende jedes Jahres. Zeigt ob genug Geld für den laufenden Betrieb vorhanden ist. Die Liquiditätskurve berücksichtigt alle Einnahmen, Ausgaben, Investitionen und Finanzierungen.' : 'Bank balance at end of each year. Shows if enough cash is available for operations. The liquidity curve accounts for all revenue, expenses, investments and financing.' },
|
cash: { title: de ? 'Liquidität (Jahresende)' : 'Cash Position (Year-End)', desc: de ? 'Kontostand am Ende jedes Jahres. Zeigt ob genug Geld für den laufenden Betrieb vorhanden ist. Die Liquiditätskurve berücksichtigt alle Einnahmen, Ausgaben, Investitionen und Finanzierungen.' : 'Bank balance at end of each year. Shows if enough cash is available for operations. The liquidity curve accounts for all revenue, expenses, investments and financing.' },
|
||||||
revcost: { title: de ? 'Umsatz vs. Gesamtkosten' : 'Revenue vs. Total Costs', desc: de ? 'Vergleich zwischen Einnahmen und Ausgaben. Der Schnittpunkt zeigt den Break-Even — ab dann verdient das Unternehmen mehr als es ausgibt.' : 'Comparison between income and expenses. The intersection shows break-even — from then on, the company earns more than it spends.' },
|
revcost: { title: de ? 'Umsatz vs. Gesamtkosten' : 'Revenue vs. Total Costs', desc: de ? 'Vergleich zwischen Einnahmen und Ausgaben. Der Schnittpunkt zeigt den Break-Even — ab dann verdient das Unternehmen mehr als es ausgibt.' : 'Comparison between income and expenses. The intersection shows break-even — from then on, the company earns more than it spends.' },
|
||||||
acv: { title: 'ACV (Average Contract Value)', desc: de ? 'Durchschnittlicher Vertragswert pro Kunde und Jahr. Steigender ACV bedeutet: Kunden kaufen mehr Module oder wechseln in höhere Tiers.' : 'Average contract value per customer per year. Rising ACV means: customers buy more modules or upgrade to higher tiers.' },
|
acv: { title: 'ACV (Average Contract Value)', desc: de ? 'Durchschnittlicher Vertragswert pro Kunde und Jahr. Steigender ACV bedeutet: Kunden kaufen mehr Module oder wechseln in höhere Tiers.' : 'Average contract value per customer per year. Rising ACV means: customers buy more modules or upgrade to higher tiers.' },
|
||||||
grossMargin: { title: 'Gross Margin', desc: de ? 'Rohertragsmarge — wieviel Prozent vom Umsatz nach Abzug der direkten Kosten (Cloud-Infrastruktur, Lizenzen) übrig bleibt. Bei SaaS typisch 70-90%.' : 'How much revenue remains after direct costs (cloud infrastructure, licenses). Typical for SaaS: 70-90%.' },
|
grossMargin: { title: 'Gross Margin', desc: de ? 'Rohertragsmarge — wieviel Prozent vom Umsatz nach Abzug der direkten Kosten (Cloud-Infrastruktur, Lizenzen) übrig bleibt. Bei SaaS typisch 70-90%.' : 'How much revenue remains after direct costs (cloud infrastructure, licenses). Typical for SaaS: 70-90%.' },
|
||||||
nrr: { title: 'NRR (Net Revenue Retention)', desc: de ? 'Umsatzwachstum durch Bestandskunden — zeigt ob bestehende Kunden mehr ausgeben als sie kündigen. NRR > 100% bedeutet: das Geschäft wächst auch ohne Neukunden.' : 'Revenue growth from existing customers — shows if current customers spend more than they churn. NRR > 100% means: the business grows even without new customers.' },
|
nrr: { title: de ? 'Umsatzwachstum (YoY)' : 'Revenue Growth (YoY)', desc: de ? 'Jahresvergleich des Gesamtumsatzes — zeigt die Wachstumsgeschwindigkeit. In der Frühphase primär durch Neukundengewinnung getrieben, später zunehmend durch Expansion bestehender Kunden (Upselling in höhere Tiers).' : 'Year-over-year total revenue comparison — shows growth velocity. In early stages driven primarily by new customer acquisition, later increasingly by expansion of existing customers (upselling to higher tiers).' },
|
||||||
ebitMargin: { title: 'EBIT Margin', desc: de ? 'Operatives Ergebnis in Prozent vom Umsatz. Zeigt die Effizienz des Geschäftsmodells. Ziel für SaaS: 20-30% in der Reifephase.' : 'Operating result as percentage of revenue. Shows business model efficiency. Target for SaaS: 20-30% at maturity.' },
|
ebitMargin: { title: 'EBIT Margin', desc: de ? 'Operatives Ergebnis in Prozent vom Umsatz. Zeigt die Effizienz des Geschäftsmodells. Ziel für SaaS: 20-30% in der Reifephase.' : 'Operating result as percentage of revenue. Shows business model efficiency. Target for SaaS: 20-30% at maturity.' },
|
||||||
}
|
}
|
||||||
const detailInfo = chartDetail ? chartDetails[chartDetail] : null
|
const detailInfo = chartDetail ? chartDetails[chartDetail] : null
|
||||||
@@ -538,7 +545,7 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId,
|
|||||||
{[
|
{[
|
||||||
{ label: 'ACV', key: 'arpu', unit: '€', color: 'text-indigo-300', bg: 'bg-indigo-500/60', detail: 'acv' },
|
{ label: 'ACV', key: 'arpu', unit: '€', color: 'text-indigo-300', bg: 'bg-indigo-500/60', detail: 'acv' },
|
||||||
{ label: 'Gross Margin', key: 'grossMargin', unit: '%', color: 'text-emerald-300', bg: 'bg-emerald-500/60', detail: 'grossMargin' },
|
{ label: 'Gross Margin', key: 'grossMargin', unit: '%', color: 'text-emerald-300', bg: 'bg-emerald-500/60', detail: 'grossMargin' },
|
||||||
{ label: 'NRR', key: 'nrr', unit: '%', color: 'text-purple-300', bg: 'bg-purple-500/60', detail: 'nrr' },
|
{ label: de ? 'Wachstum' : 'Growth', key: 'nrr', unit: '%', color: 'text-purple-300', bg: 'bg-purple-500/60', detail: 'nrr' },
|
||||||
{ label: 'EBIT Margin', key: 'ebitMargin', unit: '%', color: 'text-amber-300', bg: 'bg-amber-500/60', detail: 'ebitMargin' },
|
{ label: 'EBIT Margin', key: 'ebitMargin', unit: '%', color: 'text-amber-300', bg: 'bg-amber-500/60', detail: 'ebitMargin' },
|
||||||
].map((metric, mIdx) => (
|
].map((metric, mIdx) => (
|
||||||
<div key={mIdx} className="text-center cursor-pointer hover:bg-white/[0.03] rounded-lg p-2 transition-colors" onClick={() => setChartDetail(metric.detail)}>
|
<div key={mIdx} className="text-center cursor-pointer hover:bg-white/[0.03] rounded-lg p-2 transition-colors" onClick={() => setChartDetail(metric.detail)}>
|
||||||
@@ -571,8 +578,137 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId,
|
|||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
|
{/* === Kontenrahmen SKR04 === */}
|
||||||
|
{activeSheet === 'skr' && (() => {
|
||||||
|
const skr04: { klasse: string; title: string; color: string; accounts: { nr: string; name: string; used?: boolean }[] }[] = [
|
||||||
|
{ klasse: '0', title: de ? 'Anlagevermögen' : 'Fixed Assets', color: 'text-slate-400', accounts: [
|
||||||
|
{ nr: '0200', name: de ? 'Technische Anlagen & Maschinen' : 'Technical Equipment', used: false },
|
||||||
|
{ nr: '0400', name: de ? 'Betriebs- und Geschäftsausstattung' : 'Office & Business Equipment', used: true },
|
||||||
|
{ nr: '0420', name: de ? 'EDV-Hardware' : 'IT Hardware', used: true },
|
||||||
|
{ nr: '0440', name: de ? 'Geringwertige Wirtschaftsgüter (GWG)' : 'Low-Value Assets (GWG)', used: true },
|
||||||
|
{ nr: '0480', name: de ? 'Immaterielle Vermögensgegenstände' : 'Intangible Assets', used: true },
|
||||||
|
]},
|
||||||
|
{ klasse: '1', title: de ? 'Umlaufvermögen' : 'Current Assets', color: 'text-blue-400', accounts: [
|
||||||
|
{ nr: '1200', name: de ? 'Bank (Geschäftskonto)' : 'Bank (Business Account)', used: true },
|
||||||
|
{ nr: '1210', name: de ? 'Bank (Festgeld/Rücklagen)' : 'Bank (Fixed Deposit)', used: false },
|
||||||
|
{ nr: '1400', name: de ? 'Forderungen aus L&L' : 'Accounts Receivable', used: true },
|
||||||
|
{ nr: '1590', name: de ? 'Durchlaufende Posten' : 'Transit Items', used: false },
|
||||||
|
]},
|
||||||
|
{ klasse: '2', title: de ? 'Eigenkapital & Verbindlichkeiten' : 'Equity & Liabilities', color: 'text-purple-400', accounts: [
|
||||||
|
{ nr: '2000', name: de ? 'Gezeichnetes Kapital (Stammkapital)' : 'Share Capital', used: true },
|
||||||
|
{ nr: '2010', name: de ? 'Kapitalrücklage (Wandeldarlehen)' : 'Capital Reserve (Convertible Loan)', used: true },
|
||||||
|
{ nr: '2100', name: de ? 'Gewinnvortrag / Verlustvortrag' : 'Retained Earnings / Losses', used: true },
|
||||||
|
{ nr: '2900', name: de ? 'Jahresüberschuss / -fehlbetrag' : 'Net Income / Loss', used: true },
|
||||||
|
]},
|
||||||
|
{ klasse: '3', title: de ? 'Rückstellungen & Verbindlichkeiten' : 'Provisions & Payables', color: 'text-indigo-400', accounts: [
|
||||||
|
{ nr: '3070', name: de ? 'Verbindlichkeiten ggü. Kreditinstituten' : 'Bank Liabilities', used: true },
|
||||||
|
{ nr: '3100', name: de ? 'Verbindlichkeiten aus L&L' : 'Accounts Payable', used: true },
|
||||||
|
{ nr: '3150', name: de ? 'Darlehen (L-Bank Pre-Seed)' : 'Loan (L-Bank Pre-Seed)', used: true },
|
||||||
|
{ nr: '3500', name: de ? 'Umsatzsteuer-Verbindlichkeit' : 'VAT Payable', used: true },
|
||||||
|
{ nr: '3520', name: de ? 'Lohnsteuer-Verbindlichkeit' : 'Payroll Tax Payable', used: true },
|
||||||
|
{ nr: '3550', name: de ? 'Sozialversicherungs-Verbindlichkeit' : 'Social Security Payable', used: true },
|
||||||
|
{ nr: '3900', name: de ? 'Rückstellungen (Jahresabschluss etc.)' : 'Provisions (Closing etc.)', used: true },
|
||||||
|
]},
|
||||||
|
{ klasse: '4', title: de ? 'Betriebliche Erträge' : 'Operating Revenue', color: 'text-emerald-400', accounts: [
|
||||||
|
{ nr: '4400', name: de ? 'Erlöse Software-Lizenzen (SaaS)' : 'Software License Revenue (SaaS)', used: true },
|
||||||
|
{ nr: '4410', name: de ? 'Erlöse Beratung & Service' : 'Consulting & Service Revenue', used: true },
|
||||||
|
{ nr: '4440', name: de ? 'Erlöse Hardware (Mac Mini/Studio)' : 'Hardware Revenue', used: false },
|
||||||
|
{ nr: '4500', name: de ? 'Sonstige betriebliche Erträge' : 'Other Operating Revenue', used: true },
|
||||||
|
{ nr: '4510', name: de ? 'Fördergelder / Grants' : 'Grants / Subsidies', used: true },
|
||||||
|
{ nr: '4520', name: de ? 'Forschungszulage (§ 27a EStG)' : 'Research Tax Credit', used: true },
|
||||||
|
]},
|
||||||
|
{ klasse: '5', title: de ? 'Materialaufwand / COGS' : 'Material Costs / COGS', color: 'text-orange-400', accounts: [
|
||||||
|
{ nr: '5400', name: de ? 'Cloud-Hosting (SysEleven/Hetzner)' : 'Cloud Hosting', used: true },
|
||||||
|
{ nr: '5410', name: de ? 'LLM-Inferenzkosten' : 'LLM Inference Costs', used: true },
|
||||||
|
{ nr: '5420', name: de ? '3rd Party API (Tavily etc.)' : '3rd Party API', used: true },
|
||||||
|
{ nr: '5430', name: de ? 'Datenbank-Hosting (PostgreSQL/Qdrant)' : 'Database Hosting', used: true },
|
||||||
|
{ nr: '5440', name: de ? 'CDN / Storage / Monitoring' : 'CDN / Storage / Monitoring', used: true },
|
||||||
|
]},
|
||||||
|
{ klasse: '6', title: de ? 'Personalaufwand' : 'Personnel Costs', color: 'text-cyan-400', accounts: [
|
||||||
|
{ nr: '6000', name: de ? 'Löhne und Gehälter' : 'Wages and Salaries', used: true },
|
||||||
|
{ nr: '6010', name: de ? 'Geschäftsführer-Gehälter' : 'Managing Director Salaries', used: true },
|
||||||
|
{ nr: '6100', name: de ? 'Soziale Abgaben (AG-Anteil)' : 'Social Security (Employer)', used: true },
|
||||||
|
{ nr: '6110', name: de ? 'Berufsgenossenschaft (VBG)' : 'Professional Association (VBG)', used: true },
|
||||||
|
{ nr: '6130', name: de ? 'Vermögenswirksame Leistungen' : 'Capital-Forming Benefits', used: false },
|
||||||
|
{ nr: '6170', name: de ? 'Freiwillige Sozialleistungen' : 'Voluntary Social Benefits', used: false },
|
||||||
|
]},
|
||||||
|
{ klasse: '7', title: de ? 'Sonstige betriebliche Aufwendungen' : 'Other Operating Expenses', color: 'text-amber-400', accounts: [
|
||||||
|
{ nr: '7000', name: de ? 'Raumkosten / Miete' : 'Rent / Room Costs', used: true },
|
||||||
|
{ nr: '7100', name: de ? 'Versicherungen (D&O, Cyber, Haftpflicht)' : 'Insurance (D&O, Cyber, Liability)', used: true },
|
||||||
|
{ nr: '7200', name: de ? 'Fahrzeugkosten / KFZ' : 'Vehicle Costs', used: true },
|
||||||
|
{ nr: '7300', name: de ? 'Werbe- und Marketingkosten' : 'Marketing Costs', used: true },
|
||||||
|
{ nr: '7310', name: de ? 'Teilnahme an Messen' : 'Trade Fair Participation', used: true },
|
||||||
|
{ nr: '7320', name: de ? 'Bewirtungskosten' : 'Entertainment Costs', used: true },
|
||||||
|
{ nr: '7400', name: de ? 'Reisekosten' : 'Travel Costs', used: true },
|
||||||
|
{ nr: '7500', name: de ? 'Internet / Mobilfunk' : 'Internet / Mobile', used: true },
|
||||||
|
{ nr: '7510', name: de ? 'Serverkosten / Cloud (→ Klasse 5)' : 'Server / Cloud (→ Class 5)', used: false },
|
||||||
|
{ nr: '7600', name: de ? 'Rechts-/Beratungskosten' : 'Legal / Advisory Costs', used: true },
|
||||||
|
{ nr: '7610', name: de ? 'Buchführungskosten' : 'Bookkeeping Costs', used: true },
|
||||||
|
{ nr: '7620', name: de ? 'Jahresabschlusskosten' : 'Annual Closing Costs', used: true },
|
||||||
|
{ nr: '7630', name: de ? 'Ext. Datenschutzbeauftragter' : 'Ext. Data Protection Officer', used: true },
|
||||||
|
{ nr: '7640', name: de ? 'Zertifizierung (ISO 27001 / BSI C5)' : 'Certification (ISO 27001 / BSI C5)', used: true },
|
||||||
|
{ nr: '7650', name: de ? 'Recruiting / Stellenanzeigen' : 'Recruiting / Job Ads', used: true },
|
||||||
|
{ nr: '7680', name: de ? 'IHK / Kammerbeiträge' : 'Chamber of Commerce Fees', used: true },
|
||||||
|
{ nr: '7690', name: de ? 'Rundfunkbeitrag' : 'Broadcasting Fee', used: true },
|
||||||
|
{ nr: '7700', name: de ? 'Abschreibungen (AfA)' : 'Depreciation', used: true },
|
||||||
|
{ nr: '7750', name: de ? 'Fort- und Weiterbildung' : 'Training & Development', used: true },
|
||||||
|
{ nr: '7800', name: de ? 'Bankgebühren / Nebenkosten Geldverkehr' : 'Bank Fees', used: true },
|
||||||
|
{ nr: '7900', name: de ? 'Schutzrechte / Lizenzkosten' : 'IP Rights / License Costs', used: true },
|
||||||
|
]},
|
||||||
|
{ klasse: '8', title: de ? 'Finanzerträge & -aufwendungen' : 'Financial Income & Expenses', color: 'text-red-400', accounts: [
|
||||||
|
{ nr: '8100', name: de ? 'Zinserträge' : 'Interest Income', used: false },
|
||||||
|
{ nr: '8200', name: de ? 'Zinsaufwendungen' : 'Interest Expenses', used: true },
|
||||||
|
{ nr: '8210', name: de ? 'Zinsen L-Bank Wandeldarlehen (8%)' : 'Interest L-Bank Convertible (8%)', used: true },
|
||||||
|
]},
|
||||||
|
{ klasse: '9', title: de ? 'Steuern & Jahresabschluss' : 'Taxes & Closing', color: 'text-rose-400', accounts: [
|
||||||
|
{ nr: '9000', name: de ? 'Gewerbesteuer' : 'Trade Tax', used: true },
|
||||||
|
{ nr: '9100', name: de ? 'Körperschaftsteuer + Soli' : 'Corporate Tax + Surcharge', used: true },
|
||||||
|
{ nr: '9200', name: de ? 'Umsatzsteuer (Zahllast)' : 'VAT (Payable)', used: true },
|
||||||
|
{ nr: '9300', name: de ? 'Lohnsteuer' : 'Payroll Tax', used: true },
|
||||||
|
]},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GlassCard hover={false} className="p-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-sm font-bold text-white/80">{de ? 'Kontenrahmen SKR04 — Breakpilot COMPLAI GmbH' : 'Chart of Accounts SKR04 — Breakpilot COMPLAI GmbH'}</h3>
|
||||||
|
<div className="flex items-center gap-3 text-[9px]">
|
||||||
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-emerald-400 inline-block" /> {de ? 'Aktiv genutzt' : 'Actively used'}</span>
|
||||||
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-white/15 inline-block" /> {de ? 'Geplant / nicht aktiv' : 'Planned / inactive'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{skr04.map(k => (
|
||||||
|
<div key={k.klasse}>
|
||||||
|
<button onClick={() => toggleSKR(k.klasse)} className="w-full flex items-center gap-2 py-1.5 px-2 rounded hover:bg-white/[0.03] transition-colors text-left">
|
||||||
|
<span className="text-[10px] text-white/30 w-3">{openSKR.has(k.klasse) ? '▾' : '▸'}</span>
|
||||||
|
<span className={`text-xs font-bold ${k.color}`}>Klasse {k.klasse}</span>
|
||||||
|
<span className="text-xs text-white/60">— {k.title}</span>
|
||||||
|
<span className="text-[9px] text-white/25 ml-auto">{k.accounts.filter(a => a.used).length}/{k.accounts.length}</span>
|
||||||
|
</button>
|
||||||
|
{openSKR.has(k.klasse) && (
|
||||||
|
<div className="ml-7 mb-2 space-y-0.5">
|
||||||
|
{k.accounts.map(a => (
|
||||||
|
<div key={a.nr} className={`flex items-center gap-2 py-0.5 px-2 rounded text-[11px] ${a.used ? 'text-white/70' : 'text-white/25'}`}>
|
||||||
|
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${a.used ? 'bg-emerald-400' : 'bg-white/15'}`} />
|
||||||
|
<span className="font-mono text-white/30 w-10">{a.nr}</span>
|
||||||
|
<span>{a.name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 pt-2 border-t border-white/5 text-[10px] text-white/25 text-center">
|
||||||
|
SKR04 (Industriekontenrahmen) · {de ? 'Angepasst für SaaS/Tech GmbH' : 'Adapted for SaaS/Tech GmbH'} · {de ? '10 Klassen · 62 Konten' : '10 classes · 62 accounts'}
|
||||||
|
</div>
|
||||||
|
</GlassCard>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Year Navigation — not for GuV, KPIs, Charts */}
|
{/* Year Navigation — not for GuV, KPIs, Charts */}
|
||||||
{!['guv', 'kpis', 'charts'].includes(activeSheet) && (
|
{!['guv', 'kpis', 'charts', 'skr'].includes(activeSheet) && (
|
||||||
<div className="flex items-center justify-center gap-1 mb-2">
|
<div className="flex items-center justify-center gap-1 mb-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setYearOffset(-1)}
|
onClick={() => setYearOffset(-1)}
|
||||||
@@ -593,7 +729,7 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId,
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Data Grid — not shown for KPIs and Charts */}
|
{/* Data Grid — not shown for KPIs and Charts */}
|
||||||
{!['kpis', 'charts'].includes(activeSheet) && (
|
{!['kpis', 'charts', 'skr'].includes(activeSheet) && (
|
||||||
<GlassCard hover={false} className="p-2 overflow-x-auto">
|
<GlassCard hover={false} className="p-2 overflow-x-auto">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-8 text-white/30 text-sm">{de ? 'Lade...' : 'Loading...'}</div>
|
<div className="text-center py-8 text-white/30 text-sm">{de ? 'Lade...' : 'Loading...'}</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function ProjectionFooter({ lang }: ProjectionFooterProps) {
|
|||||||
const de = lang === 'de'
|
const de = lang === 'de'
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 pt-2 border-t border-white/5">
|
<div className="mt-3 pt-2 border-t border-white/5">
|
||||||
<p className="text-[9px] text-white/20 text-center italic">
|
<p className="text-xs text-white/30 text-center italic">
|
||||||
{de
|
{de
|
||||||
? 'Alle Finanzdaten sind Planzahlen und stellen keine Garantie für künftige Ergebnisse dar (Stand: Q2 2026)'
|
? 'Alle Finanzdaten sind Planzahlen und stellen keine Garantie für künftige Ergebnisse dar (Stand: Q2 2026)'
|
||||||
: 'All financial data are projections and do not constitute a guarantee of future results (as of: Q2 2026)'}
|
: 'All financial data are projections and do not constitute a guarantee of future results (as of: Q2 2026)'}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const PUBLIC_PATHS = [
|
|||||||
'/auth', // investor login pages
|
'/auth', // investor login pages
|
||||||
'/api/auth', // investor auth API
|
'/api/auth', // investor auth API
|
||||||
'/api/health',
|
'/api/health',
|
||||||
|
'/api/admin/fp-patch',
|
||||||
'/api/admin-auth', // admin login API
|
'/api/admin-auth', // admin login API
|
||||||
'/pitch-admin/login', // admin login page
|
'/pitch-admin/login', // admin login page
|
||||||
'/_next',
|
'/_next',
|
||||||
|
|||||||
Reference in New Issue
Block a user