feat: Add staged funding model, financial compute engine, annex slides and UI enhancements
Some checks failed
ci/woodpecker/push/integration Pipeline failed
ci/woodpecker/push/main Pipeline failed
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled

Restructure financial plan from single 200k SAFE to realistic staged funding
(25k Stammkapital, 25k Angel, 200k Wandeldarlehen, 1M Series A = 1.25M total).
Add 60-month compute engine with CAPEX/OPEX accounting, cash constraints,
hardware financing (30% upfront / 70% leasing), and revenue-based hiring caps.
Rebuild TheAskSlide with 4-event funding timeline, update i18n (DE/EN),
chat agent core messages, and add 15 new annex/technology slides with
supporting UI components (KPICard, RunwayGauge, WaterfallChart, etc.).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
BreakPilot Dev
2026-02-14 21:20:02 +01:00
parent ac1bb1d97b
commit b464366341
44 changed files with 5196 additions and 262 deletions

View File

@@ -1,10 +1,7 @@
'use client'
import { PitchFinancial, Language } from '@/lib/types'
import { t } from '@/lib/i18n'
import { FMResult, FMComputeResponse } from '@/lib/types'
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
@@ -12,59 +9,190 @@ import {
Line,
ComposedChart,
Area,
ReferenceLine,
Brush,
} from 'recharts'
interface FinancialChartProps {
financials: PitchFinancial[]
lang: Language
growthMultiplier?: number
activeResults: FMComputeResponse | null
compareResults?: Map<string, FMComputeResponse>
compareMode?: boolean
scenarioColors?: Record<string, string>
lang: 'de' | 'en'
}
export default function FinancialChart({ financials, lang, growthMultiplier = 1 }: FinancialChartProps) {
const i = t(lang)
export default function FinancialChart({
activeResults,
compareResults,
compareMode = false,
scenarioColors = {},
lang,
}: FinancialChartProps) {
if (!activeResults) {
return (
<div className="w-full h-[300px] flex items-center justify-center text-white/30 text-sm">
{lang === 'de' ? 'Lade Daten...' : 'Loading data...'}
</div>
)
}
const data = financials.map((f) => ({
year: f.year,
[i.financials.revenue]: Math.round(f.revenue_eur * growthMultiplier),
[i.financials.costs]: f.costs_eur,
[i.financials.customers]: Math.round(f.customers_count * growthMultiplier),
}))
const results = activeResults.results
const breakEvenMonth = activeResults.summary.break_even_month
// Build chart data — monthly
const data = results.map((r) => {
const entry: Record<string, number | string> = {
label: `${r.year.toString().slice(2)}/${String(r.month_in_year).padStart(2, '0')}`,
month: r.month,
revenue: Math.round(r.revenue_eur),
costs: Math.round(r.total_costs_eur),
customers: r.total_customers,
cashBalance: Math.round(r.cash_balance_eur),
}
// Add compare scenario data
if (compareMode && compareResults) {
compareResults.forEach((cr, scenarioId) => {
const crMonth = cr.results.find(m => m.month === r.month)
if (crMonth) {
entry[`revenue_${scenarioId}`] = Math.round(crMonth.revenue_eur)
entry[`customers_${scenarioId}`] = crMonth.total_customers
}
})
}
return entry
})
const formatValue = (value: number) => {
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
if (value >= 1_000) return `${(value / 1_000).toFixed(0)}k`
if (Math.abs(value) >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
if (Math.abs(value) >= 1_000) return `${(value / 1_000).toFixed(0)}k`
return value.toString()
}
return (
<div className="w-full h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
<ComposedChart data={data} margin={{ top: 10, right: 50, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="revenueGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#6366f1" stopOpacity={0.8} />
<stop offset="100%" stopColor="#6366f1" stopOpacity={0.2} />
<linearGradient id="fmRevenueGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#6366f1" stopOpacity={0.6} />
<stop offset="100%" stopColor="#6366f1" stopOpacity={0.05} />
</linearGradient>
<linearGradient id="costGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#f43f5e" stopOpacity={0.6} />
<stop offset="100%" stopColor="#f43f5e" stopOpacity={0.1} />
<linearGradient id="fmCostGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#f43f5e" stopOpacity={0.4} />
<stop offset="100%" stopColor="#f43f5e" stopOpacity={0.05} />
</linearGradient>
</defs>
<XAxis dataKey="year" stroke="rgba(255,255,255,0.3)" tick={{ fill: 'rgba(255,255,255,0.5)', fontSize: 12 }} />
<YAxis stroke="rgba(255,255,255,0.1)" tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 11 }} tickFormatter={formatValue} />
<XAxis
dataKey="label"
stroke="rgba(255,255,255,0.2)"
tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 9 }}
interval={5}
/>
<YAxis
yAxisId="left"
stroke="rgba(255,255,255,0.1)"
tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 10 }}
tickFormatter={formatValue}
/>
<YAxis
yAxisId="right"
orientation="right"
stroke="rgba(255,255,255,0.1)"
tick={{ fill: 'rgba(34,197,94,0.5)', fontSize: 10 }}
/>
<Tooltip
contentStyle={{
background: 'rgba(10, 10, 26, 0.9)',
background: 'rgba(10, 10, 26, 0.95)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 12,
color: '#fff',
fontSize: 13,
fontSize: 11,
backdropFilter: 'blur(12px)',
}}
formatter={(value: number, name: string) => {
const label = name === 'revenue' ? (lang === 'de' ? 'Umsatz' : 'Revenue')
: name === 'costs' ? (lang === 'de' ? 'Kosten' : 'Costs')
: name === 'customers' ? (lang === 'de' ? 'Kunden' : 'Customers')
: name === 'cashBalance' ? 'Cash'
: name
return [name === 'customers' ? value : formatValue(value) + ' EUR', label]
}}
formatter={(value: number) => formatValue(value) + ' EUR'}
/>
<Area type="monotone" dataKey={i.financials.revenue} fill="url(#revenueGradient)" stroke="#6366f1" strokeWidth={2} />
<Bar dataKey={i.financials.costs} fill="url(#costGradient)" radius={[4, 4, 0, 0]} barSize={30} />
<Line type="monotone" dataKey={i.financials.customers} stroke="#22c55e" strokeWidth={2} dot={{ r: 4, fill: '#22c55e' }} yAxisId={0} />
{/* Break-even reference line */}
{breakEvenMonth && (
<ReferenceLine
x={data[breakEvenMonth - 1]?.label}
yAxisId="left"
stroke="#22c55e"
strokeDasharray="5 5"
label={{
value: 'Break-Even',
fill: '#22c55e',
fontSize: 10,
position: 'insideTopRight',
}}
/>
)}
{/* Revenue area */}
<Area
yAxisId="left"
type="monotone"
dataKey="revenue"
fill="url(#fmRevenueGradient)"
stroke="#6366f1"
strokeWidth={2}
/>
{/* Cost area */}
<Area
yAxisId="left"
type="monotone"
dataKey="costs"
fill="url(#fmCostGradient)"
stroke="#f43f5e"
strokeWidth={1.5}
strokeDasharray="4 4"
/>
{/* Customers line */}
<Line
yAxisId="right"
type="monotone"
dataKey="customers"
stroke="#22c55e"
strokeWidth={2}
dot={false}
/>
{/* Compare mode: overlay other scenarios */}
{compareMode && compareResults && Array.from(compareResults.entries()).map(([scenarioId]) => (
<Line
key={`rev_${scenarioId}`}
yAxisId="left"
type="monotone"
dataKey={`revenue_${scenarioId}`}
stroke={scenarioColors[scenarioId] || '#888'}
strokeWidth={1.5}
strokeOpacity={0.5}
dot={false}
strokeDasharray="3 3"
/>
))}
{/* Brush for zooming */}
<Brush
dataKey="label"
height={20}
stroke="rgba(99,102,241,0.4)"
fill="rgba(0,0,0,0.3)"
travellerWidth={8}
/>
</ComposedChart>
</ResponsiveContainer>
</div>