feat: add pitch-deck service to core infrastructure
Migrated pitch-deck from breakpilot-pwa to breakpilot-core. Container: bp-core-pitch-deck on port 3012. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
180
pitch-deck/components/ui/AnnualCashflowChart.tsx
Normal file
180
pitch-deck/components/ui/AnnualCashflowChart.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
'use client'
|
||||
|
||||
import { FMResult } from '@/lib/types'
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
ReferenceLine,
|
||||
Line,
|
||||
ComposedChart,
|
||||
Cell,
|
||||
} from 'recharts'
|
||||
|
||||
interface AnnualCashflowChartProps {
|
||||
results: FMResult[]
|
||||
initialFunding: number
|
||||
lang: 'de' | 'en'
|
||||
}
|
||||
|
||||
interface AnnualCFRow {
|
||||
year: string
|
||||
revenue: number
|
||||
costs: number
|
||||
netCashflow: number
|
||||
cashBalance: number
|
||||
cumulativeFundingNeed: number
|
||||
}
|
||||
|
||||
export default function AnnualCashflowChart({ results, initialFunding, lang }: AnnualCashflowChartProps) {
|
||||
const de = lang === 'de'
|
||||
|
||||
// Aggregate into yearly
|
||||
const yearMap = new Map<number, FMResult[]>()
|
||||
for (const r of results) {
|
||||
if (!yearMap.has(r.year)) yearMap.set(r.year, [])
|
||||
yearMap.get(r.year)!.push(r)
|
||||
}
|
||||
|
||||
let cumulativeNeed = 0
|
||||
const data: AnnualCFRow[] = Array.from(yearMap.entries()).map(([year, months]) => {
|
||||
const revenue = months.reduce((s, m) => s + m.revenue_eur, 0)
|
||||
const costs = months.reduce((s, m) => s + m.total_costs_eur, 0)
|
||||
const netCF = revenue - costs
|
||||
const lastMonth = months[months.length - 1]
|
||||
|
||||
// Cumulative funding need: how much total external capital is needed
|
||||
if (netCF < 0) cumulativeNeed += Math.abs(netCF)
|
||||
|
||||
return {
|
||||
year: year.toString(),
|
||||
revenue: Math.round(revenue),
|
||||
costs: Math.round(costs),
|
||||
netCashflow: Math.round(netCF),
|
||||
cashBalance: Math.round(lastMonth.cash_balance_eur),
|
||||
cumulativeFundingNeed: Math.round(cumulativeNeed),
|
||||
}
|
||||
})
|
||||
|
||||
const formatValue = (value: number) => {
|
||||
if (Math.abs(value) >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
|
||||
if (Math.abs(value) >= 1_000) return `${(value / 1_000).toFixed(0)}k`
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
// Calculate total funding needed beyond initial funding
|
||||
const totalFundingGap = Math.max(0, cumulativeNeed - initialFunding)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-3 gap-2 mb-3">
|
||||
<div className="text-center">
|
||||
<p className="text-[9px] text-white/30 uppercase tracking-wider">
|
||||
{de ? 'Startkapital' : 'Initial Funding'}
|
||||
</p>
|
||||
<p className="text-sm font-bold text-white">{formatValue(initialFunding)} EUR</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-[9px] text-white/30 uppercase tracking-wider">
|
||||
{de ? 'Kum. Finanzbedarf' : 'Cum. Funding Need'}
|
||||
</p>
|
||||
<p className="text-sm font-bold text-amber-400">{formatValue(cumulativeNeed)} EUR</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-[9px] text-white/30 uppercase tracking-wider">
|
||||
{de ? 'Finanzierungsluecke' : 'Funding Gap'}
|
||||
</p>
|
||||
<p className={`text-sm font-bold ${totalFundingGap > 0 ? 'text-red-400' : 'text-emerald-400'}`}>
|
||||
{totalFundingGap > 0 ? formatValue(totalFundingGap) + ' EUR' : (de ? 'Gedeckt' : 'Covered')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<div className="w-full h-[220px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
|
||||
<XAxis
|
||||
dataKey="year"
|
||||
stroke="rgba(255,255,255,0.3)"
|
||||
tick={{ fill: 'rgba(255,255,255,0.5)', fontSize: 11 }}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 10 }}
|
||||
tickFormatter={formatValue}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: 'rgba(10, 10, 26, 0.95)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 12,
|
||||
color: '#fff',
|
||||
fontSize: 11,
|
||||
}}
|
||||
formatter={(value: number, name: string) => {
|
||||
const label =
|
||||
name === 'netCashflow' ? (de ? 'Netto-Cashflow' : 'Net Cash Flow')
|
||||
: name === 'cashBalance' ? (de ? 'Cash-Bestand' : 'Cash Balance')
|
||||
: name === 'cumulativeFundingNeed' ? (de ? 'Kum. Finanzbedarf' : 'Cum. Funding Need')
|
||||
: name
|
||||
return [formatValue(value) + ' EUR', label]
|
||||
}}
|
||||
/>
|
||||
<ReferenceLine y={0} stroke="rgba(255,255,255,0.2)" />
|
||||
|
||||
{/* Net Cashflow Bars */}
|
||||
<Bar dataKey="netCashflow" radius={[4, 4, 4, 4]} barSize={28}>
|
||||
{data.map((entry, i) => (
|
||||
<Cell
|
||||
key={i}
|
||||
fill={entry.netCashflow >= 0 ? 'rgba(34, 197, 94, 0.7)' : 'rgba(239, 68, 68, 0.6)'}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
|
||||
{/* Cash Balance Line */}
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="cashBalance"
|
||||
stroke="#6366f1"
|
||||
strokeWidth={2.5}
|
||||
dot={{ r: 4, fill: '#6366f1', stroke: '#1e1b4b', strokeWidth: 2 }}
|
||||
/>
|
||||
|
||||
{/* Cumulative Funding Need Line */}
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="cumulativeFundingNeed"
|
||||
stroke="#f59e0b"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="5 5"
|
||||
dot={{ r: 3, fill: '#f59e0b' }}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex items-center justify-center gap-4 mt-2 text-[9px] text-white/40">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-3 h-2.5 rounded-sm bg-emerald-500/70 inline-block" />
|
||||
<span className="w-3 h-2.5 rounded-sm bg-red-500/60 inline-block" />
|
||||
{de ? 'Netto-Cashflow' : 'Net Cash Flow'}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-4 h-0.5 bg-indigo-500 inline-block" />
|
||||
{de ? 'Cash-Bestand' : 'Cash Balance'}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-4 h-0.5 inline-block" style={{ borderBottom: '2px dashed #f59e0b' }} />
|
||||
{de ? 'Kum. Finanzbedarf' : 'Cum. Funding Need'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user