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,52 +1,63 @@
'use client'
import { Language } from '@/lib/types'
import { t } from '@/lib/i18n'
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { ChevronDown, ChevronRight } from 'lucide-react'
import { FMAssumption, Language } from '@/lib/types'
interface FinancialSlidersProps {
growthRate: number
churnRate: number
arpu: number
onGrowthChange: (v: number) => void
onChurnChange: (v: number) => void
onArpuChange: (v: number) => void
assumptions: FMAssumption[]
onAssumptionChange: (key: string, value: number) => void
lang: Language
}
function Slider({
label,
value,
min,
max,
step,
unit,
assumption,
onChange,
lang,
}: {
label: string
value: number
min: number
max: number
step: number
unit: string
onChange: (v: number) => void
assumption: FMAssumption
onChange: (value: number) => void
lang: Language
}) {
const value = typeof assumption.value === 'number' ? assumption.value : Number(assumption.value)
const label = lang === 'de' ? assumption.label_de : assumption.label_en
if (assumption.value_type === 'step') {
// Display step values as read-only list
const steps = Array.isArray(assumption.value) ? assumption.value : []
return (
<div className="space-y-1">
<p className="text-[11px] text-white/50">{label}</p>
<div className="flex gap-1.5">
{steps.map((s: number, i: number) => (
<div key={i} className="flex-1 text-center">
<p className="text-[9px] text-white/30">Y{i + 1}</p>
<p className="text-xs text-white font-mono">{s}</p>
</div>
))}
</div>
</div>
)
}
return (
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-white/60">{label}</span>
<span className="font-mono text-white">{value}{unit}</span>
<div className="space-y-1.5">
<div className="flex justify-between text-[11px]">
<span className="text-white/50">{label}</span>
<span className="font-mono text-white">{value}{assumption.unit === 'EUR' ? ' EUR' : assumption.unit === '%' ? '%' : ` ${assumption.unit || ''}`}</span>
</div>
<input
type="range"
min={min}
max={max}
step={step}
min={assumption.min_value ?? 0}
max={assumption.max_value ?? 100}
step={assumption.step_size ?? 1}
value={value}
onChange={(e) => onChange(parseFloat(e.target.value))}
className="w-full h-1.5 bg-white/10 rounded-full appearance-none cursor-pointer
[&::-webkit-slider-thumb]:appearance-none
[&::-webkit-slider-thumb]:w-4
[&::-webkit-slider-thumb]:h-4
[&::-webkit-slider-thumb]:w-3.5
[&::-webkit-slider-thumb]:h-3.5
[&::-webkit-slider-thumb]:rounded-full
[&::-webkit-slider-thumb]:bg-indigo-500
[&::-webkit-slider-thumb]:shadow-lg
@@ -58,47 +69,76 @@ function Slider({
)
}
export default function FinancialSliders({
growthRate,
churnRate,
arpu,
onGrowthChange,
onChurnChange,
onArpuChange,
lang,
}: FinancialSlidersProps) {
const i = t(lang)
interface CategoryGroup {
key: string
label: string
items: FMAssumption[]
}
export default function FinancialSliders({ assumptions, onAssumptionChange, lang }: FinancialSlidersProps) {
const [openCategories, setOpenCategories] = useState<Set<string>>(new Set(['revenue']))
// Group assumptions by category
const categories: CategoryGroup[] = [
{ key: 'revenue', label: lang === 'de' ? 'Revenue' : 'Revenue', items: [] },
{ key: 'costs', label: lang === 'de' ? 'Kosten' : 'Costs', items: [] },
{ key: 'team', label: 'Team', items: [] },
{ key: 'admin', label: lang === 'de' ? 'Verwaltung' : 'Administration', items: [] },
{ key: 'funding', label: 'Funding', items: [] },
]
for (const a of assumptions) {
const cat = categories.find(c => c.key === a.category) || categories[0]
cat.items.push(a)
}
const toggleCategory = (key: string) => {
setOpenCategories(prev => {
const next = new Set(prev)
if (next.has(key)) next.delete(key)
else next.add(key)
return next
})
}
return (
<div className="space-y-5 p-5 bg-white/[0.05] rounded-2xl border border-white/10">
<h4 className="text-sm font-medium text-white/60">{i.financials.adjustAssumptions}</h4>
<Slider
label={i.financials.sliderGrowth}
value={growthRate}
min={50}
max={200}
step={10}
unit="%"
onChange={onGrowthChange}
/>
<Slider
label={i.financials.sliderChurn}
value={churnRate}
min={0}
max={15}
step={0.5}
unit="%"
onChange={onChurnChange}
/>
<Slider
label={i.financials.sliderArpu}
value={arpu}
min={200}
max={1500}
step={50}
unit=" EUR"
onChange={onArpuChange}
/>
<div className="space-y-1">
{categories.filter(c => c.items.length > 0).map((cat) => {
const isOpen = openCategories.has(cat.key)
return (
<div key={cat.key} className="border border-white/[0.06] rounded-xl overflow-hidden">
<button
onClick={() => toggleCategory(cat.key)}
className="w-full flex items-center justify-between px-3 py-2 text-xs text-white/60 hover:text-white/80 transition-colors"
>
<span className="font-medium">{cat.label}</span>
{isOpen ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="px-3 pb-3 space-y-3">
{cat.items.map((a) => (
<Slider
key={a.key}
assumption={a}
onChange={(val) => onAssumptionChange(a.key, val)}
lang={lang}
/>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)
})}
</div>
)
}