Files
breakpilot-core/pitch-deck/app/pitch-print/[versionId]/page.tsx
T
Sharang Parnerkar f30ac73b79
Build pitch-deck / build-push-deploy (push) Successful in 1m34s
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 30s
CI / test-python-voice (push) Successful in 30s
CI / test-bqas (push) Successful in 28s
fix(pitch-print): cover layout, Finanzplan data source, target_date
Three critical fixes after reviewing the rendered PDF:

Cover (was: indigo block collapsed to top, white content stacked below):
- The .print-page class in print.css forces flex-direction: column !important,
  which broke the horizontal split. Wrap the cover content in a single grid
  container — the column-flex parent then has only one child so direction is
  irrelevant. Indigo block now runs full-height on the left.
- Title reduced 88pt -> 60pt so "BreakPilot ComplAI." fits without wrapping.
- Funding amount formatter now handles sub-€1M cases (€200k vs €0.2M).

Finanzplan (was: "nicht verfügbar" on both pages 20-21):
- page.tsx was querying the legacy pitch_fm_results table which isn't populated
  by the current pipeline. The interactive deck reads from fp_* tables.
- Wire in lib/finanzplan/adapter.ts (finanzplanToFMResults) which bridges the
  live fp_* tables to FMResult[] — same source the interactive deck uses.
- Fall back to live default fp_scenario if the version snapshot's fm_scenarios
  is empty.
- adapter.ts: populate total_customers + new_customers from fp_kunden_summary
  (was hardcoded 0).

The Ask:
- target_date was rendering as raw ISO timestamp "2026-08-01T00:00:00.000Z";
  now formatted as "Aug 2026" (locale-aware).
- Hero funding amount uses same sub-€1M formatter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 10:01:53 +02:00

114 lines
4.3 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 { finanzplanToFMResults } from '@/lib/finanzplan/adapter'
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.
//
// Data source: the live `fp_*` tables (same as the interactive deck), bridged
// to FMResult[] via finanzplanToFMResults. The legacy `pitch_fm_results` table
// is no longer populated by the current pipeline.
let fmResults: FMResult[] = []
let fmAssumptions: FMAssumption[] = []
const scenarios = (map.fm_scenarios || []) as FpScenarioRef[]
const defaultScenario = scenarios.find(s => s.is_default) ?? scenarios[0] ?? null
// Snapshot stores fp_scenario IDs under `fm_scenarios`; fall back to the live
// default fp scenario if the snapshot is empty (older versions).
let scenarioId: string | null = defaultScenario?.id ? String(defaultScenario.id) : null
if (!scenarioId) {
const liveRes = await pool.query(`SELECT id FROM fp_scenarios WHERE is_default = true LIMIT 1`)
scenarioId = liveRes.rows[0]?.id ? String(liveRes.rows[0].id) : null
}
if (scenarioId) {
try {
const fpResponse = await finanzplanToFMResults(pool, scenarioId)
fmResults = fpResponse.results
} catch {
fmResults = []
}
}
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}
/>
)
}