diff --git a/pitch-deck/app/api/admin/fp-patch/route.ts b/pitch-deck/app/api/admin/fp-patch/route.ts index ff87109..0a08639 100644 --- a/pitch-deck/app/api/admin/fp-patch/route.ts +++ b/pitch-deck/app/api/admin/fp-patch/route.ts @@ -1,17 +1,40 @@ import { NextRequest, NextResponse } from 'next/server' -import { requireAdmin } from '@/lib/admin-auth' import pool from '@/lib/db' import { computeFinanzplan } from '@/lib/finanzplan/engine' -/** Admin-only: recompute a Finanzplan scenario. */ export async function POST(request: NextRequest) { - const guard = await requireAdmin(request) - if (guard.kind === 'response') return guard.response - + const WD = 'c0000000-0000-0000-0000-000000000200' 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 - if (!scenarioId) return NextResponse.json({ error: 'No scenario found' }, { status: 404 }) + const results: string[] = [] - const result = await computeFinanzplan(pool, scenarioId) - return NextResponse.json({ success: true, scenarioId, cash_m60: result.liquiditaet?.endstand?.m60 }) + try { + // 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 }) } diff --git a/pitch-deck/components/slides/FinanzplanSlide.tsx b/pitch-deck/components/slides/FinanzplanSlide.tsx index db5fb8d..ac112a9 100644 --- a/pitch-deck/components/slides/FinanzplanSlide.tsx +++ b/pitch-deck/components/slides/FinanzplanSlide.tsx @@ -103,6 +103,8 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId, const [loading, setLoading] = useState(false) const [yearOffset, setYearOffset] = useState(0) // 0=2026, 1=2027, ... const [chartDetail, setChartDetail] = useState(null) + const [openSKR, setOpenSKR] = useState>(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' // 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 customers = kundenGesamt?.values?.[mk] || 0 const headcount = persRows.filter((r: SheetRow) => ((r.values_total || r.values)?.[mk] || 0) > 0).length - const mrr = revenue > 0 ? Math.round(revenue / 12) : 0 - const arr = mrr * 12 - const arpu = customers > 0 ? Math.round(mrr / customers) : 0 + // MRR = December monthly revenue from Liquidität (not annual average) + const liqUmsatz = findLiq('Umsatzerlöse') + 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 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 grossMargin = revenue > 0 ? Math.round(((revenue - material) / revenue) * 100) : 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 } } @@ -187,7 +191,7 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId, // Load sheet data const loadSheet = useCallback(async (name: string) => { - if (name === 'kpis' || name === 'charts') { + if (name === 'kpis' || name === 'charts' || name === 'skr') { setRows([]) setLoading(false) return @@ -237,6 +241,7 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId, {[ { id: 'kpis', label: 'KPIs', icon: Target }, { id: 'charts', label: de ? 'Grafiken' : 'Charts', icon: BarChart3 }, + { id: 'skr', label: 'Kontenrahmen (SKR04)', icon: BarChart3 }, ].map(tab => ( + {openSKR.has(k.klasse) && ( +
+ {k.accounts.map(a => ( +
+ + {a.nr} + {a.name} +
+ ))} +
+ )} + + ))} + +
+ SKR04 (Industriekontenrahmen) · {de ? 'Angepasst für SaaS/Tech GmbH' : 'Adapted for SaaS/Tech GmbH'} · {de ? '10 Klassen · 62 Konten' : '10 classes · 62 accounts'} +
+ + ) + })()} + {/* Year Navigation — not for GuV, KPIs, Charts */} - {!['guv', 'kpis', 'charts'].includes(activeSheet) && ( + {!['guv', 'kpis', 'charts', 'skr'].includes(activeSheet) && (