From f30ac73b79272d762d9c9149e38b57d012a5f976 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Wed, 20 May 2026 10:01:53 +0200 Subject: [PATCH] fix(pitch-print): cover layout, Finanzplan data source, target_date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../_components/PrintIntroSlides.tsx | 162 ++++++++++-------- .../_components/PrintMarketSlides.tsx | 25 ++- .../app/pitch-print/[versionId]/page.tsx | 25 ++- pitch-deck/lib/finanzplan/adapter.ts | 9 +- 4 files changed, 133 insertions(+), 88 deletions(-) diff --git a/pitch-deck/app/pitch-print/[versionId]/_components/PrintIntroSlides.tsx b/pitch-deck/app/pitch-print/[versionId]/_components/PrintIntroSlides.tsx index 9b6a424..fd1c27f 100644 --- a/pitch-deck/app/pitch-print/[versionId]/_components/PrintIntroSlides.tsx +++ b/pitch-deck/app/pitch-print/[versionId]/_components/PrintIntroSlides.tsx @@ -10,93 +10,103 @@ export function PrintCoverPage({ company, funding, lang, versionName }: { compan const instrument = funding?.instrument || 'Pre-Seed' const amount = funding?.amount_eur || 1_000_000 const tagline = de ? (company?.tagline_de || 'Kontinuierliche Compliance für europäische Unternehmen.') : (company?.tagline_en || 'Continuous compliance for European companies.') + const amountLabel = amount >= 1_000_000 + ? '€' + (amount / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M' + : '€' + Math.round(amount / 1_000) + 'k' + return (
-
+
+ {/* + The .print-page class in print.css forces flex-direction: column !important, + which would collapse a horizontal flex split. We sidestep that by putting a + single full-size grid container as the only child — the column-flex parent + has just one item so direction no longer matters. + */} +
- {/* LEFT INDIGO BLOCK */} -
-
-
- {de ? 'Investor Brief' : 'Investor Brief'} + {/* LEFT INDIGO BLOCK */} +
+
+
+ {de ? 'Investor Brief' : 'Investor Brief'} +
+
+
+ {de + ? 'DSGVO-konforme KI-Plattform für kontinuierliche Code-Security und automatisierte Compliance. Souverän gehostet, integriert in europäische Workflows.' + : 'GDPR-compliant AI platform for continuous code security and automated compliance. Sovereign-hosted, integrated into European workflows.'} +
-
-
- {de - ? 'DSGVO-konforme KI-Plattform für kontinuierliche Code-Security und automatisierte Compliance. Souverän gehostet, integriert in europäische Workflows.' - : 'GDPR-compliant AI platform for continuous code security and automated compliance. Sovereign-hosted, integrated into European workflows.'} + + {/* Mid stats */} +
+
{de ? 'Auf einen Blick' : 'At a glance'}
+
+ {de ? '25 000 + atomare Prüfaspekte' : '25 000 + atomic audit aspects'}
+ {de ? '380 + Regularien · 10 Branchen' : '380 + regulations · 10 industries'}
+ {de ? '500 K + Lines of Code · 45 Container' : '500 K + lines of code · 45 containers'}
+ {de ? '100 % EU-Hosting · BSI Cloud DE' : '100 % EU hosting · BSI cloud DE'} +
+
+ + {/* Footer */} +
+
{versionName}
+
{de ? 'Vertraulich · Nur Investoren' : 'Confidential · Investors only'}
- {/* Hero stat */} -
-
- {de ? '25 000 + atomare Prüfaspekte' : '25 000 + atomic audit aspects'} + {/* RIGHT WHITE PANE */} +
+
+
+ {instrument} · Q4 2026 +
+

+ {company?.name || 'BreakPilot'}. +

+
+

+ {tagline} +

-
- {de ? '380 + Regularien · 10 Branchen' : '380 + regulations · 10 industries'} -
-
- {de ? '500 K + Lines of Code · 45 Container' : '500 K + lines of code · 45 containers'} + + {/* Key terms */} +
+
+ {de ? 'Key Terms' : 'Key terms'} +
+
+ {([ + [de ? 'Funding' : 'Funding', amountLabel], + [de ? 'Pre-Money' : 'Pre-money', '€4.0M'], + [de ? 'Instrument' : 'Instrument', instrument], + [de ? 'Standort' : 'HQ', company?.hq_city || 'Bodman'], + ] as [string, string][]).map(([label, val]) => ( +
+
{label}
+
{val}
+
+ ))} +
+
+ {de + ? 'Gründerteam Benjamin Bönisch (CEO) und Sharang Parnerkar (CTO). Markeneintragung DPMA · EUIPO-Anmeldung in Bearbeitung · GmbH-Gründung August 2026.' + : 'Founding team Benjamin Bönisch (CEO) and Sharang Parnerkar (CTO). Trademark DPMA registered · EUIPO filing in progress · GmbH incorporation August 2026.'} +
- {/* Bottom: version + confidential */} -
-
{versionName}
-
{de ? 'Vertraulich · Nur Investoren' : 'Confidential · Investors only'}
-
-
- - {/* RIGHT WHITE PANE */} -
-
-
- {instrument} · Q4 2026 -
-

- {company?.name || 'BreakPilot'}. -

-
-

- {tagline} -

-
- - {/* Key terms */} -
-
- {de ? 'Key Terms' : 'Key terms'} -
-
- {([ - [de ? 'Funding' : 'Funding', '€' + (amount / 1_000_000).toFixed(1) + 'M'], - [de ? 'Pre-Money' : 'Pre-money', '€4.0M'], - [de ? 'Instrument' : 'Instrument', instrument], - [de ? 'Standort' : 'HQ', company?.hq_city || 'Bodman'], - ] as [string, string][]).map(([label, val]) => ( -
-
{label}
-
{val}
-
- ))} -
-
- {de - ? 'Gründerteam Benjamin Bönisch (CEO) und Sharang Parnerkar (CTO). Markeneintragung DPMA · EUIPO-Anmeldung in Bearbeitung · GmbH-Gründung August 2026.' - : 'Founding team Benjamin Bönisch (CEO) and Sharang Parnerkar (CTO). Trademark DPMA registered · EUIPO filing in progress · GmbH incorporation August 2026.'} -
-
diff --git a/pitch-deck/app/pitch-print/[versionId]/_components/PrintMarketSlides.tsx b/pitch-deck/app/pitch-print/[versionId]/_components/PrintMarketSlides.tsx index e368173..bab42b5 100644 --- a/pitch-deck/app/pitch-print/[versionId]/_components/PrintMarketSlides.tsx +++ b/pitch-deck/app/pitch-print/[versionId]/_components/PrintMarketSlides.tsx @@ -217,6 +217,25 @@ export function PrintTeamPage({ team, lang, pageNum, totalPages, versionName }: /* ===== THE ASK ===== */ +function formatTargetDate(raw: string | undefined, de: boolean): string { + if (!raw) return de ? 'Q3 2026' : 'Q3 2026' + // Accept ISO timestamps, ISO dates, or already-formatted strings. + const d = new Date(raw) + if (isNaN(d.getTime())) return raw + const months = de + ? ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'] + : ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + return `${months[d.getUTCMonth()]} ${d.getUTCFullYear()}` +} + +function formatFunding(amount: number): string { + if (amount >= 1_000_000) { + const m = amount / 1_000_000 + return '€' + (m % 1 === 0 ? m.toFixed(0) : m.toFixed(1)) + 'M' + } + return '€' + Math.round(amount / 1_000) + 'k' +} + export function PrintTheAskPage({ funding, lang, pageNum, totalPages, versionName }: SlideBase & { funding: PitchFunding }) { const de = lang === 'de' const amount = funding?.amount_eur || 1_000_000 @@ -230,7 +249,7 @@ export function PrintTheAskPage({ funding, lang, pageNum, totalPages, versionNam ] return ( - +
{/* Hero amount */} @@ -238,9 +257,9 @@ export function PrintTheAskPage({ funding, lang, pageNum, totalPages, versionNam
{de ? 'Funding' : 'Funding'}
- €{(amount / 1_000_000).toFixed(1)}M + {formatFunding(amount)}
-
{instrument} · {funding?.round_name || 'Pre-Seed'} · {de ? 'Zielabschluss' : 'Target close'}: {funding?.target_date || 'Q3 2026'}
+
{instrument} · {funding?.round_name || 'Pre-Seed'} · {de ? 'Zielabschluss' : 'Target close'}: {formatTargetDate(funding?.target_date, de)}
diff --git a/pitch-deck/app/pitch-print/[versionId]/page.tsx b/pitch-deck/app/pitch-print/[versionId]/page.tsx index 1c92a2d..f4bfa6a 100644 --- a/pitch-deck/app/pitch-print/[versionId]/page.tsx +++ b/pitch-deck/app/pitch-print/[versionId]/page.tsx @@ -6,6 +6,7 @@ import { 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 { @@ -68,17 +69,29 @@ export default async function PitchPrintPage({ params, searchParams }: Ctx) { // 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 - 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[] + // 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> diff --git a/pitch-deck/lib/finanzplan/adapter.ts b/pitch-deck/lib/finanzplan/adapter.ts index ca9c3b5..13dcd5d 100644 --- a/pitch-deck/lib/finanzplan/adapter.ts +++ b/pitch-deck/lib/finanzplan/adapter.ts @@ -17,18 +17,20 @@ export async function finanzplanToFMResults(pool: Pool, scenarioId?: string): Pr } // Load computed data - const [personalRes, liquidRes, betriebRes, umsatzRes, materialRes, investRes] = await Promise.all([ + const [personalRes, liquidRes, betriebRes, umsatzRes, materialRes, investRes, kundenRes] = await Promise.all([ pool.query("SELECT * FROM fp_personalkosten WHERE scenario_id = $1 ORDER BY sort_order", [sid]), pool.query("SELECT * FROM fp_liquiditaet WHERE scenario_id = $1 ORDER BY sort_order", [sid]), pool.query("SELECT * FROM fp_betriebliche_aufwendungen WHERE scenario_id = $1 ORDER BY sort_order", [sid]), pool.query("SELECT * FROM fp_umsatzerloese WHERE scenario_id = $1 AND section = 'revenue' AND row_label = 'GESAMTUMSATZ' LIMIT 1", [sid]), pool.query("SELECT * FROM fp_materialaufwand WHERE scenario_id = $1 AND row_label = 'SUMME' LIMIT 1", [sid]), pool.query("SELECT * FROM fp_investitionen WHERE scenario_id = $1 ORDER BY sort_order", [sid]), + pool.query("SELECT * FROM fp_kunden_summary WHERE scenario_id = $1 AND row_label = 'Bestandskunden gesamt' LIMIT 1", [sid]), ]) const personal = personalRes.rows const liquid = liquidRes.rows const betrieb = betriebRes.rows + const customersByMonth = (kundenRes.rows[0]?.values as MonthlyValues) || emptyMonthly() // Helper to sum a field across personnel function sumPersonalField(field: string): MonthlyValues { @@ -92,9 +94,9 @@ export async function finanzplanToFMResults(pool: Pool, scenarioId?: string): Pr month: m, year, month_in_year: month, - new_customers: 0, + new_customers: Math.max((customersByMonth[`m${m}`] || 0) - prevCustomers, 0), churned_customers: 0, - total_customers: 0, + total_customers: Math.round(customersByMonth[`m${m}`] || 0), mrr_eur: Math.round(rev / 1), // monthly arr_eur: Math.round(rev * 12), revenue_eur: Math.round(rev), @@ -113,6 +115,7 @@ export async function finanzplanToFMResults(pool: Pool, scenarioId?: string): Pr cash_balance_eur: Math.round(cash), cumulative_revenue_eur: Math.round(cumulativeRevenue), }) + prevCustomers = customersByMonth[`m${m}`] || 0 } // Summary