bb85ee2e27
Build pitch-deck / build-push-deploy (push) Successful in 1m59s
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 52s
CI / test-python-voice (push) Successful in 37s
CI / test-bqas (push) Successful in 36s
Two bug fixes plus the requested visual rework — the deck now looks like a pitch deck, not a research paper. Bugs: - BASE_PAGES corrected from 28 to 29; disclaimer no longer shows "29/28" - fmResults + fmAssumptions now load for the standard PDF, not only when financial=true; Finanzplan annex + KPI dashboard now render Visual rework (per user: "graphic elements, not just text"): - Cover: split layout — indigo block left (tagline + hero stats + version meta), white block right with oversized title and key terms - Modules: 12 lucide icons in indigo-50 tiles (ScanLine, ShieldCheck, FileText, ClipboardCheck, Users, UserCheck, AlertTriangle, Brain, Target, GraduationCap, TrendingUp, MessageSquare) - USP cards: icon-led card heads with FileSearch/ArrowLeftRight/Repeat/Layers/etc.; LoopDiagram SVG on the closing "Compliance ↔ Code" hub - How It Works: StepStrip primitive with visible right-arrows between steps - Market: nested-rectangle MarketFunnel (TAM > SAM > SOM) replaces three stacked boxes - Customer Savings: 4 hero KPIs + ComparisonBars (today vs. with BP) per cost item - The Ask: DonutChart for use-of-funds - Cap Table: DonutChart for equity distribution - Finanzplan p2: 2×2 chart grid — Revenue (bars), EBIT (bars, tone by sign), Cash balance (line+area), Headcount (bars) - Architecture: ArchitectureDiagram primitive (3 tiers, vertical arrows between tiers) - AI Pipeline: PipelineFlow primitive (4 stages, horizontal arrows) - Team: founder photos (32×32mm) added; falls back to initials if photo_url missing New primitives: - PrintCharts.tsx — BarChart, LineChart, ComparisonBars, DonutChart, ProgressBar, MarketFunnel - PrintDiagrams.tsx — FlowNode, VArrow, HArrow, StepStrip, ArchitectureDiagram, LoopDiagram, PipelineFlow All files under 500 LOC cap. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
101 lines
3.6 KiB
TypeScript
101 lines
3.6 KiB
TypeScript
import { redirect, notFound } from 'next/navigation'
|
|
import pool from '@/lib/db'
|
|
import { getAdminFromCookie } from '@/lib/admin-auth'
|
|
import {
|
|
Language, PitchData, PitchCompany, PitchTeamMember, PitchFinancial, PitchMarket,
|
|
PitchCompetitor, PitchFeature, PitchMilestone, PitchMetric, PitchFunding, PitchProduct,
|
|
FpScenarioRef, FMResult, FMAssumption,
|
|
} from '@/lib/types'
|
|
import PrintDeck from './_components/PrintDeck'
|
|
|
|
interface Ctx {
|
|
params: Promise<{ versionId: string }>
|
|
searchParams: Promise<{ financial?: string; lang?: string }>
|
|
}
|
|
|
|
export default async function PitchPrintPage({ params, searchParams }: Ctx) {
|
|
const admin = await getAdminFromCookie()
|
|
if (!admin) redirect('/pitch-admin/login')
|
|
|
|
const { versionId } = await params
|
|
const { financial: finParam, lang: langParam } = await searchParams
|
|
const financial = finParam === 'true' || finParam === '1'
|
|
const lang: Language = langParam === 'en' ? 'en' : 'de'
|
|
|
|
// Version metadata
|
|
const verRes = await pool.query(
|
|
`SELECT name FROM pitch_versions WHERE id = $1`,
|
|
[versionId],
|
|
)
|
|
if (verRes.rows.length === 0) notFound()
|
|
const versionName: string = verRes.rows[0].name
|
|
|
|
// Version data tables (snapshot)
|
|
const dataRes = await pool.query(
|
|
`SELECT table_name, data FROM pitch_version_data WHERE version_id = $1`,
|
|
[versionId],
|
|
)
|
|
|
|
const map: Record<string, unknown[]> = {}
|
|
for (const row of dataRes.rows) {
|
|
map[row.table_name] = typeof row.data === 'string' ? JSON.parse(row.data) : row.data
|
|
}
|
|
|
|
if (Object.keys(map).length === 0) {
|
|
return (
|
|
<div style={{ padding: '40px', fontFamily: 'system-ui', color: '#374151' }}>
|
|
<h2>Version has no data</h2>
|
|
<p>Please make sure the version has been populated with pitch data before exporting.</p>
|
|
<a href={`/pitch-admin/versions/${versionId}`} style={{ color: '#6366f1' }}>← Back to version</a>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const pitchData: PitchData = {
|
|
company: ((map.company || [])[0] || {}) as PitchCompany,
|
|
team: (map.team || []) as PitchTeamMember[],
|
|
financials: (map.financials || []) as PitchFinancial[],
|
|
market: (map.market || []) as PitchMarket[],
|
|
competitors: (map.competitors || []) as PitchCompetitor[],
|
|
features: (map.features || []) as PitchFeature[],
|
|
milestones: (map.milestones || []) as PitchMilestone[],
|
|
metrics: (map.metrics || []) as PitchMetric[],
|
|
funding: ((map.funding || [])[0] || {}) as PitchFunding,
|
|
products: (map.products || []) as PitchProduct[],
|
|
fp_scenarios: (map.fm_scenarios || []) as FpScenarioRef[],
|
|
}
|
|
|
|
// Always fetch FM results + assumptions so the standard PDF can render the
|
|
// annex-finanzplan slide. The `financial` flag only adds the extra detail
|
|
// P&L page and the cap-table page.
|
|
let fmResults: FMResult[] = []
|
|
let fmAssumptions: FMAssumption[] = []
|
|
|
|
const scenarios = (map.fm_scenarios || []) as FpScenarioRef[]
|
|
const defaultScenario = scenarios.find(s => s.is_default) ?? scenarios[0] ?? null
|
|
if (defaultScenario?.id) {
|
|
const resultsRes = await pool.query(
|
|
`SELECT * FROM pitch_fm_results WHERE scenario_id = $1 ORDER BY month`,
|
|
[defaultScenario.id],
|
|
)
|
|
fmResults = resultsRes.rows as FMResult[]
|
|
}
|
|
|
|
const rawAssumptions = (map.fm_assumptions || []) as Array<Record<string, unknown>>
|
|
fmAssumptions = rawAssumptions.map(a => ({
|
|
...a,
|
|
value: typeof a.value === 'string' ? JSON.parse(a.value as string) : a.value,
|
|
})) as FMAssumption[]
|
|
|
|
return (
|
|
<PrintDeck
|
|
pitchData={pitchData}
|
|
versionName={versionName}
|
|
fmResults={fmResults}
|
|
fmAssumptions={fmAssumptions}
|
|
financial={financial}
|
|
lang={lang}
|
|
/>
|
|
)
|
|
}
|