feat: Finanzplan Phase 1-4 — DB + Engine + API + Spreadsheet-UI

Phase 1: DB-Schema (12 fp_* Tabellen) + Excel-Import (332 Zeilen importiert)
Phase 2: Compute Engine (Personal, Invest, Umsatz, Material, Betrieblich, Liquiditaet, GuV)
Phase 3: API (/api/finanzplan/ — GET sheets, PUT cells, POST compute)
Phase 4: Spreadsheet-UI (FinanzplanSlide als Annex mit Tab-Leiste, editierbarem Grid, Jahres-Navigation)

Zusaetzlich:
- Gruendungsdatum verschoben: Feb→Aug 2026 (DB + Personalkosten)
- Neue Preisstaffel: Startup/<10 MA ab 3.600 EUR/Jahr (14-Tage-Test, Kreditkarte)
- Competition-Slide: Pricing-Tiers aktualisiert

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-26 19:26:46 +01:00
parent f514667ef9
commit a58cd16f01
16 changed files with 4589 additions and 5 deletions

View File

@@ -0,0 +1,100 @@
import { NextRequest, NextResponse } from 'next/server'
import pool from '@/lib/db'
const TABLE_MAP: Record<string, string> = {
kunden: 'fp_kunden',
kunden_summary: 'fp_kunden_summary',
umsatzerloese: 'fp_umsatzerloese',
materialaufwand: 'fp_materialaufwand',
personalkosten: 'fp_personalkosten',
betriebliche: 'fp_betriebliche_aufwendungen',
investitionen: 'fp_investitionen',
sonst_ertraege: 'fp_sonst_ertraege',
liquiditaet: 'fp_liquiditaet',
guv: 'fp_guv',
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ sheetName: string }> }
) {
const { sheetName } = await params
const table = TABLE_MAP[sheetName]
if (!table) {
return NextResponse.json({ error: `Unknown sheet: ${sheetName}` }, { status: 400 })
}
const scenarioId = request.nextUrl.searchParams.get('scenarioId')
try {
let query = `SELECT * FROM ${table}`
const params: string[] = []
if (scenarioId) {
query += ' WHERE scenario_id = $1'
params.push(scenarioId)
} else {
query += ' WHERE scenario_id = (SELECT id FROM fp_scenarios WHERE is_default = true LIMIT 1)'
}
query += ' ORDER BY sort_order'
const { rows } = await pool.query(query, params)
return NextResponse.json({ sheet: sheetName, rows })
} catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 })
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ sheetName: string }> }
) {
const { sheetName } = await params
const table = TABLE_MAP[sheetName]
if (!table) {
return NextResponse.json({ error: `Unknown sheet: ${sheetName}` }, { status: 400 })
}
try {
const body = await request.json()
const { rowId, updates } = body // updates: { field: value } or { m3: 1500 } for monthly values
if (!rowId) {
return NextResponse.json({ error: 'rowId required' }, { status: 400 })
}
// Check if updating monthly values (JSONB) or scalar fields
const monthlyKeys = Object.keys(updates).filter(k => k.startsWith('m') && !isNaN(parseInt(k.substring(1))))
const scalarKeys = Object.keys(updates).filter(k => !k.startsWith('m') || isNaN(parseInt(k.substring(1))))
if (monthlyKeys.length > 0) {
// Update specific months in the values JSONB
const jsonbSet = monthlyKeys.map(k => `'${k}', '${updates[k]}'::jsonb`).join(', ')
const valuesCol = sheetName === 'personalkosten' ? 'values_brutto' : 'values'
// Use jsonb_set for each key
let updateSql = `UPDATE ${table} SET `
const setClauses: string[] = []
for (const k of monthlyKeys) {
setClauses.push(`${valuesCol} = jsonb_set(${valuesCol}, '{${k}}', '${updates[k]}')`)
}
setClauses.push(`updated_at = NOW()`)
updateSql += setClauses.join(', ') + ` WHERE id = $1`
await pool.query(updateSql, [rowId])
}
if (scalarKeys.length > 0) {
// Update scalar columns directly
const setClauses = scalarKeys.map((k, i) => `${k} = $${i + 2}`).join(', ')
await pool.query(
`UPDATE ${table} SET ${setClauses}, updated_at = NOW() WHERE id = $1`,
[rowId, ...scalarKeys.map(k => updates[k])]
)
}
// Return updated row
const { rows } = await pool.query(`SELECT * FROM ${table} WHERE id = $1`, [rowId])
return NextResponse.json({ updated: rows[0] })
} catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 })
}
}