fix(pitch-deck): sync Executive Summary + BusinessModel with compute engine
Some checks failed
Build pitch-deck / build-push-deploy (push) Failing after 35s
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 35s
CI / test-python-voice (push) Successful in 33s
CI / test-bqas (push) Successful in 33s
Some checks failed
Build pitch-deck / build-push-deploy (push) Failing after 35s
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 35s
CI / test-python-voice (push) Successful in 33s
CI / test-bqas (push) Successful in 33s
ExecutiveSummarySlide: - Unternehmensentwicklung: hardcoded table → useFinancialModel + computeAnnualKPIs (MA, Kunden, ARR now computed from finanzplan DB for all versions) - Pricing: aligned with BusinessModelSlide tiers (Starter/Professional/Enterprise) Enterprise: 40k → 50k (matching Folie 11) BusinessModelSlide: - ACV: hardcoded "15–50k" → computed from summary.final_arr / final_customers - Gross Margin: hardcoded "> 80%" → computed from lastResult.gross_margin_pct All financial numbers on all slides now flow from the same compute engine. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -151,7 +151,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout,
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'executive-summary':
|
case 'executive-summary':
|
||||||
return <ExecutiveSummarySlide lang={lang} data={data} />
|
return <ExecutiveSummarySlide lang={lang} data={data} investorId={investor?.id || null} />
|
||||||
case 'cover':
|
case 'cover':
|
||||||
return <CoverSlide lang={lang} onNext={nav.nextSlide} funding={data.funding} />
|
return <CoverSlide lang={lang} onNext={nav.nextSlide} funding={data.funding} />
|
||||||
case 'problem':
|
case 'problem':
|
||||||
|
|||||||
@@ -61,9 +61,14 @@ export default function BusinessModelSlide({ lang, investorId }: BusinessModelSl
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const grossMargin = lastResult?.gross_margin_pct ?? 0
|
||||||
|
const acvLabel = acv > 0
|
||||||
|
? (de ? `${(acv / 1000).toFixed(1).replace('.', ',')}k EUR` : `EUR ${(acv / 1000).toFixed(1)}k`)
|
||||||
|
: '—'
|
||||||
|
|
||||||
const metrics = [
|
const metrics = [
|
||||||
{ icon: DollarSign, color: 'text-indigo-400', label: 'ACV', value: de ? '15 – 50k EUR' : '15 – 50k EUR', sub: de ? 'Durchschnittlicher Vertragswert' : 'Average Contract Value' },
|
{ icon: DollarSign, color: 'text-indigo-400', label: 'ACV (2030)', value: acvLabel, sub: de ? 'Durchschnittlicher Vertragswert (berechnet)' : 'Average Contract Value (computed)' },
|
||||||
{ icon: TrendingUp, color: 'text-emerald-400', label: 'Gross Margin', value: '> 80%', sub: de ? 'Cloud-native, keine Hardware-Kosten' : 'Cloud-native, no hardware costs' },
|
{ icon: TrendingUp, color: 'text-emerald-400', label: 'Gross Margin', value: `${Math.round(grossMargin)}%`, sub: de ? 'Cloud-native, keine Hardware-Kosten' : 'Cloud-native, no hardware costs' },
|
||||||
{ icon: Repeat, color: 'text-purple-400', label: 'NRR Ziel', value: '> 120%', sub: de ? 'Upsell: mehr Module, mehr Nutzer' : 'Upsell: more modules, more users' },
|
{ icon: Repeat, color: 'text-purple-400', label: 'NRR Ziel', value: '> 120%', sub: de ? 'Upsell: mehr Module, mehr Nutzer' : 'Upsell: more modules, more users' },
|
||||||
{ icon: Target, color: 'text-amber-400', label: 'Payback', value: de ? '< 3 Monate' : '< 3 Months', sub: de ? 'Ersparnis übersteigt Kosten ab Q1' : 'Savings exceed costs from Q1' },
|
{ icon: Target, color: 'text-amber-400', label: 'Payback', value: de ? '< 3 Monate' : '< 3 Months', sub: de ? 'Ersparnis übersteigt Kosten ab Q1' : 'Savings exceed costs from Q1' },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useCallback } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { Language, PitchData } from '@/lib/types'
|
import { Language, PitchData } from '@/lib/types'
|
||||||
import { t, formatEur } from '@/lib/i18n'
|
import { t, formatEur } from '@/lib/i18n'
|
||||||
|
import { useFinancialModel } from '@/lib/hooks/useFinancialModel'
|
||||||
|
import { computeAnnualKPIs } from '@/lib/finanzplan/annual-kpis'
|
||||||
import GradientText from '../ui/GradientText'
|
import GradientText from '../ui/GradientText'
|
||||||
import FadeInView from '../ui/FadeInView'
|
import FadeInView from '../ui/FadeInView'
|
||||||
import GlassCard from '../ui/GlassCard'
|
import GlassCard from '../ui/GlassCard'
|
||||||
@@ -11,13 +13,21 @@ import { Download, Shield, Server, Brain, TrendingUp, FileText, Target, ScanLine
|
|||||||
interface ExecutiveSummarySlideProps {
|
interface ExecutiveSummarySlideProps {
|
||||||
lang: Language
|
lang: Language
|
||||||
data: PitchData
|
data: PitchData
|
||||||
|
investorId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExecutiveSummarySlide({ lang, data }: ExecutiveSummarySlideProps) {
|
export default function ExecutiveSummarySlide({ lang, data, investorId }: ExecutiveSummarySlideProps) {
|
||||||
const i = t(lang)
|
const i = t(lang)
|
||||||
const es = i.executiveSummary
|
const es = i.executiveSummary
|
||||||
const de = lang === 'de'
|
const de = lang === 'de'
|
||||||
|
|
||||||
|
// Financial model for Unternehmensentwicklung
|
||||||
|
const fm = useFinancialModel(investorId || null)
|
||||||
|
const annualKPIs = useMemo(
|
||||||
|
() => computeAnnualKPIs(fm.activeResults?.results || []),
|
||||||
|
[fm.activeResults],
|
||||||
|
)
|
||||||
|
|
||||||
const funding = data.funding
|
const funding = data.funding
|
||||||
const amount = funding?.amount_eur || 0
|
const amount = funding?.amount_eur || 0
|
||||||
const amountLabel = amount >= 1_000_000
|
const amountLabel = amount >= 1_000_000
|
||||||
@@ -527,20 +537,19 @@ export default function ExecutiveSummarySlide({ lang, data }: ExecutiveSummarySl
|
|||||||
<span>{de ? 'Jahr' : 'Year'}</span><span className="text-right">MA</span><span className="text-right">{de ? 'Kunden' : 'Customers'}</span><span className="text-right">ARR</span>
|
<span>{de ? 'Jahr' : 'Year'}</span><span className="text-right">MA</span><span className="text-right">{de ? 'Kunden' : 'Customers'}</span><span className="text-right">ARR</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{[
|
{annualKPIs.map((k, idx) => {
|
||||||
{ year: '2026', emp: '5', cust: '~17', arr: de ? '~84k EUR' : '~EUR 84k' },
|
const arrLabel = k.arr >= 1_000_000
|
||||||
{ year: '2027', emp: '10', cust: '~132', arr: de ? '~1,1 Mio. EUR' : '~EUR 1.1M' },
|
? (de ? `~${(k.arr / 1_000_000).toFixed(1).replace('.', ',')} Mio. EUR` : `~EUR ${(k.arr / 1_000_000).toFixed(1)}M`)
|
||||||
{ year: '2028', emp: '17', cust: '~400', arr: de ? '~3,6 Mio. EUR' : '~EUR 3.6M' },
|
: (de ? `~${Math.round(k.arr / 1000)}k EUR` : `~EUR ${Math.round(k.arr / 1000)}k`)
|
||||||
{ year: '2029', emp: '25', cust: '~780', arr: de ? '~6,9 Mio. EUR' : '~EUR 6.9M' },
|
return (
|
||||||
{ year: '2030', emp: '35', cust: '~1.320', arr: de ? '~11,1 Mio. EUR' : '~EUR 11.1M' },
|
|
||||||
].map((r, idx) => (
|
|
||||||
<div key={idx} className="grid grid-cols-4 gap-x-3 text-xs">
|
<div key={idx} className="grid grid-cols-4 gap-x-3 text-xs">
|
||||||
<span className="text-white/40">{r.year}</span>
|
<span className="text-white/40">{k.year}</span>
|
||||||
<span className="text-right text-white/50">{r.emp}</span>
|
<span className="text-right text-white/50">{k.employees}</span>
|
||||||
<span className="text-right text-white/50">{r.cust}</span>
|
<span className="text-right text-white/50">~{k.customers.toLocaleString('de-DE')}</span>
|
||||||
<span className={`text-right font-mono ${idx >= 3 ? 'text-emerald-300 font-bold' : 'text-white/70'}`}>{r.arr}</span>
|
<span className={`text-right font-mono ${idx >= 3 ? 'text-emerald-300 font-bold' : 'text-white/70'}`}>{arrLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
|
|
||||||
@@ -578,10 +587,9 @@ export default function ExecutiveSummarySlide({ lang, data }: ExecutiveSummarySl
|
|||||||
<h3 className="text-xs font-bold text-amber-400 uppercase tracking-wider mb-2">{de ? 'Pricing' : 'Pricing'}</h3>
|
<h3 className="text-xs font-bold text-amber-400 uppercase tracking-wider mb-2">{de ? 'Pricing' : 'Pricing'}</h3>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
{[
|
{[
|
||||||
{ tier: 'Startup', price: de ? 'ab 3.600€/J.' : 'from €3,600/yr' },
|
{ tier: de ? 'Starter (<10 MA)' : 'Starter (<10 emp.)', price: de ? '3.600€/J.' : '€3,600/yr' },
|
||||||
{ tier: '10–50 MA', price: de ? 'ab 15.000€/J.' : 'from €15k/yr' },
|
{ tier: de ? 'Professional (10–250)' : 'Professional (10–250)', price: de ? '15.000–40.000€/J.' : '€15k–40k/yr', highlight: true },
|
||||||
{ tier: '50–250 MA', price: de ? 'ab 30.000€/J.' : 'from €30k/yr' },
|
{ tier: de ? 'Enterprise (250+)' : 'Enterprise (250+)', price: de ? 'ab 50.000€/J.' : 'from €50k/yr' },
|
||||||
{ tier: '250+ MA', price: de ? 'ab 40.000€/J.' : 'from €40k/yr', highlight: true },
|
|
||||||
].map((t, idx) => (
|
].map((t, idx) => (
|
||||||
<div key={idx} className={`flex justify-between text-xs ${t.highlight ? 'text-amber-300 font-bold' : 'text-white/60'}`}>
|
<div key={idx} className={`flex justify-between text-xs ${t.highlight ? 'text-amber-300 font-bold' : 'text-white/60'}`}>
|
||||||
<span>{t.tier}</span>
|
<span>{t.tier}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user