fix(pitch-deck): all financial slides now read from fp_* tables via useFpKPIs
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m18s
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 34s
CI / test-python-voice (push) Successful in 32s
CI / test-bqas (push) Successful in 32s
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m18s
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 34s
CI / test-python-voice (push) Successful in 32s
CI / test-bqas (push) Successful in 32s
New shared hook: useFpKPIs — loads annual KPIs from fp_guv/liquiditaet/personal/kunden. Replaces useFinancialModel (simplified model) for KPI display on all slides. Slides updated: - CompetitionSlide: "110 Gesetze" → "380+ Regularien & Normen" - BusinessModelSlide: ACV + Gross Margin from fp_* (was useFinancialModel) - ExecutiveSummarySlide: Unternehmensentwicklung from fp_* (was useFinancialModel) - FinancialsSlide: KPI cards from fp_* (ARR, Customers, Break-Even, EBIT 2030) All slides now show consistent numbers from the same source of truth (fp_* tables). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -161,7 +161,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout,
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'executive-summary':
|
case 'executive-summary':
|
||||||
return <ExecutiveSummarySlide lang={lang} data={data} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} />
|
return <ExecutiveSummarySlide lang={lang} data={data} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} />
|
||||||
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':
|
||||||
@@ -179,7 +179,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout,
|
|||||||
case 'market':
|
case 'market':
|
||||||
return <MarketSlide lang={lang} market={data.market} />
|
return <MarketSlide lang={lang} market={data.market} />
|
||||||
case 'business-model':
|
case 'business-model':
|
||||||
return <BusinessModelSlide lang={lang} products={data.products} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} />
|
return <BusinessModelSlide lang={lang} products={data.products} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} />
|
||||||
case 'traction':
|
case 'traction':
|
||||||
return <TractionSlide lang={lang} milestones={data.milestones} metrics={data.metrics} />
|
return <TractionSlide lang={lang} milestones={data.milestones} metrics={data.metrics} />
|
||||||
case 'competition':
|
case 'competition':
|
||||||
@@ -187,7 +187,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout,
|
|||||||
case 'team':
|
case 'team':
|
||||||
return <TeamSlide lang={lang} team={data.team} />
|
return <TeamSlide lang={lang} team={data.team} />
|
||||||
case 'financials':
|
case 'financials':
|
||||||
return <FinancialsSlide lang={lang} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} />
|
return <FinancialsSlide lang={lang} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} />
|
||||||
case 'the-ask':
|
case 'the-ask':
|
||||||
return <TheAskSlide lang={lang} funding={data.funding} />
|
return <TheAskSlide lang={lang} funding={data.funding} />
|
||||||
case 'cap-table':
|
case 'cap-table':
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Language } from '@/lib/types'
|
import { Language } from '@/lib/types'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
import { useFinancialModel } from '@/lib/hooks/useFinancialModel'
|
import { useFpKPIs } from '@/lib/hooks/useFpKPIs'
|
||||||
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'
|
||||||
@@ -13,18 +13,17 @@ interface BusinessModelSlideProps {
|
|||||||
products?: unknown[]
|
products?: unknown[]
|
||||||
investorId?: string | null
|
investorId?: string | null
|
||||||
preferredScenarioId?: string | null
|
preferredScenarioId?: string | null
|
||||||
|
isWandeldarlehen?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BusinessModelSlide({ lang, investorId, preferredScenarioId }: BusinessModelSlideProps) {
|
export default function BusinessModelSlide({ lang, isWandeldarlehen }: BusinessModelSlideProps) {
|
||||||
const i = t(lang)
|
const i = t(lang)
|
||||||
const de = lang === 'de'
|
const de = lang === 'de'
|
||||||
const fm = useFinancialModel(investorId || null, preferredScenarioId)
|
const { last } = useFpKPIs(isWandeldarlehen)
|
||||||
const summary = fm.activeResults?.summary
|
const finalCustomers = last?.customers || 0
|
||||||
const results = fm.activeResults?.results || []
|
const finalArr = last?.arr || 0
|
||||||
const lastResult = results[results.length - 1]
|
|
||||||
const finalCustomers = summary?.final_customers || 0
|
|
||||||
const finalArr = summary?.final_arr || 0
|
|
||||||
const acv = finalCustomers > 0 ? Math.round(finalArr / finalCustomers) : 0
|
const acv = finalCustomers > 0 ? Math.round(finalArr / finalCustomers) : 0
|
||||||
|
const grossMargin = last?.grossMargin ?? 0
|
||||||
|
|
||||||
const tiers = [
|
const tiers = [
|
||||||
{
|
{
|
||||||
@@ -62,7 +61,7 @@ export default function BusinessModelSlide({ lang, investorId, preferredScenario
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const grossMargin = lastResult?.gross_margin_pct ?? 0
|
// grossMargin already defined above from useFpKPIs
|
||||||
const acvLabel = acv > 0
|
const acvLabel = acv > 0
|
||||||
? (de ? `${(acv / 1000).toFixed(1).replace('.', ',')}k EUR` : `EUR ${(acv / 1000).toFixed(1)}k`)
|
? (de ? `${(acv / 1000).toFixed(1).replace('.', ',')}k EUR` : `EUR ${(acv / 1000).toFixed(1)}k`)
|
||||||
: '—'
|
: '—'
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ const ALL_FEATURES: ComparisonFeature[] = [
|
|||||||
// Top 5 Differentiators (isDiff=true) — no other vendor has ANY of these
|
// Top 5 Differentiators (isDiff=true) — no other vendor has ANY of these
|
||||||
{ de: 'Self-Hosted / On-Premise', en: 'Self-Hosted / On-Premise', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true },
|
{ de: 'Self-Hosted / On-Premise', en: 'Self-Hosted / On-Premise', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true },
|
||||||
{ de: 'Code-Security & DevSecOps (6 Tools)', en: 'Code Security & DevSecOps (6 Tools)', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true },
|
{ de: 'Code-Security & DevSecOps (6 Tools)', en: 'Code Security & DevSecOps (6 Tools)', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true },
|
||||||
{ de: '110 Gesetze & Regularien, 25.000+ Sicherheitskontrollen', en: '110 Laws & Regulations, 25,000+ Security Controls', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true },
|
{ de: '380+ Regularien & Normen, 25.000+ Prüfaspekte', en: '380+ Regulations & Standards, 25,000+ Audit Controls', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true },
|
||||||
{ de: 'Hardware-Moat (Mac Mini/Studio)', en: 'Hardware Moat (Mac Mini/Studio)', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true },
|
{ de: 'Hardware-Moat (Mac Mini/Studio)', en: 'Hardware Moat (Mac Mini/Studio)', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true },
|
||||||
{ de: 'PII-Redaction LLM Gateway', en: 'PII Redaction LLM Gateway', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true },
|
{ de: 'PII-Redaction LLM Gateway', en: 'PII Redaction LLM Gateway', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: true, isUSP: true },
|
||||||
// More USPs
|
// More USPs
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback } 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 { useFpKPIs } from '@/lib/hooks/useFpKPIs'
|
||||||
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'
|
||||||
@@ -15,19 +14,16 @@ interface ExecutiveSummarySlideProps {
|
|||||||
data: PitchData
|
data: PitchData
|
||||||
investorId?: string | null
|
investorId?: string | null
|
||||||
preferredScenarioId?: string | null
|
preferredScenarioId?: string | null
|
||||||
|
isWandeldarlehen?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExecutiveSummarySlide({ lang, data, investorId, preferredScenarioId }: ExecutiveSummarySlideProps) {
|
export default function ExecutiveSummarySlide({ lang, data, investorId, preferredScenarioId, isWandeldarlehen }: 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
|
// Unternehmensentwicklung from fp_* tables (source of truth)
|
||||||
const fm = useFinancialModel(investorId || null, preferredScenarioId)
|
const { kpis: fpKPIs } = useFpKPIs(isWandeldarlehen)
|
||||||
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
|
||||||
@@ -538,16 +534,18 @@ export default function ExecutiveSummarySlide({ lang, data, investorId, preferre
|
|||||||
<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.length === 0 ? (
|
{!fpKPIs.y2026 ? (
|
||||||
<p className="text-xs text-white/30 text-center py-2">{de ? 'Lade Finanzplan...' : 'Loading financial plan...'}</p>
|
<p className="text-xs text-white/30 text-center py-2">{de ? 'Lade Finanzplan...' : 'Loading financial plan...'}</p>
|
||||||
) : annualKPIs.map((k, idx) => {
|
) : [2026, 2027, 2028, 2029, 2030].map((year, idx) => {
|
||||||
|
const k = fpKPIs[`y${year}`]
|
||||||
|
if (!k) return null
|
||||||
const arrLabel = k.arr >= 1_000_000
|
const arrLabel = k.arr >= 1_000_000
|
||||||
? (de ? `~${(k.arr / 1_000_000).toFixed(1).replace('.', ',')} Mio. EUR` : `~EUR ${(k.arr / 1_000_000).toFixed(1)}M`)
|
? (de ? `~${(k.arr / 1_000_000).toFixed(1).replace('.', ',')} Mio. EUR` : `~EUR ${(k.arr / 1_000_000).toFixed(1)}M`)
|
||||||
: (de ? `~${Math.round(k.arr / 1000)}k EUR` : `~EUR ${Math.round(k.arr / 1000)}k`)
|
: (de ? `~${Math.round(k.arr / 1000)}k EUR` : `~EUR ${Math.round(k.arr / 1000)}k`)
|
||||||
return (
|
return (
|
||||||
<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">{k.year}</span>
|
<span className="text-white/40">{year}</span>
|
||||||
<span className="text-right text-white/50">{k.employees}</span>
|
<span className="text-right text-white/50">{k.headcount}</span>
|
||||||
<span className="text-right text-white/50">~{k.customers.toLocaleString('de-DE')}</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'}`}>{arrLabel}</span>
|
<span className={`text-right font-mono ${idx >= 3 ? 'text-emerald-300 font-bold' : 'text-white/70'}`}>{arrLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Language } from '@/lib/types'
|
|||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
import ProjectionFooter from '../ui/ProjectionFooter'
|
import ProjectionFooter from '../ui/ProjectionFooter'
|
||||||
import { useFinancialModel } from '@/lib/hooks/useFinancialModel'
|
import { useFinancialModel } from '@/lib/hooks/useFinancialModel'
|
||||||
|
import { useFpKPIs } from '@/lib/hooks/useFpKPIs'
|
||||||
import GradientText from '../ui/GradientText'
|
import GradientText from '../ui/GradientText'
|
||||||
import FadeInView from '../ui/FadeInView'
|
import FadeInView from '../ui/FadeInView'
|
||||||
import FinancialChart from '../ui/FinancialChart'
|
import FinancialChart from '../ui/FinancialChart'
|
||||||
@@ -23,9 +24,10 @@ interface FinancialsSlideProps {
|
|||||||
lang: Language
|
lang: Language
|
||||||
investorId: string | null
|
investorId: string | null
|
||||||
preferredScenarioId?: string | null
|
preferredScenarioId?: string | null
|
||||||
|
isWandeldarlehen?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FinancialsSlide({ lang, investorId, preferredScenarioId }: FinancialsSlideProps) {
|
export default function FinancialsSlide({ lang, investorId, preferredScenarioId, isWandeldarlehen }: FinancialsSlideProps) {
|
||||||
const i = t(lang)
|
const i = t(lang)
|
||||||
const fm = useFinancialModel(investorId, preferredScenarioId)
|
const fm = useFinancialModel(investorId, preferredScenarioId)
|
||||||
const [activeTab, setActiveTab] = useState<FinTab>('overview')
|
const [activeTab, setActiveTab] = useState<FinTab>('overview')
|
||||||
@@ -35,6 +37,18 @@ export default function FinancialsSlide({ lang, investorId, preferredScenarioId
|
|||||||
const summary = activeResults?.summary
|
const summary = activeResults?.summary
|
||||||
const lastResult = activeResults?.results[activeResults.results.length - 1]
|
const lastResult = activeResults?.results[activeResults.results.length - 1]
|
||||||
|
|
||||||
|
// KPI cards from fp_* tables (source of truth)
|
||||||
|
const { last: fpLast, kpis: fpKPIs } = useFpKPIs(isWandeldarlehen)
|
||||||
|
const kpiArr = fpLast?.arr || summary?.final_arr || 0
|
||||||
|
const kpiCustomers = fpLast?.customers || summary?.final_customers || 0
|
||||||
|
const kpiEbit = fpKPIs?.y2029?.ebit // First profitable year
|
||||||
|
const kpiBreakEven = (() => {
|
||||||
|
for (const y of [2026, 2027, 2028, 2029, 2030]) {
|
||||||
|
if ((fpKPIs[`y${y}`]?.ebit || 0) > 0) return y
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})()
|
||||||
|
|
||||||
// Build scenario color map
|
// Build scenario color map
|
||||||
const scenarioColors: Record<string, string> = {}
|
const scenarioColors: Record<string, string> = {}
|
||||||
fm.scenarios.forEach(s => { scenarioColors[s.id] = s.color })
|
fm.scenarios.forEach(s => { scenarioColors[s.id] = s.color })
|
||||||
@@ -74,9 +88,9 @@ export default function FinancialsSlide({ lang, investorId, preferredScenarioId
|
|||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-3">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-3">
|
||||||
<KPICard
|
<KPICard
|
||||||
label={`ARR 2030`}
|
label={`ARR 2030`}
|
||||||
value={summary ? Math.round(summary.final_arr / 1_000_000 * 10) / 10 : 0}
|
value={kpiArr >= 1_000_000 ? Math.round(kpiArr / 1_000_000 * 10) / 10 : Math.round(kpiArr / 1000)}
|
||||||
suffix=" Mio."
|
suffix={kpiArr >= 1_000_000 ? ' Mio.' : 'k'}
|
||||||
decimals={1}
|
decimals={kpiArr >= 1_000_000 ? 1 : 0}
|
||||||
trend="up"
|
trend="up"
|
||||||
color="#6366f1"
|
color="#6366f1"
|
||||||
delay={0.1}
|
delay={0.1}
|
||||||
@@ -84,28 +98,28 @@ export default function FinancialsSlide({ lang, investorId, preferredScenarioId
|
|||||||
/>
|
/>
|
||||||
<KPICard
|
<KPICard
|
||||||
label={de ? 'Kunden 2030' : 'Customers 2030'}
|
label={de ? 'Kunden 2030' : 'Customers 2030'}
|
||||||
value={summary?.final_customers || 0}
|
value={kpiCustomers}
|
||||||
trend="up"
|
trend="up"
|
||||||
color="#22c55e"
|
color="#22c55e"
|
||||||
delay={0.15}
|
delay={0.15}
|
||||||
/>
|
/>
|
||||||
<KPICard
|
<KPICard
|
||||||
label="Break-Even"
|
label="Break-Even"
|
||||||
value={summary?.break_even_month || 0}
|
value={kpiBreakEven || 0}
|
||||||
suffix={de ? ' Mo.' : ' mo.'}
|
trend={kpiBreakEven && kpiBreakEven <= 2029 ? 'up' : 'down'}
|
||||||
trend={summary?.break_even_month && summary.break_even_month <= 24 ? 'up' : 'down'}
|
|
||||||
color="#eab308"
|
color="#eab308"
|
||||||
delay={0.2}
|
delay={0.2}
|
||||||
subLabel={summary?.break_even_month ? `~${Math.ceil((summary.break_even_month) / 12) + 2025}` : ''}
|
subLabel=""
|
||||||
/>
|
/>
|
||||||
<KPICard
|
<KPICard
|
||||||
label="LTV/CAC"
|
label="EBIT 2030"
|
||||||
value={summary?.final_ltv_cac || 0}
|
value={fpLast?.ebit ? Math.round(fpLast.ebit / 1_000_000 * 10) / 10 : 0}
|
||||||
suffix="x"
|
suffix=" Mio."
|
||||||
decimals={1}
|
decimals={1}
|
||||||
trend={(summary?.final_ltv_cac || 0) >= 3 ? 'up' : 'down'}
|
trend={(fpLast?.ebit || 0) > 0 ? 'up' : 'down'}
|
||||||
color="#a855f7"
|
color="#a855f7"
|
||||||
delay={0.25}
|
delay={0.25}
|
||||||
|
subLabel="EUR"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
90
pitch-deck/lib/hooks/useFpKPIs.ts
Normal file
90
pitch-deck/lib/hooks/useFpKPIs.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
export interface FpAnnualKPIs {
|
||||||
|
revenue: number
|
||||||
|
ebit: number
|
||||||
|
personal: number
|
||||||
|
netIncome: number
|
||||||
|
steuern: number
|
||||||
|
liquiditaet: number
|
||||||
|
customers: number
|
||||||
|
headcount: number
|
||||||
|
mrr: number
|
||||||
|
arr: number
|
||||||
|
arpu: number
|
||||||
|
revPerEmp: number
|
||||||
|
ebitMargin: number
|
||||||
|
grossMargin: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SheetRow {
|
||||||
|
row_label?: string
|
||||||
|
values?: Record<string, number>
|
||||||
|
values_total?: Record<string, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads annual KPIs directly from fp_* tables (source of truth).
|
||||||
|
* Returns a map of year keys (y2026-y2030) to KPI objects.
|
||||||
|
*/
|
||||||
|
export function useFpKPIs(isWandeldarlehen?: boolean) {
|
||||||
|
const [kpis, setKpis] = useState<Record<string, FpAnnualKPIs>>({})
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
const param = isWandeldarlehen ? '?scenarioId=c0000000-0000-0000-0000-000000000200' : ''
|
||||||
|
const [guvRes, liqRes, persRes, kundenRes] = await Promise.all([
|
||||||
|
fetch(`/api/finanzplan/guv${param}`, { cache: 'no-store' }),
|
||||||
|
fetch(`/api/finanzplan/liquiditaet${param}`, { cache: 'no-store' }),
|
||||||
|
fetch(`/api/finanzplan/personalkosten${param}`, { cache: 'no-store' }),
|
||||||
|
fetch(`/api/finanzplan/kunden${param}`, { cache: 'no-store' }),
|
||||||
|
])
|
||||||
|
const [guv, liq, pers, kunden] = await Promise.all([guvRes.json(), liqRes.json(), persRes.json(), kundenRes.json()])
|
||||||
|
|
||||||
|
const guvRows: SheetRow[] = guv.rows || []
|
||||||
|
const liqRows: SheetRow[] = liq.rows || []
|
||||||
|
const persRows: SheetRow[] = pers.rows || []
|
||||||
|
const kundenRows: SheetRow[] = kunden.rows || []
|
||||||
|
|
||||||
|
const findGuv = (label: string) => guvRows.find(r => (r.row_label || '').includes(label))
|
||||||
|
const findLiq = (label: string) => liqRows.find(r => (r.row_label || '').includes(label))
|
||||||
|
const kundenGesamt = kundenRows.find(r => r.row_label === 'Bestandskunden gesamt')
|
||||||
|
|
||||||
|
const result: Record<string, FpAnnualKPIs> = {}
|
||||||
|
|
||||||
|
for (const y of [2026, 2027, 2028, 2029, 2030]) {
|
||||||
|
const yk = `y${y}`
|
||||||
|
const mk = `m${(y - 2026) * 12 + 12}` // December
|
||||||
|
|
||||||
|
const revenue = findGuv('Umsatzerlöse')?.values?.[yk] || 0
|
||||||
|
const ebit = findGuv('EBIT')?.values?.[yk] || 0
|
||||||
|
const personal = findGuv('Summe Personalaufwand')?.values?.[yk] || 0
|
||||||
|
const netIncome = findGuv('Jahresüberschuss')?.values?.[yk] || findGuv('Jahresueber')?.values?.[yk] || 0
|
||||||
|
const steuern = findGuv('Steuern gesamt')?.values?.[yk] || 0
|
||||||
|
const liquiditaet = findLiq('LIQUIDIT')?.values?.[mk] || findLiq('LIQUIDITAET')?.values?.[mk] || 0
|
||||||
|
const customers = kundenGesamt?.values?.[mk] || 0
|
||||||
|
const headcount = persRows.filter(r => ((r.values_total || r.values)?.[mk] || 0) > 0).length
|
||||||
|
const mrr = revenue > 0 ? Math.round(revenue / 12) : 0
|
||||||
|
const arr = mrr * 12
|
||||||
|
const arpu = customers > 0 ? Math.round(mrr / customers) : 0
|
||||||
|
const revPerEmp = headcount > 0 ? Math.round(revenue / headcount) : 0
|
||||||
|
const ebitMargin = revenue > 0 ? Math.round((ebit / revenue) * 100) : 0
|
||||||
|
const grossMargin = revenue > 0 ? Math.round(((revenue - (findGuv('Summe Materialaufwand')?.values?.[yk] || 0)) / revenue) * 100) : 0
|
||||||
|
|
||||||
|
result[yk] = { revenue, ebit, personal, netIncome, steuern, liquiditaet, customers, headcount, mrr, arr, arpu, revPerEmp, ebitMargin, grossMargin }
|
||||||
|
}
|
||||||
|
|
||||||
|
setKpis(result)
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
load()
|
||||||
|
}, [isWandeldarlehen])
|
||||||
|
|
||||||
|
const last = kpis.y2030
|
||||||
|
return { kpis, loading, last }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user