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:
@@ -41,6 +41,7 @@ import RegulatorySlide from './slides/RegulatorySlide'
|
||||
import EngineeringSlide from './slides/EngineeringSlide'
|
||||
import AIPipelineSlide from './slides/AIPipelineSlide'
|
||||
import SDKDemoSlide from './slides/SDKDemoSlide'
|
||||
import FinanzplanSlide from './slides/FinanzplanSlide'
|
||||
|
||||
interface PitchDeckProps {
|
||||
lang: Language
|
||||
@@ -163,6 +164,8 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
|
||||
return <AIPipelineSlide lang={lang} />
|
||||
case 'annex-sdk-demo':
|
||||
return <SDKDemoSlide lang={lang} />
|
||||
case 'annex-finanzplan':
|
||||
return <FinanzplanSlide lang={lang} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -280,7 +280,8 @@ const PRICING_COMPARISON: CompetitorPricing[] = [
|
||||
model: 'Cloud (BSI DE / OVH FR)',
|
||||
publicPricing: true,
|
||||
tiers: [
|
||||
{ name: { de: '<50 MA', en: '<50 emp.' }, price: 'ab €1.250/mo', annual: 'ab €15.000/yr', notes: { de: 'Cloud, modular, 84 Regularien', en: 'Cloud, modular, 84 regulations' } },
|
||||
{ name: { de: 'Startup/<10', en: 'Startup/<10' }, price: 'ab €300/mo', annual: 'ab €3.600/yr', notes: { de: '14-Tage-Test, Kreditkarte', en: '14-day trial, credit card' } },
|
||||
{ name: { de: '10-50 MA', en: '10-50 emp.' }, price: 'ab €1.250/mo', annual: 'ab €15.000/yr', notes: { de: 'Cloud, modular, 84 Regularien', en: 'Cloud, modular, 84 regulations' } },
|
||||
{ name: { de: '50-250 MA', en: '50-250 emp.' }, price: 'ab €2.500/mo', annual: 'ab €30.000/yr', notes: { de: 'Cloud, alle Module, Priority', en: 'Cloud, all modules, priority' } },
|
||||
{ name: { de: '250+ MA', en: '250+ emp.' }, price: 'ab €3.500/mo', annual: 'ab €40.000/yr', notes: { de: 'Cloud, Enterprise, Dedicated', en: 'Cloud, enterprise, dedicated' } },
|
||||
],
|
||||
|
||||
251
pitch-deck/components/slides/FinanzplanSlide.tsx
Normal file
251
pitch-deck/components/slides/FinanzplanSlide.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import { RefreshCw, Download, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
|
||||
interface FinanzplanSlideProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
interface SheetMeta {
|
||||
name: string
|
||||
label_de: string
|
||||
label_en: string
|
||||
rows: number
|
||||
}
|
||||
|
||||
interface SheetRow {
|
||||
id: number
|
||||
row_label?: string
|
||||
person_name?: string
|
||||
item_name?: string
|
||||
category?: string
|
||||
section?: string
|
||||
is_editable?: boolean
|
||||
is_sum_row?: boolean
|
||||
values?: Record<string, number>
|
||||
values_total?: Record<string, number>
|
||||
values_brutto?: Record<string, number>
|
||||
brutto_monthly?: number
|
||||
position?: string
|
||||
start_date?: string
|
||||
purchase_amount?: number
|
||||
}
|
||||
|
||||
const MONTH_LABELS = [
|
||||
'Jan', 'Feb', 'Mrz', 'Apr', 'Mai', 'Jun',
|
||||
'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez',
|
||||
]
|
||||
|
||||
function getLabel(row: SheetRow): string {
|
||||
return row.row_label || row.person_name || row.item_name || '—'
|
||||
}
|
||||
|
||||
function getValues(row: SheetRow): Record<string, number> {
|
||||
return row.values || row.values_total || row.values_brutto || {}
|
||||
}
|
||||
|
||||
function formatCell(v: number | undefined): string {
|
||||
if (v === undefined || v === null) return ''
|
||||
if (v === 0) return '—'
|
||||
if (Math.abs(v) >= 1000) return v.toLocaleString('de-DE', { maximumFractionDigits: 0 })
|
||||
return v.toLocaleString('de-DE', { maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
export default function FinanzplanSlide({ lang }: FinanzplanSlideProps) {
|
||||
const [sheets, setSheets] = useState<SheetMeta[]>([])
|
||||
const [activeSheet, setActiveSheet] = useState<string>('personalkosten')
|
||||
const [rows, setRows] = useState<SheetRow[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [computing, setComputing] = useState(false)
|
||||
const [yearOffset, setYearOffset] = useState(0) // 0=2026, 1=2027, ...
|
||||
const de = lang === 'de'
|
||||
|
||||
// Load sheet list
|
||||
useEffect(() => {
|
||||
fetch('/api/finanzplan')
|
||||
.then(r => r.json())
|
||||
.then(data => setSheets(data.sheets || []))
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
// Load sheet data
|
||||
const loadSheet = useCallback(async (name: string) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const r = await fetch(`/api/finanzplan/${name}`)
|
||||
const data = await r.json()
|
||||
setRows(data.rows || [])
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => { loadSheet(activeSheet) }, [activeSheet, loadSheet])
|
||||
|
||||
// Compute
|
||||
const handleCompute = async () => {
|
||||
setComputing(true)
|
||||
try {
|
||||
await fetch('/api/finanzplan/compute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
||||
await loadSheet(activeSheet)
|
||||
} catch { /* ignore */ }
|
||||
setComputing(false)
|
||||
}
|
||||
|
||||
// Cell edit
|
||||
const handleCellEdit = async (rowId: number, monthKey: string, newValue: string) => {
|
||||
const numVal = parseFloat(newValue.replace(/[^\d.-]/g, ''))
|
||||
if (isNaN(numVal)) return
|
||||
|
||||
try {
|
||||
await fetch(`/api/finanzplan/${activeSheet}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ rowId, updates: { [monthKey]: numVal } }),
|
||||
})
|
||||
await loadSheet(activeSheet)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const currentYear = 2026 + yearOffset
|
||||
const monthStart = yearOffset * 12 + 1
|
||||
const monthEnd = monthStart + 11
|
||||
|
||||
return (
|
||||
<div className="max-w-[95vw] mx-auto">
|
||||
<FadeInView className="text-center mb-4">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-1">
|
||||
<GradientText>{de ? 'Finanzplan' : 'Financial Plan'}</GradientText>
|
||||
</h2>
|
||||
<p className="text-sm text-white/40">{de ? '2026–2030 · Monatliche Granularitaet · Editierbar' : '2026–2030 · Monthly Granularity · Editable'}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* Tab Bar */}
|
||||
<div className="flex items-center gap-1 mb-3 overflow-x-auto pb-1">
|
||||
{sheets.map(s => (
|
||||
<button
|
||||
key={s.name}
|
||||
onClick={() => setActiveSheet(s.name)}
|
||||
className={`px-3 py-1.5 text-[10px] font-medium rounded-lg whitespace-nowrap transition-colors ${
|
||||
activeSheet === s.name
|
||||
? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30'
|
||||
: 'text-white/40 hover:text-white/70 hover:bg-white/[0.05]'
|
||||
}`}
|
||||
>
|
||||
{de ? s.label_de : s.label_en}
|
||||
{s.rows > 0 && <span className="ml-1 text-[8px] opacity-50">({s.rows})</span>}
|
||||
</button>
|
||||
))}
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
onClick={handleCompute}
|
||||
disabled={computing}
|
||||
className="flex items-center gap-1 px-3 py-1.5 text-[10px] bg-emerald-500/20 text-emerald-300 rounded-lg hover:bg-emerald-500/30 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-3 h-3 ${computing ? 'animate-spin' : ''}`} />
|
||||
{de ? 'Berechnen' : 'Compute'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Year Navigation */}
|
||||
<div className="flex items-center justify-center gap-4 mb-2">
|
||||
<button onClick={() => setYearOffset(Math.max(0, yearOffset - 1))} disabled={yearOffset === 0}
|
||||
className="p-1 text-white/40 hover:text-white/70 disabled:opacity-20">
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="text-sm font-bold text-white">{currentYear}</span>
|
||||
<button onClick={() => setYearOffset(Math.min(4, yearOffset + 1))} disabled={yearOffset === 4}
|
||||
className="p-1 text-white/40 hover:text-white/70 disabled:opacity-20">
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Data Grid */}
|
||||
<GlassCard hover={false} className="p-2 overflow-x-auto">
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-white/30 text-sm">{de ? 'Lade...' : 'Loading...'}</div>
|
||||
) : (
|
||||
<table className="w-full text-[10px]">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10">
|
||||
<th className="text-left py-1.5 px-2 text-white/40 font-medium sticky left-0 bg-slate-900/90 backdrop-blur min-w-[160px]">
|
||||
{de ? 'Position' : 'Item'}
|
||||
</th>
|
||||
<th className="text-right py-1.5 px-2 text-white/40 font-medium min-w-[70px]">
|
||||
{currentYear}
|
||||
</th>
|
||||
{MONTH_LABELS.map((label, idx) => (
|
||||
<th key={idx} className="text-right py-1.5 px-1.5 text-white/30 font-normal min-w-[55px]">
|
||||
{label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(row => {
|
||||
const values = getValues(row)
|
||||
const label = getLabel(row)
|
||||
const isSumRow = row.is_sum_row || label.includes('GESAMT') || label.includes('Summe') || label.includes('UEBERSCHUSS') || label.includes('LIQUIDITAET')
|
||||
const isEditable = row.is_editable
|
||||
|
||||
// Annual sum for visible year
|
||||
let annual = 0
|
||||
for (let m = monthStart; m <= monthEnd; m++) annual += values[`m${m}`] || 0
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={`border-b border-white/[0.03] ${isSumRow ? 'bg-white/[0.03]' : ''} hover:bg-white/[0.02]`}
|
||||
>
|
||||
<td className={`py-1 px-2 sticky left-0 bg-slate-900/90 backdrop-blur ${isSumRow ? 'font-bold text-white/80' : 'text-white/60'}`}>
|
||||
<div className="flex items-center gap-1">
|
||||
{isEditable && <span className="w-1 h-1 rounded-full bg-indigo-400 flex-shrink-0" />}
|
||||
<span className="truncate">{label}</span>
|
||||
{row.position && <span className="text-white/20 ml-1">({row.position})</span>}
|
||||
{row.section && <span className="text-white/20 ml-1">[{row.section}]</span>}
|
||||
</div>
|
||||
</td>
|
||||
<td className={`text-right py-1 px-2 font-medium ${annual < 0 ? 'text-red-400' : isSumRow ? 'text-white/80' : 'text-white/50'}`}>
|
||||
{formatCell(annual)}
|
||||
</td>
|
||||
{Array.from({ length: 12 }, (_, idx) => {
|
||||
const mKey = `m${monthStart + idx}`
|
||||
const v = values[mKey] || 0
|
||||
|
||||
return (
|
||||
<td
|
||||
key={idx}
|
||||
className={`text-right py-1 px-1.5 ${
|
||||
v < 0 ? 'text-red-400/70' : v > 0 ? (isSumRow ? 'text-white/70' : 'text-white/50') : 'text-white/15'
|
||||
} ${isEditable ? 'cursor-pointer hover:bg-indigo-500/10' : ''}`}
|
||||
onDoubleClick={() => {
|
||||
if (!isEditable) return
|
||||
const input = prompt(`${label} — ${MONTH_LABELS[idx]} ${currentYear}`, String(v))
|
||||
if (input !== null) handleCellEdit(row.id, mKey, input)
|
||||
}}
|
||||
>
|
||||
{formatCell(v)}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</GlassCard>
|
||||
|
||||
<p className="text-center text-[9px] text-white/20 mt-2">
|
||||
{de
|
||||
? 'Doppelklick auf blaue Zellen zum Bearbeiten · Gruendung: 01.08.2026'
|
||||
: 'Double-click blue cells to edit · Founding: 01.08.2026'}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -27,7 +27,8 @@ const MODULES = [
|
||||
]
|
||||
|
||||
const PRICING_TIERS = [
|
||||
{ employees: '< 50', priceDe: 'ab 15.000 EUR/Jahr', priceEn: 'from EUR 15,000/yr', highlight: false },
|
||||
{ employees: 'Startup / < 10', priceDe: 'ab 3.600 EUR/Jahr', priceEn: 'from EUR 3,600/yr', highlight: false, noteDe: '14 Tage Test (Kreditkarte)', noteEn: '14-day trial (credit card)' },
|
||||
{ employees: '10 – 50', priceDe: 'ab 15.000 EUR/Jahr', priceEn: 'from EUR 15,000/yr', highlight: false },
|
||||
{ employees: '50 – 250', priceDe: 'ab 30.000 EUR/Jahr', priceEn: 'from EUR 30,000/yr', highlight: false },
|
||||
{ employees: '250+', priceDe: 'ab 40.000 EUR/Jahr', priceEn: 'from EUR 40,000/yr', highlight: true },
|
||||
]
|
||||
@@ -78,9 +79,14 @@ export default function ProductSlide({ lang }: ProductSlideProps) {
|
||||
<span className="text-xs text-white/70 font-medium">{tier.employees}</span>
|
||||
<span className="text-[10px] text-white/40 ml-1">{de ? 'Mitarbeiter' : 'employees'}</span>
|
||||
</div>
|
||||
<span className={`text-xs font-bold ${tier.highlight ? 'text-indigo-300' : 'text-white/70'}`}>
|
||||
{de ? tier.priceDe : tier.priceEn}
|
||||
</span>
|
||||
<div className="text-right">
|
||||
<span className={`text-xs font-bold ${tier.highlight ? 'text-indigo-300' : 'text-white/70'}`}>
|
||||
{de ? tier.priceDe : tier.priceEn}
|
||||
</span>
|
||||
{tier.noteDe && (
|
||||
<p className="text-[8px] text-white/30">{de ? tier.noteDe : tier.noteEn}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user