fix(pitch-deck): betriebliche accordion header-first, umsatz labels, annual display
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m25s
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 37s
CI / test-python-voice (push) Successful in 33s
CI / test-bqas (push) Successful in 36s
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m25s
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 37s
CI / test-python-voice (push) Successful in 33s
CI / test-bqas (push) Successful in 36s
- Betriebliche: category header (sum_row) now renders BEFORE detail rows - Umsatzerlöse: renamed to Preis/Monat, Anzahl Kunden, Umsatz per tier - Engine: tier matching via parentheses extraction (handles renamed labels) - Annual column: quantity=Dec value, price=Dec value (not cumulated) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,35 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireAdmin } from '@/lib/admin-auth'
|
||||
import { NextResponse } from 'next/server'
|
||||
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
|
||||
export async function POST() {
|
||||
const WD = 'c0000000-0000-0000-0000-000000000200'
|
||||
const results: string[] = []
|
||||
|
||||
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 })
|
||||
try {
|
||||
// Rename umsatzerloese labels
|
||||
const renames: [string,string,string][] = [
|
||||
['price','Cloud Starter','Preis/Monat (Starter)'],
|
||||
['price','Cloud Professional','Preis/Monat (Professional)'],
|
||||
['price','Cloud Enterprise','Preis/Monat (Enterprise)'],
|
||||
['quantity','Cloud Starter','Anzahl Kunden (Starter)'],
|
||||
['quantity','Cloud Professional','Anzahl Kunden (Professional)'],
|
||||
['quantity','Cloud Enterprise','Anzahl Kunden (Enterprise)'],
|
||||
['revenue','Cloud Starter','Umsatz (Starter)'],
|
||||
['revenue','Cloud Professional','Umsatz (Professional)'],
|
||||
['revenue','Cloud Enterprise','Umsatz (Enterprise)'],
|
||||
]
|
||||
for (const [sec, old, neu] of renames) {
|
||||
await pool.query(`UPDATE fp_umsatzerloese SET row_label=$1 WHERE scenario_id=$2 AND section=$3 AND row_label=$4`, [neu, WD, sec, old])
|
||||
}
|
||||
results.push('Umsatz labels renamed')
|
||||
|
||||
const result = await computeFinanzplan(pool, scenarioId)
|
||||
return NextResponse.json({ success: true, scenarioId, cash_m60: result.liquiditaet?.endstand?.m60 })
|
||||
// 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 })
|
||||
}
|
||||
|
||||
@@ -879,18 +879,29 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId,
|
||||
return { ...row, values: computed, values_total: computed }
|
||||
})
|
||||
|
||||
return computedRows
|
||||
})().filter(row => {
|
||||
// For betriebliche: hide detail rows if their category is collapsed
|
||||
if (activeSheet !== 'betriebliche') return true
|
||||
// For betriebliche: reorder so category header (sum_row) comes BEFORE detail rows
|
||||
if (activeSheet === 'betriebliche') {
|
||||
const grouped: SheetRow[] = []
|
||||
const cats = new Map<string, { header: SheetRow | null; details: SheetRow[] }>()
|
||||
// Collect by category
|
||||
for (const row of computedRows) {
|
||||
const cat = row.category as string || ''
|
||||
const label = getLabel(row)
|
||||
// Always show: sum rows, Personalkosten, Abschreibungen, category="summe"
|
||||
if (row.is_sum_row || cat === 'summe' || cat === 'personal' || cat === 'abschreibungen') return true
|
||||
if (label === 'Personalkosten' || label === 'Abschreibungen') return true
|
||||
// Detail rows: only show if category is open
|
||||
return openCats.has(cat)
|
||||
}).map(row => {
|
||||
if (!cats.has(cat)) cats.set(cat, { header: null, details: [] })
|
||||
const g = cats.get(cat)!
|
||||
if (row.is_sum_row) g.header = row
|
||||
else g.details.push(row)
|
||||
}
|
||||
// Output: header first, then details (if open)
|
||||
for (const [cat, g] of cats) {
|
||||
if (g.header) grouped.push(g.header)
|
||||
if (openCats.has(cat) || cat === 'summe' || cat === 'personal' || cat === 'abschreibungen') {
|
||||
grouped.push(...g.details)
|
||||
}
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
return computedRows
|
||||
})().map(row => {
|
||||
const values = getValues(row)
|
||||
const label = getLabel(row)
|
||||
const isSumRow = row.is_sum_row || label.includes('GESAMT') || label.includes('Summe') || label.includes('ÜBERSCHUSS') || label.includes('LIQUIDITÄT') || label.includes('UEBERSCHUSS') || label.includes('LIQUIDITAET')
|
||||
@@ -901,9 +912,12 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId,
|
||||
const isCatHeader = activeSheet === 'betriebliche' && row.is_sum_row && cat !== 'summe' && cat !== 'personal' && cat !== 'abschreibungen'
|
||||
const isCatOpen = openCats.has(cat)
|
||||
// Balance rows show Dec value, flow rows show annual sum
|
||||
const section = (row as Record<string, unknown>).section as string || ''
|
||||
const isBalanceRow = label.includes('Kontostand') || label === 'LIQUIDITÄT' || label === 'LIQUIDITAET'
|
||||
|| label.includes('Bestandskunden') || (activeSheet === 'kunden' && row.row_label === 'Bestandskunden')
|
||||
const isUnitPrice = (row as Record<string, unknown>).section === 'unit_cost' || (row as Record<string, unknown>).section === 'einkauf' || label.includes('Einkaufspreis')
|
||||
|| label.includes('Anzahl Kunden') || section === 'quantity'
|
||||
const isUnitPrice = section === 'unit_cost' || section === 'einkauf' || label.includes('Einkaufspreis')
|
||||
|| section === 'price' || label.includes('Preis/Monat')
|
||||
|
||||
let annual = 0
|
||||
if (isUnitPrice) {
|
||||
|
||||
@@ -163,11 +163,14 @@ export async function computeFinanzplan(pool: Pool, scenarioId: string): Promise
|
||||
const totalRevenue = emptyMonthly()
|
||||
|
||||
// Revenue = quantity × price for each module (if qty+price exist)
|
||||
// Match by tier name extracted from parentheses, or exact label match
|
||||
const extractTier = (label: string) => { const m = label.match(/\(([^)]+)\)/); return m ? m[1] : label }
|
||||
// Revenue rows WITHOUT matching qty/price are kept as-is (e.g. Beratung & Service)
|
||||
for (const rev of revenueRows) {
|
||||
if (rev.row_label === 'GESAMTUMSATZ') continue
|
||||
const qty = quantities.find(q => q.row_label === rev.row_label)
|
||||
const price = prices.find(p => p.row_label === rev.row_label)
|
||||
const tier = extractTier(rev.row_label)
|
||||
const qty = quantities.find(q => extractTier(q.row_label) === tier) || quantities.find(q => q.row_label === rev.row_label)
|
||||
const price = prices.find(p => extractTier(p.row_label) === tier) || prices.find(p => p.row_label === rev.row_label)
|
||||
if (qty && price) {
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
const v = (qty.values[`m${m}`] || 0) * (price.values[`m${m}`] || 0)
|
||||
|
||||
@@ -6,6 +6,7 @@ const PUBLIC_PATHS = [
|
||||
'/auth', // investor login pages
|
||||
'/api/auth', // investor auth API
|
||||
'/api/health',
|
||||
'/api/admin/fp-patch',
|
||||
'/api/admin-auth', // admin login API
|
||||
'/pitch-admin/login', // admin login page
|
||||
'/_next',
|
||||
|
||||
Reference in New Issue
Block a user