From 2e8cbfff3f3dda38e490469c5b79e660dd1e1373 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Tue, 12 May 2026 13:00:19 +0200 Subject: [PATCH 1/2] feat(pitch-deck): add per-version PDF export (standard + financial) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds /pitch-print/[versionId] — a server-rendered, print-CSS-optimized page that generates investor-ready PDFs via the browser's native print dialog (Save as PDF). Two variants per version: - Standard PDF (9 pages): Cover, Problem, Solution, Products, Market, Team, Milestones, The Ask - Financial PDF (+4 pages): adds Financials P&L table (aggregated from pitch_fm_results), Assumptions, Cap Table, Legal Disclaimer White background with indigo accents, A4 landscape via @page CSS, all color-rendered in print via print-color-adjust: exact. Auto-triggers window.print() 900ms after load. Admin toolbar visible on screen only. Export buttons added to /pitch-admin/versions/[id] detail page. Co-Authored-By: Claude Sonnet 4.6 --- .../(authed)/versions/[id]/page.tsx | 16 +- .../_components/PrintCoreSlides.tsx | 289 ++++++++++++++++++ .../[versionId]/_components/PrintDeck.tsx | 142 +++++++++ .../_components/PrintFinancialSlides.tsx | 277 +++++++++++++++++ .../[versionId]/_components/PrintLayout.tsx | 175 +++++++++++ .../app/pitch-print/[versionId]/page.tsx | 101 ++++++ 6 files changed, 999 insertions(+), 1 deletion(-) create mode 100644 pitch-deck/app/pitch-print/[versionId]/_components/PrintCoreSlides.tsx create mode 100644 pitch-deck/app/pitch-print/[versionId]/_components/PrintDeck.tsx create mode 100644 pitch-deck/app/pitch-print/[versionId]/_components/PrintFinancialSlides.tsx create mode 100644 pitch-deck/app/pitch-print/[versionId]/_components/PrintLayout.tsx create mode 100644 pitch-deck/app/pitch-print/[versionId]/page.tsx diff --git a/pitch-deck/app/pitch-admin/(authed)/versions/[id]/page.tsx b/pitch-deck/app/pitch-admin/(authed)/versions/[id]/page.tsx index a8cc669..72d66ae 100644 --- a/pitch-deck/app/pitch-admin/(authed)/versions/[id]/page.tsx +++ b/pitch-deck/app/pitch-admin/(authed)/versions/[id]/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useState, useCallback } from 'react' import { useParams, useRouter } from 'next/navigation' import Link from 'next/link' -import { ArrowLeft, Lock, Save, GitFork, Eye, Code } from 'lucide-react' +import { ArrowLeft, Lock, Save, GitFork, Eye, Code, FileText, BarChart3 } from 'lucide-react' import TabEditor from './_components/TabEditors' const TABLE_LABELS: Record = { @@ -134,6 +134,20 @@ export default function VersionEditorPage() { > Preview + + Export PDF + + + Export PDF + Financial + {isDraft && ( + + + + + {/* Print pages */} +
+ {/* Page 1: Cover */} + + + {/* Page 2: Problem */} + + + {/* Page 3: Solution */} + + + {/* Page 4: Products */} + {pitchData.products?.length > 0 && ( + + )} + + {/* Page 5: Market */} + {pitchData.market?.length > 0 && ( + + )} + + {/* Page 6: Team */} + {pitchData.team?.length > 0 && ( + + )} + + {/* Page 7: Milestones */} + {pitchData.milestones?.length > 0 && ( + + )} + + {/* Page 8-9: The Ask (always last of core) */} + + + {/* Financial annex */} + {financial && ( + <> + {annualRows.length > 0 && ( + + )} + {fmAssumptions.length > 0 && ( + + )} + {!isWandeldarlehen && ( + + )} + + + )} +
+ + ) +} diff --git a/pitch-deck/app/pitch-print/[versionId]/_components/PrintFinancialSlides.tsx b/pitch-deck/app/pitch-print/[versionId]/_components/PrintFinancialSlides.tsx new file mode 100644 index 0000000..d9f976c --- /dev/null +++ b/pitch-deck/app/pitch-print/[versionId]/_components/PrintFinancialSlides.tsx @@ -0,0 +1,277 @@ +import { PrintPage, SectionTitle, PrintTable, Badge, COLORS } from './PrintLayout' +import { Language, FMResult, FMAssumption } from '@/lib/types' + +interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string } + +interface AnnualPLRow { + year: number + revenue_eur: number + cogs_eur: number + gross_profit_eur: number + personnel_eur: number + marketing_eur: number + infra_eur: number + ebitda_eur: number + total_customers: number + employees_count: number +} + +export function aggregateAnnualRows(results: FMResult[]): AnnualPLRow[] { + const byYear = new Map() + for (const r of results) { + if (!byYear.has(r.year)) byYear.set(r.year, []) + byYear.get(r.year)!.push(r) + } + return Array.from(byYear.entries()) + .sort(([a], [b]) => a - b) + .map(([year, rows]) => { + const last = rows[rows.length - 1] + const revenue = rows.reduce((s, r) => s + r.revenue_eur, 0) + const cogs = rows.reduce((s, r) => s + r.cogs_eur, 0) + const personnel = rows.reduce((s, r) => s + r.personnel_eur, 0) + const marketing = rows.reduce((s, r) => s + r.marketing_eur, 0) + const infra = rows.reduce((s, r) => s + r.infra_eur, 0) + const gross = revenue - cogs + const ebitda = gross - personnel - marketing - infra + return { + year, + revenue_eur: revenue, + cogs_eur: cogs, + gross_profit_eur: gross, + personnel_eur: personnel, + marketing_eur: marketing, + infra_eur: infra, + ebitda_eur: ebitda, + total_customers: last?.total_customers ?? 0, + employees_count: last?.employees_count ?? 0, + } + }) +} + +function fmtEur(n: number) { + const abs = Math.abs(n) + if (abs >= 1_000_000) return `${(n / 1_000_000).toLocaleString('de-DE', { maximumFractionDigits: 1 })}M` + if (abs >= 1_000) return `${(n / 1_000).toLocaleString('de-DE', { maximumFractionDigits: 0 })}k` + return n.toLocaleString('de-DE', { maximumFractionDigits: 0 }) +} + +function colorEur(n: number) { return n >= 0 ? '#16a34a' : '#dc2626' } + +export function PrintFinancialsPage({ annualRows, lang, pageNum, totalPages, versionName }: SlideBase & { annualRows: AnnualPLRow[] }) { + const de = lang === 'de' + const headers = [ + de ? 'Jahr' : 'Year', + de ? 'Umsatz' : 'Revenue', + de ? 'Rohertrag' : 'Gross Profit', + de ? 'Personal' : 'Personnel', + de ? 'Marketing' : 'Marketing', + 'Infra', + 'EBITDA', + de ? 'Kunden' : 'Customers', + de ? 'MA' : 'FTE', + ] + const rows = annualRows.map(r => [ + {r.year}, + {fmtEur(r.revenue_eur)}, + fmtEur(r.gross_profit_eur), + `(${fmtEur(r.personnel_eur)})`, + `(${fmtEur(r.marketing_eur)})`, + `(${fmtEur(r.infra_eur)})`, + {fmtEur(r.ebitda_eur)}, + r.total_customers.toString(), + r.employees_count.toString(), + ]) + + const finalYear = annualRows[annualRows.length - 1] + const breakEvenYear = annualRows.find(r => r.ebitda_eur > 0)?.year + + return ( + + + {de ? 'Finanzprognose (Planzahlen)' : 'Financial Projections (Plan)'} + +
+ +
+ {(finalYear || breakEvenYear) && ( +
+ {finalYear &&
+

{de ? 'ARR (letztes Jahr)' : 'ARR (final year)'}

+

{fmtEur(finalYear.revenue_eur)}

+
} + {finalYear &&
+

{de ? 'Kunden (letztes Jahr)' : 'Customers (final year)'}

+

{finalYear.total_customers}

+
} + {breakEvenYear &&
+

{de ? 'Break-Even' : 'Break-Even'}

+

{breakEvenYear}

+
} +
+ )} +

+ {de ? '* Planzahlen · Szenario: Base Case · In Klammern = Kosten' : '* Projections · Scenario: Base Case · Parentheses = costs'} +

+
+ ) +} + +export function PrintAssumptionsPage({ assumptions, lang, pageNum, totalPages, versionName }: SlideBase & { assumptions: FMAssumption[] }) { + const de = lang === 'de' + const scalars = assumptions + .filter(a => a.value_type === 'scalar') + .sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) + .slice(0, 28) + + const byCategory = scalars.reduce>((acc, a) => { + const cat = a.category || 'General' + if (!acc[cat]) acc[cat] = [] + acc[cat].push(a) + return acc + }, {}) + + return ( + + + {de ? 'Finanzielle Annahmen' : 'Financial Assumptions'} + +
+ {Object.entries(byCategory).slice(0, 4).map(([cat, items]) => ( +
+

+ {cat} +

+ + + {items.map(a => ( + + + + + ))} + +
{de ? a.label_de : a.label_en} + {typeof a.value === 'number' ? a.value.toLocaleString('de-DE') : String(a.value)} + {a.unit && {a.unit}} +
+
+ ))} +
+
+ ) +} + +const CAP_TABLE_DATA = [ + { name: 'Benjamin Bönisch (CEO)', pct: 37.3, color: '#6366f1' }, + { name: 'Sharang Parnerkar (CTO)', pct: 37.3, color: '#8b5cf6' }, + { name: 'Pre-Seed Investor', pct: 20.0, color: '#f59e0b' }, + { name: 'ESOP Pool', pct: 5.4, color: '#94a3b8' }, +] + +export function PrintCapTablePage({ lang, pageNum, totalPages, versionName }: SlideBase) { + const de = lang === 'de' + return ( + + + {de ? 'Investition & Anteilsverteilung' : 'Investment & Share Distribution'} + +
+ {/* Stacked bar */} +
+

+ {de ? 'Anteilsverteilung nach Pre-Seed' : 'Share Distribution Post Pre-Seed'} +

+
+ {CAP_TABLE_DATA.map(d => ( +
+ ))} +
+ {CAP_TABLE_DATA.map(d => ( +
+
+ {d.name} + {d.pct}% +
+ ))} +
+ + {/* Deal terms */} +
+

+ {de ? 'Konditionen' : 'Deal Terms'} +

+ +
+
+ + ) +} + +const DISCLAIMER_DE = { + heading: 'Rechtlicher Hinweis', + h1: 'Haftungsausschluss', + p1: 'Dieses Dokument wird vorgelegt von Benjamin Boenisch, wohnhaft in Bodman, Deutschland, und Sharang Parnerkar, wohnhaft in Engen, Deutschland (nachfolgend „Gründer"). Die Gründer beabsichtigen die Gründung der BreakPilot GmbH im dritten Quartal 2026. Zum Zeitpunkt der Erstellung dieses Dokuments ist die Gesellschaft weder gegründet noch im Handelsregister eingetragen.', + p2: 'Dieses Dokument stellt weder ein Angebot zum Verkauf noch eine Aufforderung zur Abgabe eines Angebots zum Erwerb von Wertpapieren dar. Es handelt sich nicht um einen Wertpapierprospekt im Sinne des VermAnlG oder der EU-Prospektverordnung.', + p3: 'Dieses Dokument enthält zukunftsgerichtete Aussagen, die auf gegenwärtigen Erwartungen und Annahmen beruhen. Sämtliche Finanzangaben sind Planzahlen und stellen keine Garantie für künftige Ergebnisse dar.', + p4: 'Eine Beteiligung an einem jungen Unternehmen ist mit erheblichen Risiken verbunden, einschließlich des Risikos eines Totalverlusts des eingesetzten Kapitals.', + h2: 'Vertraulichkeit', + p5: 'Dieses Dokument ist vertraulich und wurde ausschließlich für den namentlich eingeladenen Empfänger erstellt. Durch die Kenntnisnahme erklärt sich der Empfänger mit folgenden Bedingungen einverstanden:', + pa: '(a) Geheimhaltung — Inhalt vertraulich behandeln und nicht an Dritte weitergeben.', + pb: '(b) Zweckbindung — Ausschließlich zur Bewertung einer möglichen Beteiligung verwenden.', + pc: '(c) Geltungsdauer — Diese Vertraulichkeitsverpflichtung gilt für drei (3) Jahre ab Übermittlung. Gerichtsstand ist Konstanz, Deutschland.', + footer: 'Stand: April 2026 · Dieser Hinweis ersetzt keine Rechtsberatung.', +} + +const DISCLAIMER_EN = { + heading: 'Legal Notice', + h1: 'Disclaimer', + p1: 'This document is presented by Benjamin Boenisch, residing in Bodman, Germany, and Sharang Parnerkar, residing in Engen, Germany (hereinafter "Founders"). The Founders intend to establish BreakPilot GmbH in Q3 2026. At the time of this document, the company is neither founded nor registered.', + p2: 'This document constitutes neither an offer to sell nor a solicitation of an offer to acquire securities. It is not a securities prospectus within the meaning of VermAnlG or the EU Prospectus Regulation.', + p3: 'This document contains forward-looking statements based on current expectations. All financial figures are projections and do not constitute a guarantee of future results.', + p4: 'An investment in a young company involves significant risks, including the risk of total loss of invested capital.', + h2: 'Confidentiality', + p5: 'This document is confidential and prepared exclusively for the personally invited recipient. By accessing, the recipient agrees to:', + pa: '(a) Confidentiality — Treat contents confidentially and not disclose to third parties.', + pb: '(b) Purpose limitation — Use only for evaluating a possible participation.', + pc: '(c) Duration — This obligation applies for three (3) years from transmission. Place of jurisdiction is Konstanz, Germany.', + footer: 'As of: April 2026 · This notice does not replace legal advice.', +} + +export function PrintDisclaimerPage({ lang, pageNum, totalPages, versionName }: SlideBase) { + const d = lang === 'de' ? DISCLAIMER_DE : DISCLAIMER_EN + const sectionStyle = { padding: '10px 12px', border: `1px solid ${COLORS.border}`, borderRadius: '6px', marginBottom: '8px' } + const pStyle = { fontSize: '8px', color: COLORS.med, lineHeight: 1.55, margin: '4px 0 0' } + const hStyle = { fontSize: '9px', fontWeight: 700, color: COLORS.indigo, margin: 0, textTransform: 'uppercase' as const, letterSpacing: '0.05em' } + return ( + + {d.heading} +
+
+

{d.h1}

+

{d.p1}

+

{d.p2}

+

{d.p3}

+

{d.p4}

+
+
+

{d.h2}

+

{d.p5}

+

{d.pa}

+

{d.pb}

+

{d.pc}

+
+
+

{d.footer}

+
+ ) +} diff --git a/pitch-deck/app/pitch-print/[versionId]/_components/PrintLayout.tsx b/pitch-deck/app/pitch-print/[versionId]/_components/PrintLayout.tsx new file mode 100644 index 0000000..b02815e --- /dev/null +++ b/pitch-deck/app/pitch-print/[versionId]/_components/PrintLayout.tsx @@ -0,0 +1,175 @@ +import React from 'react' + +const INDIGO = '#6366f1' +const INDIGO_LIGHT = '#eef2ff' +const TEXT_DARK = '#1e1b4b' +const TEXT_MED = '#374151' +const TEXT_LIGHT = '#6b7280' +const BORDER = '#e0e7ff' + +interface PrintPageProps { + title: string + pageNum: number + totalPages: number + versionName: string + children: React.ReactNode + noPadding?: boolean +} + +export function PrintPage({ title, pageNum, totalPages, versionName, children, noPadding }: PrintPageProps) { + return ( +
+ {/* Header bar */} +
+ + BreakPilot + + {title} +
+ + {/* Content area */} +
+ {children} +
+ + {/* Footer bar */} +
+ {versionName} + + CONFIDENTIAL + + {pageNum} / {totalPages} +
+
+ ) +} + +interface SectionTitleProps { + children: React.ReactNode + subtitle?: string +} + +export function SectionTitle({ children, subtitle }: SectionTitleProps) { + return ( +
+

+ {children} +

+ {subtitle && ( +

+ {subtitle} +

+ )} +
+ ) +} + +interface TableProps { + headers: string[] + rows: (string | React.ReactNode)[][] + colWidths?: string[] +} + +export function PrintTable({ headers, rows, colWidths }: TableProps) { + return ( + + + + {headers.map((h, i) => ( + + ))} + + + + {rows.map((row, ri) => ( + + {row.map((cell, ci) => ( + + ))} + + ))} + +
+ {h} +
+ {cell} +
+ ) +} + +export function Badge({ children, color = INDIGO }: { children: React.ReactNode; color?: string }) { + return ( + + {children} + + ) +} + +export const COLORS = { indigo: INDIGO, indigoLight: INDIGO_LIGHT, dark: TEXT_DARK, med: TEXT_MED, light: TEXT_LIGHT, border: BORDER } diff --git a/pitch-deck/app/pitch-print/[versionId]/page.tsx b/pitch-deck/app/pitch-print/[versionId]/page.tsx new file mode 100644 index 0000000..737749e --- /dev/null +++ b/pitch-deck/app/pitch-print/[versionId]/page.tsx @@ -0,0 +1,101 @@ +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 = {} + 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 ( +
+

Version has no data

+

Please make sure the version has been populated with pitch data before exporting.

+ ← Back to version +
+ ) + } + + 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[], + } + + // Financial variant: fetch FM results + parse assumptions + let fmResults: FMResult[] = [] + let fmAssumptions: FMAssumption[] = [] + + if (financial) { + 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> + fmAssumptions = rawAssumptions.map(a => ({ + ...a, + value: typeof a.value === 'string' ? JSON.parse(a.value as string) : a.value, + })) as FMAssumption[] + } + + return ( + + ) +} From cf18b1074aa79b8b777ddcac74b7bcc779168d4b Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Tue, 12 May 2026 13:40:31 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(pitch-deck):=20PDF=20print=20layout=20?= =?UTF-8?q?=E2=80=94=20fill=20page=20height,=20fix=20page=20breaks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch from inline pageBreakAfter to CSS class `.print-page` with explicit `page-break-after: always !important` so Chrome print preview creates a new page per slide (was collapsing to 2 pages) - Remove margin/box-shadow in @media print so A4 boundaries align - Content areas now use flex:1 so cards/pillars stretch to fill the full page height (no more blank void below content) - Remove conditional rendering on data-dependent slides — always render all 9 core pages - Larger font sizes throughout (11px body, 13px card titles) Co-Authored-By: Claude Sonnet 4.6 --- .../_components/PrintCoreSlides.tsx | 202 +++++++++--------- .../[versionId]/_components/PrintDeck.tsx | 114 ++++------ .../[versionId]/_components/PrintLayout.tsx | 86 +++----- 3 files changed, 181 insertions(+), 221 deletions(-) diff --git a/pitch-deck/app/pitch-print/[versionId]/_components/PrintCoreSlides.tsx b/pitch-deck/app/pitch-print/[versionId]/_components/PrintCoreSlides.tsx index ea85e75..b7d6b5d 100644 --- a/pitch-deck/app/pitch-print/[versionId]/_components/PrintCoreSlides.tsx +++ b/pitch-deck/app/pitch-print/[versionId]/_components/PrintCoreSlides.tsx @@ -2,25 +2,25 @@ import { PrintPage, SectionTitle, PrintTable, Badge, COLORS } from './PrintLayou import { Language, PitchCompany, PitchFunding, PitchProduct, PitchMarket, PitchTeamMember, PitchMilestone } from '@/lib/types' const DE_PROBLEM_CARDS = [ - { title: 'KI-Dilemma', stat: 'Abgehängt', desc: 'Produzierende Unternehmen brauchen KI, um wettbewerbsfähig zu bleiben. Aber US-KI an den eigenen Quellcode zu lassen, kommt für die meisten nicht in Frage. Wer auf US-KI verzichtet, verliert den Anschluss. Wer sie nutzt, riskiert seine Datensouveränität.' }, - { title: 'Patriot Act + FISA 702', stat: 'Kein Schutz', desc: 'Selbst wer EU-Server bei AWS, Google oder Microsoft bucht, ist nicht geschützt. US-Gesetze wie FISA 702 und der Cloud Act gelten extraterritorial — US-Behörden können auf Daten zugreifen, egal wo der Server steht.' }, - { title: 'Regulierungs-Tsunami', stat: 'Nicht tragbar', desc: 'Seit 2024 greifen AI Act, NIS2 und Cyber Resilience Act — zusätzlich zu DSGVO, Data Act, Maschinenverordnung und Lieferkettengesetz. KMU können die Compliance-Kosten nicht mehr allein stemmen.' }, + { title: 'KI-Dilemma', stat: 'Abgehängt', desc: 'Produzierende Unternehmen brauchen KI, um wettbewerbsfähig zu bleiben. Aber US-KI an den eigenen Quellcode und die Konstruktionsdaten zu lassen, kommt für die meisten nicht in Frage. Wer auf US-KI verzichtet, verliert den Anschluss. Wer sie nutzt, riskiert seine Datensouveränität und verstößt gegen europäisches Recht.' }, + { title: 'Patriot Act + FISA 702', stat: 'Kein Schutz', desc: 'Selbst wer EU-Server bei AWS, Google oder Microsoft bucht, ist nicht geschützt. US-Gesetze wie FISA 702 und der Cloud Act gelten extraterritorial — US-Behörden können auf Daten zugreifen, egal wo der Server steht. Das Schrems-II-Urteil des EuGH hat das bestätigt.' }, + { title: 'Regulierungs-Tsunami', stat: 'Nicht tragbar', desc: 'Seit 2024 greifen AI Act, NIS2 und Cyber Resilience Act — zusätzlich zu DSGVO, Data Act, Maschinenverordnung und Lieferkettengesetz. Europäische Unternehmen tragen Compliance-Kosten, die US- und Asien-Konkurrenten nicht haben. KMU können das nicht mehr allein stemmen.' }, ] const EN_PROBLEM_CARDS = [ - { title: 'AI Dilemma', stat: 'Left Behind', desc: 'Manufacturing companies need AI to stay competitive. But letting US AI access their source code is out of the question for most. Those avoiding US AI fall behind. Those using it risk their data sovereignty.' }, - { title: 'Patriot Act + FISA 702', stat: 'No Protection', desc: 'Even booking EU servers at AWS, Google or Microsoft offers no protection. US laws like FISA 702 and the Cloud Act apply extraterritorially — US authorities can access data regardless of server location.' }, - { title: 'Regulation Tsunami', stat: 'Unsustainable', desc: 'Since 2024, the AI Act, NIS2 and CRA apply — on top of GDPR, Data Act, Machinery Regulation. European companies bear compliance costs that US and Asian competitors do not face.' }, + { title: 'AI Dilemma', stat: 'Left Behind', desc: 'Manufacturing companies need AI to stay competitive. But letting US AI access their source code and engineering data is out of the question for most. Those avoiding US AI fall behind. Those using it risk their data sovereignty and may violate European law.' }, + { title: 'Patriot Act + FISA 702', stat: 'No Protection', desc: 'Even booking EU servers at AWS, Google or Microsoft offers no protection. US laws like FISA 702 and the Cloud Act apply extraterritorially — US authorities can access data regardless of server location. The Schrems II ruling by the CJEU confirmed this.' }, + { title: 'Regulation Tsunami', stat: 'Unsustainable', desc: 'Since 2024, the AI Act, NIS2 and Cyber Resilience Act apply — on top of GDPR, Data Act, Machinery Regulation and Supply Chain Act. European companies bear compliance costs that US and Asian competitors do not face. SMEs can no longer handle this alone.' }, ] const DE_PILLARS = [ - { title: 'Kontinuierliche Code-Security', desc: 'SAST, DAST, SBOM und Pentesting bei jeder Code-Änderung — nicht einmal im Jahr. Findings direkt als Tickets im Issue-Tracker. 15.000+ EUR/Jahr Pentest-Kosten gespart.' }, - { title: 'Compliance auf Autopilot', desc: 'VVT, TOMs, DSFA, Löschfristen, CE-Risikobeurteilung automatisch. Nach dem Audit: Abweichungen End-to-End — Rollen, Stichtage, Tickets, Nachweise, Eskalation bis zur GF.' }, - { title: 'Deutsche Cloud, volle Integration', desc: 'BSI-zertifizierte Cloud in Deutschland. Live-Support via Jitsi und Matrix. Keine US-SaaS im Source Code. Optional Mac Mini/Studio für maximale Privacy.' }, + { title: 'Kontinuierliche Code-Security', desc: 'SAST, DAST, SBOM und Pentesting bei jeder Code-Änderung — nicht einmal im Jahr. Findings direkt als Tickets im Issue-Tracker deiner Wahl, mit Implementierungsvorschlägen. 15.000+ EUR pro Jahr und Anwendung an Pentest-Kosten gespart. Kein manueller Aufwand, keine vergessenen Schwachstellen.' }, + { title: 'Compliance auf Autopilot', desc: 'VVT, TOMs, DSFA, Löschfristen, CE-Risikobeurteilung automatisch generiert. Nach dem Audit: Haupt- und Nebenabweichungen End-to-End — Rollen zuweisen, Stichtage, Tickets, Nachweise einfordern, Eskalation an GF. Kein Excel, kein Hinterherlaufen, kein Stressaudit.' }, + { title: 'Deutsche Cloud, volle Integration', desc: 'BSI-zertifizierte Cloud in Deutschland. Live-Support über Jitsi (Video) und Matrix (Chat). Keine US-SaaS im Source Code — DSGVO-konform by design. Optional: Mac Mini/Studio für maximale Privacy bei Kleinstunternehmen. Nahtlose Integration in bestehende Workflows.' }, ] const EN_PILLARS = [ - { title: 'Continuous Code Security', desc: 'SAST, DAST, SBOM and pentesting on every code change — not once a year. Findings as tickets in your issue tracker. EUR 15,000+ per year in pentest costs saved.' }, - { title: 'Compliance on Autopilot', desc: 'RoPA, TOMs, DPIA, retention policies, CE risk assessment generated automatically. Post-audit deviations end-to-end: roles, deadlines, tickets, evidence, escalation to management.' }, - { title: 'German Cloud, Full Integration', desc: 'BSI-certified cloud in Germany. Live support via Jitsi and Matrix. No US SaaS in source code. Optional Mac Mini/Studio for maximum privacy.' }, + { title: 'Continuous Code Security', desc: 'SAST, DAST, SBOM and pentesting on every code change — not once a year. Findings as tickets in the issue tracker of your choice, with implementation suggestions. EUR 15,000+ per year per application in pentest costs saved. No manual effort, no forgotten vulnerabilities.' }, + { title: 'Compliance on Autopilot', desc: 'RoPA, TOMs, DPIA, retention policies, CE risk assessment generated automatically. Post-audit: major and minor deviations end-to-end — role assignment, deadlines, tickets, evidence collection, escalation to management. No Excel, no chasing, no stress audits.' }, + { title: 'German Cloud, Full Integration', desc: 'BSI-certified cloud in Germany. Live support via Jitsi (video) and Matrix (chat). No US SaaS in source code — GDPR compliant by design. Optional: Mac Mini/Studio for maximum privacy for micro businesses. Seamless integration into existing workflows.' }, ] function fmtEur(n: number) { return n.toLocaleString('de-DE', { maximumFractionDigits: 0 }) + ' EUR' } @@ -31,46 +31,41 @@ export function PrintCoverPage({ company, funding, versionName, lang }: { compan const de = lang === 'de' const instrument = funding?.instrument || 'Pre-Seed' return ( -
- {/* Big indigo header */} -
- BreakPilot - +
+
+ BreakPilot + {de ? 'Compliance & Code-Security' : 'Compliance & Code Security'}
- {/* Main content */} -
-

+

+

{instrument} · {versionName}

-

+

{company?.name || 'BreakPilot'}

-

+

{de ? (company?.tagline_de || 'Kontinuierliche Compliance für europäische Unternehmen.') : (company?.tagline_en || 'Continuous compliance for European companies.')}

- - {/* Info row */} -
- {[ +
+ {([ [de ? 'Gegründet' : 'Founded', company?.founding_date ? new Date(company.founding_date).getFullYear().toString() : 'Aug 2026'], [de ? 'Standort' : 'HQ', company?.hq_city || 'Bodman-Ludwigshafen'], [de ? 'Instrument' : 'Instrument', instrument], - [de ? 'Zielrunde' : 'Round', funding?.round_name || 'Pre-Seed'], - ].map(([label, val]) => ( + [de ? 'Runde' : 'Round', funding?.round_name || 'Pre-Seed'], + ] as [string, string][]).map(([label, val]) => (
-

{label}

-

{val}

+

{label}

+

{val}

))}
- {/* Footer */} -
- +
+ {de ? 'Vertraulich — Nur für Investoren' : 'Confidential — For Investor Use Only'}
@@ -86,19 +81,19 @@ export function PrintProblemPage({ lang, pageNum, totalPages, versionName }: Sli {de ? 'Das Problem' : 'The Problem'} -
+
{cards.map((c) => ( -
-
- {c.stat} - {c.title} +
+
+ {c.stat} + {c.title}
-

{c.desc}

+

{c.desc}

))}
-
-

+

+

{de ? '„Produzierende Unternehmen brauchen eine KI-Lösung, die in Europa läuft, ihren Code schützt und Compliance automatisiert — ohne ihre Daten an US-Konzerne zu geben."' : '"Manufacturing companies need an AI solution that runs in Europe, protects their code and automates compliance — without giving their data to US corporations."'}

@@ -115,24 +110,22 @@ export function PrintSolutionPage({ lang, pageNum, totalPages, versionName }: Sl {de ? 'Die Lösung' : 'The Solution'} -
+
{pillars.map((p, i) => ( -
-
+
+
{icons[i]}
-

{p.title}

-

{p.desc}

+

{p.title}

+

{p.desc}

))}
-
+
{(de ? ['BSI-Cloud DE', 'DSGVO-konform', 'Kein US-SaaS', 'Kontinuierlich, nicht einmal/Jahr'] : ['BSI Cloud DE', 'GDPR Compliant', 'No US SaaS', 'Continuous, not once/year'] - ).map(tag => ( - {tag} - ))} + ).map(tag => {tag})}
) @@ -142,7 +135,7 @@ export function PrintProductPage({ products, lang, pageNum, totalPages, versionN const de = lang === 'de' const headers = [de ? 'Produkt' : 'Product', de ? 'Preis/Monat' : 'Price/Month', 'Hardware', de ? 'Key Features' : 'Key Features'] const rows = products.map(p => [ - {p.name}{p.is_popular ? : null}, + {p.name}{p.is_popular ? ★ Beliebt : null}, fmtEur(p.monthly_price_eur), p.hardware || '—', (de ? p.features_de : p.features_en)?.slice(0, 3).join(', ') || '—', @@ -152,13 +145,15 @@ export function PrintProductPage({ products, lang, pageNum, totalPages, versionN {de ? 'Produkte & Pricing' : 'Products & Pricing'} -
+
-
-
-

- {de ? 'Kunden zahlen ~50.000 EUR/Jahr und sparen >50.000 EUR (Pentests + CE-Beurteilungen + Auditmanager). ROI ab Tag 1.' : 'Customers pay ~EUR 50,000/year and save >EUR 50,000 (pentests + CE assessments + audit managers). ROI from day 1.'} -

+
+

+ {de + ? '💡 Kunden zahlen ~50.000 EUR/Jahr und sparen >50.000 EUR (Pentests + CE-Beurteilungen + Auditmanager). ROI ab Tag 1.' + : '💡 Customers pay ~EUR 50,000/year and save >EUR 50,000 (pentests + CE assessments + audit managers). ROI from day 1.'} +

+
) @@ -166,15 +161,30 @@ export function PrintProductPage({ products, lang, pageNum, totalPages, versionN export function PrintMarketPage({ market, lang, pageNum, totalPages, versionName }: SlideBase & { market: PitchMarket[] }) { const de = lang === 'de' - const headers = [de ? 'Segment' : 'Segment', de ? 'Markt' : 'Label', de ? 'Volumen' : 'Volume', de ? 'Wachstum p.a.' : 'Growth p.a.', de ? 'Quelle' : 'Source'] - const rows = market.map(m => [m.market_segment.toUpperCase(), m.label, fmtEur(m.value_eur), `${m.growth_rate_pct}%`, m.source]) + const headers = [de ? 'Segment' : 'Segment', de ? 'Marktbeschreibung' : 'Description', de ? 'Volumen' : 'Volume', de ? 'Wachstum p.a.' : 'Growth p.a.', de ? 'Quelle' : 'Source'] + const rows = market.map(m => [ + {m.market_segment.toUpperCase()}, + m.label, + fmtEur(m.value_eur), + `${m.growth_rate_pct}%`, + m.source, + ]) return ( {de ? 'Marktchance' : 'Market Opportunity'} -
- +
+ +
+ {market.map(m => ( +
+

{m.market_segment}

+

{fmtEur(m.value_eur)}

+

{m.growth_rate_pct}% p.a.

+
+ ))} +
) @@ -187,22 +197,22 @@ export function PrintTeamPage({ team, lang, pageNum, totalPages, versionName }: {de ? 'Das Team' : 'The Team'} -
+
{team.slice(0, 4).map(m => ( -
-
+
+
-

{m.name}

-

{de ? m.role_de : m.role_en}

+

{m.name}

+

{de ? m.role_de : m.role_en}

{m.equity_pct > 0 && {m.equity_pct}%}
-

+

{de ? m.bio_de : m.bio_en}

{m.expertise?.length > 0 && ( -
- {m.expertise.slice(0, 4).map(e => {e})} +
+ {m.expertise.slice(0, 5).map(e => {e})}
)}
@@ -217,13 +227,15 @@ export function PrintMilestonesPage({ milestones, lang, pageNum, totalPages, ver const ordered = [...milestones].sort((a, b) => { const order = { completed: 0, in_progress: 1, planned: 2 } return (order[a.status] - order[b.status]) || new Date(a.milestone_date).getTime() - new Date(b.milestone_date).getTime() - }).slice(0, 12) - const statusColor = (s: string) => ({ completed: '#16a34a', in_progress: '#d97706', planned: '#94a3b8' })[s] || '#94a3b8' - const statusLabel = (s: string) => de ? ({ completed: 'Abgeschlossen', in_progress: 'In Arbeit', planned: 'Geplant' }[s] || s) : ({ completed: 'Completed', in_progress: 'In Progress', planned: 'Planned' }[s] || s) + }).slice(0, 10) + const statusColor = (s: string) => ({ completed: '#16a34a', in_progress: '#d97706', planned: '#94a3b8' }[s] || '#94a3b8') + const statusLabel = (s: string) => de + ? ({ completed: 'Abgeschlossen', in_progress: 'In Arbeit', planned: 'Geplant' }[s] || s) + : ({ completed: 'Completed', in_progress: 'In Progress', planned: 'Planned' }[s] || s) const fmtDate = (d: string) => new Date(d).toLocaleDateString(de ? 'de-DE' : 'en-GB', { month: 'short', year: 'numeric' }) const rows = ordered.map(m => [ fmtDate(m.milestone_date), - de ? m.title_de : m.title_en, + {de ? m.title_de : m.title_en}, m.category, {statusLabel(m.status)}, ]) @@ -232,8 +244,8 @@ export function PrintMilestonesPage({ milestones, lang, pageNum, totalPages, ver {de ? 'Meilensteine' : 'Milestones'} -
- +
+
) @@ -245,40 +257,38 @@ export function PrintTheAskPage({ funding, lang, pageNum, totalPages, versionNam const isWD = (funding?.instrument || '').toLowerCase() === 'wandeldarlehen' const targetDate = funding?.target_date ? (() => { const d = new Date(funding.target_date); return `Q${Math.ceil((d.getMonth()+1)/3)} ${d.getFullYear()}` })() : 'TBD' const uof = funding?.use_of_funds || [] + const amountLabel = amount >= 1_000_000 ? `${(amount / 1_000_000).toFixed(1)} Mio. EUR` : amount >= 1_000 ? `${Math.round(amount / 1_000)}k EUR` : `${amount} EUR` return ( - + The Ask -
-
-

- {amount >= 1_000_000 ? `${(amount / 1_000_000).toFixed(1)} Mio.` : amount >= 1_000 ? `${Math.round(amount / 1_000)}k` : amount.toString()} - EUR -

-
- {[ +
+
+

{amountLabel}

+
+ {([ [de ? 'Instrument' : 'Instrument', isWD ? 'Wandeldarlehen' : 'Equity'], [de ? 'Runde' : 'Round', funding?.round_name || 'Pre-Seed'], [de ? 'Zieldatum' : 'Target', targetDate], - ].map(([k, v]) => ( -
- {k} - {v} + ] as [string, string][]).map(([k, v]) => ( +
+ {k} + {v}
))}
-
-

+

+

{de ? 'Mittelverwendung' : 'Use of Funds'}

{uof.map(u => ( -
-
- {de ? u.label_de : u.label_en} - {u.percentage}% +
+
+ {de ? u.label_de : u.label_en} + {u.percentage}%
-
-
+
+
))} diff --git a/pitch-deck/app/pitch-print/[versionId]/_components/PrintDeck.tsx b/pitch-deck/app/pitch-print/[versionId]/_components/PrintDeck.tsx index aa4563a..70b65f0 100644 --- a/pitch-deck/app/pitch-print/[versionId]/_components/PrintDeck.tsx +++ b/pitch-deck/app/pitch-print/[versionId]/_components/PrintDeck.tsx @@ -14,12 +14,10 @@ interface PrintDeckProps { lang: Language } -const CORE_PAGES = 9 -const FINANCIAL_EXTRA = 4 - export default function PrintDeck({ pitchData, versionName, fmResults, fmAssumptions, financial, lang }: PrintDeckProps) { const isWandeldarlehen = (pitchData.funding?.instrument || '').toLowerCase() === 'wandeldarlehen' - const totalPages = financial ? CORE_PAGES + FINANCIAL_EXTRA - (isWandeldarlehen ? 1 : 0) : CORE_PAGES + const hasCapTable = financial && !isWandeldarlehen + const totalPages = financial ? (hasCapTable ? 13 : 12) : 9 const annualRows = aggregateAnnualRows(fmResults) const de = lang === 'de' @@ -28,11 +26,7 @@ export default function PrintDeck({ pitchData, versionName, fmResults, fmAssumpt return () => clearTimeout(t) }, []) - function p(n: number) { - return { lang, pageNum: n, totalPages, versionName } - } - - let financialPage = CORE_PAGES + 1 + function p(n: number) { return { lang, pageNum: n, totalPages, versionName } } return ( <> @@ -41,99 +35,79 @@ export default function PrintDeck({ pitchData, versionName, fmResults, fmAssumpt @page { size: A4 landscape; margin: 0; } body { margin: 0; -webkit-print-color-adjust: exact; print-color-adjust: exact; } .no-print { display: none !important; } + .print-page { + page-break-after: always !important; + break-after: page !important; + page-break-inside: avoid !important; + break-inside: avoid !important; + margin: 0 !important; + box-shadow: none !important; + width: 297mm !important; + height: 210mm !important; + } + .print-page-cover { + page-break-after: always !important; + break-after: page !important; + margin: 0 !important; + box-shadow: none !important; + } + .print-deck-wrapper { padding: 0 !important; } } @media screen { - body { background: #e5e7eb; } + body { background: #d1d5db; } } * { box-sizing: border-box; } `} - {/* Toolbar — hidden at print */} + {/* Toolbar — screen only */}
BreakPilot {versionName} - {financial ? (de ? 'PDF + Finanzen' : 'PDF + Financial') : (de ? 'Standard PDF' : 'Standard PDF')} + {financial ? (de ? 'PDF + Finanzen' : 'PDF + Financial') : 'Standard PDF'} {totalPages} {de ? 'Seiten' : 'pages'}
- -
- {/* Print pages */} -
- {/* Page 1: Cover */} +
- - {/* Page 2: Problem */} - - {/* Page 3: Solution */} - - {/* Page 4: Products */} - {pitchData.products?.length > 0 && ( - - )} - - {/* Page 5: Market */} - {pitchData.market?.length > 0 && ( - - )} - - {/* Page 6: Team */} - {pitchData.team?.length > 0 && ( - - )} - - {/* Page 7: Milestones */} - {pitchData.milestones?.length > 0 && ( - - )} - - {/* Page 8-9: The Ask (always last of core) */} + + + + - {/* Financial annex */} + {/* Page 9: standard last page OR financial annex start */} + {!financial && } + {financial && ( <> - {annualRows.length > 0 && ( - - )} - {fmAssumptions.length > 0 && ( - - )} - {!isWandeldarlehen && ( - - )} - + {annualRows.length > 0 + ? + : + } + + {hasCapTable && } + )}
diff --git a/pitch-deck/app/pitch-print/[versionId]/_components/PrintLayout.tsx b/pitch-deck/app/pitch-print/[versionId]/_components/PrintLayout.tsx index b02815e..69a17d1 100644 --- a/pitch-deck/app/pitch-print/[versionId]/_components/PrintLayout.tsx +++ b/pitch-deck/app/pitch-print/[versionId]/_components/PrintLayout.tsx @@ -13,91 +13,81 @@ interface PrintPageProps { totalPages: number versionName: string children: React.ReactNode - noPadding?: boolean } -export function PrintPage({ title, pageNum, totalPages, versionName, children, noPadding }: PrintPageProps) { +export function PrintPage({ title, pageNum, totalPages, versionName, children }: PrintPageProps) { return ( -
{/* Header bar */}
- - BreakPilot - - {title} + BreakPilot + {title}
- {/* Content area */} -
+ {/* Content area — must stretch to fill all remaining height */} +
{children}
{/* Footer bar */}
- {versionName} - - CONFIDENTIAL - - {pageNum} / {totalPages} + {versionName} + CONFIDENTIAL + {pageNum} / {totalPages}
) } -interface SectionTitleProps { - children: React.ReactNode - subtitle?: string -} +interface SectionTitleProps { children: React.ReactNode; subtitle?: string } export function SectionTitle({ children, subtitle }: SectionTitleProps) { return ( -
-

+
+

{children}

{subtitle && ( -

+

{subtitle}

)} @@ -113,7 +103,7 @@ interface TableProps { export function PrintTable({ headers, rows, colWidths }: TableProps) { return ( - +
{headers.map((h, i) => ( @@ -123,8 +113,8 @@ export function PrintTable({ headers, rows, colWidths }: TableProps) { fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.04em', - fontSize: '8px', - padding: '5px 8px', + fontSize: '9px', + padding: '6px 9px', textAlign: 'left', width: colWidths?.[i], WebkitPrintColorAdjust: 'exact', @@ -139,13 +129,7 @@ export function PrintTable({ headers, rows, colWidths }: TableProps) { {rows.map((row, ri) => ( {row.map((cell, ci) => ( - ))} @@ -158,15 +142,7 @@ export function PrintTable({ headers, rows, colWidths }: TableProps) { export function Badge({ children, color = INDIGO }: { children: React.ReactNode; color?: string }) { return ( - + {children} )
+ {cell}