feat(pitch-deck): Use of Funds computed from fp_* spending data
Some checks failed
Build pitch-deck / build-push-deploy (push) Successful in 1m20s
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) Failing after 22s
CI / test-python-voice (push) Successful in 32s
CI / test-bqas (push) Successful in 35s
Some checks failed
Build pitch-deck / build-push-deploy (push) Successful in 1m20s
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) Failing after 22s
CI / test-python-voice (push) Successful in 32s
CI / test-bqas (push) Successful in 35s
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) <noreply@anthropic.com>
This commit is contained in:
@@ -189,7 +189,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout,
|
|||||||
case 'financials':
|
case 'financials':
|
||||||
return <FinancialsSlide lang={lang} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} />
|
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} isWandeldarlehen={isWandeldarlehen} />
|
||||||
case 'cap-table':
|
case 'cap-table':
|
||||||
if (isWandeldarlehen) return null
|
if (isWandeldarlehen) return null
|
||||||
return <CapTableSlide lang={lang} />
|
return <CapTableSlide lang={lang} />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { Language, PitchFunding } from '@/lib/types'
|
import { Language, PitchFunding } from '@/lib/types'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
|
import { useFpKPIs } from '@/lib/hooks/useFpKPIs'
|
||||||
import ProjectionFooter from '../ui/ProjectionFooter'
|
import ProjectionFooter from '../ui/ProjectionFooter'
|
||||||
import GradientText from '../ui/GradientText'
|
import GradientText from '../ui/GradientText'
|
||||||
import FadeInView from '../ui/FadeInView'
|
import FadeInView from '../ui/FadeInView'
|
||||||
@@ -14,6 +15,7 @@ import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'
|
|||||||
interface TheAskSlideProps {
|
interface TheAskSlideProps {
|
||||||
lang: Language
|
lang: Language
|
||||||
funding: PitchFunding
|
funding: PitchFunding
|
||||||
|
isWandeldarlehen?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLORS = ['#6366f1', '#a78bfa', '#60a5fa', '#34d399', '#fbbf24']
|
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 i = t(lang)
|
||||||
const de = lang === 'de'
|
const de = lang === 'de'
|
||||||
const isWandeldarlehen = (funding?.instrument || '').toLowerCase().includes('wandeldarlehen')
|
const isWD = isWandeldarlehen || (funding?.instrument || '').toLowerCase() === 'wandeldarlehen'
|
||||||
const rawFunds = funding?.use_of_funds
|
|
||||||
const useOfFunds = Array.isArray(rawFunds) ? rawFunds : (typeof rawFunds === 'string' ? JSON.parse(rawFunds) : [])
|
|
||||||
const amount = Number(funding?.amount_eur) || 0
|
const amount = Number(funding?.amount_eur) || 0
|
||||||
const { target, suffix } = formatFundingAmount(amount)
|
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<string, unknown>) => ({
|
const pieData = useOfFunds.map((item: Record<string, unknown>) => ({
|
||||||
name: (de ? item.label_de : item.label_en) as string || 'N/A',
|
name: (de ? item.label_de : item.label_en) as string || 'N/A',
|
||||||
|
|||||||
@@ -104,6 +104,72 @@ export function useFpKPIs(isWandeldarlehen?: boolean) {
|
|||||||
load()
|
load()
|
||||||
}, [isWandeldarlehen])
|
}, [isWandeldarlehen])
|
||||||
|
|
||||||
|
// Use of Funds: compute spending breakdown m8-m24 (funding period)
|
||||||
|
const [useOfFunds, setUseOfFunds] = useState<Array<{ category: string; label_de: string; label_en: string; percentage: number }>>([])
|
||||||
|
|
||||||
|
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<string, unknown>)[field] as Record<string, number> || 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<string, unknown>).category === 'marketing' && !(r as Record<string, unknown>).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<string, unknown>
|
||||||
|
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
|
const last = kpis.y2030
|
||||||
return { kpis, loading, last }
|
return { kpis, loading, last, useOfFunds }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user