From 30c63bbef62be73cbbebddb0c9ad10873d916e57 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sun, 19 Apr 2026 17:50:08 +0200 Subject: [PATCH] feat(pitch-deck): Use of Funds computed from fp_* spending data Use of Funds pie chart now shows actual spending breakdown from fp_* tables (months 8-24) instead of manually set percentages: - Engineering & Personal: from fp_personalkosten - Vertrieb & Marketing: from fp_betriebliche (marketing category) - Betrieb & Infrastruktur: from fp_betriebliche (other categories) - Hardware & Ausstattung: from fp_investitionen Falls back to funding.use_of_funds if fp_* data not yet loaded. Co-Authored-By: Claude Opus 4.6 (1M context) --- pitch-deck/components/PitchDeck.tsx | 2 +- pitch-deck/components/slides/TheAskSlide.tsx | 15 +++-- pitch-deck/lib/hooks/useFpKPIs.ts | 68 +++++++++++++++++++- 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/pitch-deck/components/PitchDeck.tsx b/pitch-deck/components/PitchDeck.tsx index deba086..bbcb751 100644 --- a/pitch-deck/components/PitchDeck.tsx +++ b/pitch-deck/components/PitchDeck.tsx @@ -189,7 +189,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, case 'financials': return case 'the-ask': - return + return case 'cap-table': if (isWandeldarlehen) return null return diff --git a/pitch-deck/components/slides/TheAskSlide.tsx b/pitch-deck/components/slides/TheAskSlide.tsx index f108c8c..d8d84bf 100644 --- a/pitch-deck/components/slides/TheAskSlide.tsx +++ b/pitch-deck/components/slides/TheAskSlide.tsx @@ -3,6 +3,7 @@ import { motion } from 'framer-motion' import { Language, PitchFunding } from '@/lib/types' import { t } from '@/lib/i18n' +import { useFpKPIs } from '@/lib/hooks/useFpKPIs' import ProjectionFooter from '../ui/ProjectionFooter' import GradientText from '../ui/GradientText' import FadeInView from '../ui/FadeInView' @@ -14,6 +15,7 @@ import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts' interface TheAskSlideProps { lang: Language funding: PitchFunding + isWandeldarlehen?: boolean } const COLORS = ['#6366f1', '#a78bfa', '#60a5fa', '#34d399', '#fbbf24'] @@ -39,15 +41,18 @@ function formatTargetDate(dateStr: string, lang: Language): string { } } -export default function TheAskSlide({ lang, funding }: TheAskSlideProps) { +export default function TheAskSlide({ lang, funding, isWandeldarlehen }: TheAskSlideProps) { const i = t(lang) const de = lang === 'de' - const isWandeldarlehen = (funding?.instrument || '').toLowerCase().includes('wandeldarlehen') - const rawFunds = funding?.use_of_funds - const useOfFunds = Array.isArray(rawFunds) ? rawFunds : (typeof rawFunds === 'string' ? JSON.parse(rawFunds) : []) + const isWD = isWandeldarlehen || (funding?.instrument || '').toLowerCase() === 'wandeldarlehen' const amount = Number(funding?.amount_eur) || 0 const { target, suffix } = formatFundingAmount(amount) - const totalBudget = isWandeldarlehen ? amount * 2 : amount + const totalBudget = isWD ? amount * 2 : amount + + // Use of Funds from fp_* data (computed, not manual) + const { useOfFunds: fpUseOfFunds } = useFpKPIs(isWD) + const rawFunds = fpUseOfFunds.length > 0 ? fpUseOfFunds : (funding?.use_of_funds || []) + const useOfFunds = Array.isArray(rawFunds) ? rawFunds : (typeof rawFunds === 'string' ? JSON.parse(rawFunds) : []) const pieData = useOfFunds.map((item: Record) => ({ name: (de ? item.label_de : item.label_en) as string || 'N/A', diff --git a/pitch-deck/lib/hooks/useFpKPIs.ts b/pitch-deck/lib/hooks/useFpKPIs.ts index e7de914..9158971 100644 --- a/pitch-deck/lib/hooks/useFpKPIs.ts +++ b/pitch-deck/lib/hooks/useFpKPIs.ts @@ -104,6 +104,72 @@ export function useFpKPIs(isWandeldarlehen?: boolean) { load() }, [isWandeldarlehen]) + // Use of Funds: compute spending breakdown m8-m24 (funding period) + const [useOfFunds, setUseOfFunds] = useState>([]) + + useEffect(() => { + async function loadUoF() { + try { + const param = isWandeldarlehen ? '?scenarioId=c0000000-0000-0000-0000-000000000200' : '' + const [persRes, betriebRes, investRes] = await Promise.all([ + fetch(`/api/finanzplan/personalkosten${param}`, { cache: 'no-store' }), + fetch(`/api/finanzplan/betriebliche${param}`, { cache: 'no-store' }), + fetch(`/api/finanzplan/investitionen${param}`, { cache: 'no-store' }), + ]) + const [pers, betrieb, invest] = await Promise.all([persRes.json(), betriebRes.json(), investRes.json()]) + + // Sum spending m8-m24 + const sumRange = (rows: SheetRow[], field: string, m1: number, m2: number) => { + let total = 0 + for (const row of (rows || [])) { + const vals = (row as Record)[field] as Record || row.values || {} + for (let m = m1; m <= m2; m++) total += vals[`m${m}`] || 0 + } + return total + } + + const personalTotal = sumRange(pers.rows || [], 'values_total', 8, 24) + + // Marketing rows from betriebliche + const betriebRows: SheetRow[] = betrieb.rows || [] + const marketingRows = betriebRows.filter((r: SheetRow) => + (r as Record).category === 'marketing' && !(r as Record).is_sum_row + ) + const marketingTotal = sumRange(marketingRows, 'values', 8, 24) + + // All other betriebliche (excl. personal, marketing, abschreibungen, sum rows) + const otherBetrieb = betriebRows.filter((r: SheetRow) => { + const any = r as Record + return any.category !== 'marketing' && any.category !== 'personal' && any.category !== 'abschreibungen' && + !any.is_sum_row && !(r.row_label || '').includes('Summe') && !(r.row_label || '').includes('SUMME') + }) + const operationsTotal = sumRange(otherBetrieb, 'values', 8, 24) + + // Investitionen + const investTotal = sumRange(invest.rows || [], 'values_invest', 8, 24) + + const grandTotal = personalTotal + marketingTotal + operationsTotal + investTotal + if (grandTotal <= 0) return + + const pctPersonal = Math.round((personalTotal / grandTotal) * 100) + const pctMarketing = Math.round((marketingTotal / grandTotal) * 100) + const pctOps = Math.round((operationsTotal / grandTotal) * 100) + const pctInvest = Math.round((investTotal / grandTotal) * 100) + // Adjust rounding to 100% + const pctReserve = 100 - pctPersonal - pctMarketing - pctOps - pctInvest + + setUseOfFunds([ + { category: 'engineering', label_de: 'Engineering & Personal', label_en: 'Engineering & Personnel', percentage: pctPersonal }, + { category: 'sales', label_de: 'Vertrieb & Marketing', label_en: 'Sales & Marketing', percentage: pctMarketing }, + { category: 'operations', label_de: 'Betrieb & Infrastruktur', label_en: 'Operations & Infrastructure', percentage: pctOps }, + { category: 'hardware', label_de: 'Hardware & Ausstattung', label_en: 'Hardware & Equipment', percentage: pctInvest }, + ...(pctReserve > 0 ? [{ category: 'reserve', label_de: 'Reserve', label_en: 'Reserve', percentage: pctReserve }] : []), + ].filter(f => f.percentage > 0)) + } catch { /* ignore */ } + } + loadUoF() + }, [isWandeldarlehen]) + const last = kpis.y2030 - return { kpis, loading, last } + return { kpis, loading, last, useOfFunds } }