fix(pitch-print): page count, Finanzplan loading, visual energy
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
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>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Language } from '@/lib/types'
|
||||
import { Page, COLORS, Callout, DataTable, ThreeCol, Bullets } from './PrintLayout'
|
||||
import { ArchitectureDiagram, PipelineFlow } from './PrintDiagrams'
|
||||
|
||||
interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string }
|
||||
|
||||
@@ -158,68 +159,31 @@ export function PrintRegulatoryPage({ lang, pageNum, totalPages, versionName }:
|
||||
export function PrintArchitecturePage({ lang, pageNum, totalPages, versionName }: SlideBase) {
|
||||
const de = lang === 'de'
|
||||
return (
|
||||
<Page kicker="20" section={de ? 'ANHANG · SYSTEMARCHITEKTUR' : 'APPENDIX · SYSTEM ARCHITECTURE'} title={de ? 'Drei Produkt-Domänen. Eine LiteLLM-Gateway. Lokale Inferenz.' : 'Three product domains. One LiteLLM gateway. Local inference.'} subtitle={de ? 'BreakPilot · CERTifAI · Compliance Scanner, über LiteLLM-Proxy mit lokalen LLM-Inferenz-Knoten verbunden. 100% EU, kein US-Anbieter.' : 'BreakPilot · CERTifAI · Compliance Scanner, connected via LiteLLM proxy to local LLM inference nodes. 100% EU, no US providers.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
|
||||
|
||||
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
{/* PRODUCT TIER */}
|
||||
<div style={{ marginBottom: '4mm' }}>
|
||||
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: '2mm' }}>{de ? 'Produkt-Schicht' : 'Product Tier'}</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm' }}>
|
||||
{[
|
||||
{ t: 'CERTifAI', s: de ? 'GenAI Mandantenportal' : 'GenAI Tenant Portal', tech: 'Rust · Dioxus · MongoDB · Keycloak · SearXNG · LangGraph', services: ['LiteLLM Dashboard', 'LibreChat + SSO', 'LangGraph Agents', 'MCP Hub'] },
|
||||
{ t: 'COMPLAI', s: de ? 'Compliance & Audit' : 'Compliance & Audit', tech: 'Next.js 15 · FastAPI · Go/Gin · PostgreSQL · Qdrant · Valkey', services: [de ? 'DSGVO/AI Act/NIS2 (25k+ Controls)' : 'GDPR/AI Act/NIS2 (25k+ controls)', de ? 'RAG (380+ Quellen)' : 'RAG (380+ sources)', de ? 'Control Pipeline (LLM)' : 'Control pipeline (LLM)', 'MCP Client'] },
|
||||
{ t: 'Compliance Scanner', s: de ? 'Code-Sicherheit' : 'Code Security', tech: 'Rust · Axum · MongoDB · Semgrep · Gitleaks · Syft', services: ['SAST · SBOM · CVE Pipeline', de ? 'KI-Triage (False Positives)' : 'AI Triage (false positives)', de ? 'KI-Pentest (autonom)' : 'AI Pentest (autonomous)', 'MCP Server'] },
|
||||
].map((p, i) => (
|
||||
<div key={i} style={{ border: `1px solid ${COLORS.slate200}`, borderTop: `2px solid ${COLORS.indigo600}`, padding: '3mm 4mm' }}>
|
||||
<div style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900, marginBottom: '0.5mm' }}>{p.t}</div>
|
||||
<div style={{ fontSize: '8pt', color: COLORS.indigo600, marginBottom: '2mm', fontWeight: 600 }}>{p.s}</div>
|
||||
<div style={{ fontSize: '7pt', color: COLORS.slate600, fontFamily: 'monospace', marginBottom: '2mm', lineHeight: 1.4 }}>{p.tech}</div>
|
||||
<div>
|
||||
{p.services.map((s, j) => (
|
||||
<div key={j} style={{ fontSize: '7.5pt', color: COLORS.slate700, padding: '0.8mm 0', borderTop: j > 0 ? `1px solid ${COLORS.slate100}` : 'none' }}>· {s}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PROXY TIER */}
|
||||
<div style={{ marginBottom: '4mm' }}>
|
||||
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: '2mm' }}>{de ? 'Gateway-Schicht (KI-Proxy mit Guardrails)' : 'Gateway Tier (AI proxy with guardrails)'}</div>
|
||||
<div style={{ border: `1px solid ${COLORS.amber600}`, borderTop: `2px solid ${COLORS.amber600}`, padding: '3mm 4mm', background: COLORS.amber50, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: '4mm', marginBottom: '2mm' }}>
|
||||
<span style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900 }}>LiteLLM Proxy</span>
|
||||
<span style={{ fontSize: '8pt', color: COLORS.amber700, fontWeight: 600 }}>{de ? 'KI-Gateway · Bearer-Auth · Rate-Limiting · PII-Filter · Spend-Tracking' : 'AI gateway · bearer auth · rate limiting · PII filter · spend tracking'}</span>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '3mm', fontSize: '7.5pt', color: COLORS.slate700 }}>
|
||||
<div>· {de ? 'Token-Budget pro Mandant' : 'Per-tenant token budget'}</div>
|
||||
<div>· {de ? 'PII-Guardrails alle Anfragen' : 'PII guardrails on all requests'}</div>
|
||||
<div>· {de ? 'Anonyme EU-Web-Suche (SearXNG)' : 'Anonymous EU web search (SearXNG)'}</div>
|
||||
<div>· {de ? 'Namespace-Isolierung pro API-Key' : 'Namespace isolation per API key'}</div>
|
||||
<div>· {de ? 'Failover-Routing zwischen Modellen' : 'Failover routing between models'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* INFERENCE TIER */}
|
||||
<div>
|
||||
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: '2mm' }}>{de ? 'Inferenz-Schicht (lokal, air-gap-fähig)' : 'Inference Tier (local, air-gap capable)'}</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm' }}>
|
||||
{[
|
||||
{ t: de ? 'LLM Inferenz' : 'LLM Inference', tech: 'Qwen3-32B · Qwen3-Coder-30B · DeepSeek-R1-8B · Ollama', desc: de ? 'Vollständig lokal, air-gap fähig, GPU-optimiert. Daten verlassen nie den Server.' : 'Fully local, air-gap capable, GPU-optimized. Data never leaves the server.' },
|
||||
{ t: 'Embeddings', tech: 'bge-m3 · Qdrant Vector DB · Sentence-Transformers', desc: de ? 'RAG mit 380+ Rechtsquellen indexiert, multilinguale Einbettungen, lokal.' : 'RAG with 380+ legal sources indexed, multilingual embeddings, local.' },
|
||||
{ t: de ? 'KI-Tools' : 'AI Tools', tech: 'SearXNG · MCP Protocol · Semgrep API · Gitleaks API', desc: de ? 'Anonymisierte EU-Web-Suche, MCP-Integration für Audit-Dokumente und Code-Findings.' : 'Anonymized EU web search, MCP integration for audit docs and code findings.' },
|
||||
].map((p, i) => (
|
||||
<div key={i} style={{ border: `1px solid ${COLORS.slate200}`, padding: '3mm 4mm' }}>
|
||||
<div style={{ fontSize: '10pt', fontWeight: 700, color: COLORS.slate900, marginBottom: '1mm' }}>{p.t}</div>
|
||||
<div style={{ fontSize: '7pt', color: COLORS.slate500, fontFamily: 'monospace', marginBottom: '2mm', lineHeight: 1.4 }}>{p.tech}</div>
|
||||
<div style={{ fontSize: '8pt', color: COLORS.slate700, lineHeight: 1.45 }}>{p.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Page kicker="20" section={de ? 'ANHANG · SYSTEMARCHITEKTUR' : 'APPENDIX · SYSTEM ARCHITECTURE'} title={de ? 'Drei Produkt-Domänen. Ein LiteLLM-Gateway. Lokale Inferenz.' : 'Three product domains. One LiteLLM gateway. Local inference.'} subtitle={de ? 'BreakPilot · CERTifAI · Compliance Scanner, über LiteLLM-Proxy mit lokalen Inferenz-Knoten verbunden. 100% EU, kein US-Anbieter.' : 'BreakPilot · CERTifAI · Compliance Scanner, connected via LiteLLM proxy to local inference nodes. 100% EU, no US providers.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
|
||||
<ArchitectureDiagram
|
||||
lang={lang}
|
||||
product={[
|
||||
{ kicker: 'GENAI', title: 'CERTifAI', subtitle: de ? 'GenAI Mandantenportal' : 'GenAI Tenant Portal', tech: 'Rust · Dioxus · MongoDB · Keycloak · SearXNG · LangGraph', services: ['LiteLLM Dashboard', 'LibreChat + SSO', 'LangGraph Agents', 'MCP Hub'] },
|
||||
{ kicker: 'COMPLIANCE', title: 'COMPLAI', subtitle: de ? 'Compliance & Audit' : 'Compliance & Audit', tech: 'Next.js 15 · FastAPI · Go/Gin · PostgreSQL · Qdrant · Valkey', services: [de ? 'DSGVO/AI Act/NIS2 (25k+ Controls)' : 'GDPR/AI Act/NIS2 (25k+ controls)', de ? 'RAG (380+ Quellen)' : 'RAG (380+ sources)', de ? 'Control Pipeline (LLM)' : 'Control pipeline (LLM)', 'MCP Client'] },
|
||||
{ kicker: 'SECURITY', title: 'Compliance Scanner', subtitle: de ? 'Code-Sicherheit' : 'Code Security', tech: 'Rust · Axum · MongoDB · Semgrep · Gitleaks · Syft', services: ['SAST · SBOM · CVE Pipeline', de ? 'KI-Triage (False Positives)' : 'AI Triage (false positives)', de ? 'KI-Pentest (autonom)' : 'AI Pentest (autonomous)', 'MCP Server'] },
|
||||
]}
|
||||
proxy={{
|
||||
title: 'LiteLLM Proxy',
|
||||
subtitle: de ? 'KI-Gateway · Bearer-Auth · Rate-Limiting · PII-Filter · Spend-Tracking' : 'AI gateway · bearer auth · rate limiting · PII filter · spend tracking',
|
||||
features: [
|
||||
de ? 'Token-Budget pro Mandant' : 'Per-tenant token budget',
|
||||
de ? 'PII-Guardrails alle Anfragen' : 'PII guardrails on all requests',
|
||||
de ? 'Anonyme EU-Web-Suche (SearXNG)' : 'Anonymous EU web search (SearXNG)',
|
||||
de ? 'Namespace-Isolierung pro API-Key' : 'Namespace isolation per API key',
|
||||
de ? 'Failover-Routing zwischen Modellen' : 'Failover routing between models',
|
||||
],
|
||||
}}
|
||||
inference={[
|
||||
{ title: de ? 'LLM Inferenz' : 'LLM Inference', subtitle: de ? 'Lokale Sprachmodelle' : 'Local language models', tech: 'Qwen3-32B · Qwen3-Coder-30B · DeepSeek-R1-8B · Ollama', desc: de ? 'Vollständig lokal, air-gap fähig, GPU-optimiert. Daten verlassen nie den Server.' : 'Fully local, air-gap capable, GPU-optimized. Data never leaves the server.' },
|
||||
{ title: 'Embeddings', subtitle: de ? 'Semantische Suche' : 'Semantic search', tech: 'bge-m3 · Qdrant Vector DB · Sentence-Transformers', desc: de ? 'RAG mit 380+ Rechtsquellen indexiert, multilinguale Einbettungen, lokal.' : 'RAG with 380+ legal sources indexed, multilingual embeddings, local.' },
|
||||
{ title: de ? 'KI-Tools' : 'AI Tools', subtitle: de ? 'Web-Suche & MCP' : 'Web search & MCP', tech: 'SearXNG · MCP Protocol · Semgrep API · Gitleaks API', desc: de ? 'Anonymisierte EU-Web-Suche, MCP-Integration für Audit-Dokumente und Code-Findings.' : 'Anonymized EU web search, MCP integration for audit docs and code findings.' },
|
||||
]}
|
||||
/>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -318,23 +282,14 @@ export function PrintAIPipelinePage({ lang, pageNum, totalPages, versionName }:
|
||||
{/* Pipeline flow */}
|
||||
<div>
|
||||
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: '2mm' }}>{de ? 'Pipeline-Fluss' : 'Pipeline flow'}</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '4mm' }}>
|
||||
{[
|
||||
<PipelineFlow
|
||||
stages={[
|
||||
{ n: '01', t: 'Ingestion', d: de ? 'Rohdokumente (PDF, HTML, XML, Word) → Pass 0a OCR → Pass 0b Strukturierung. Markdown + Metadaten.' : 'Raw docs (PDF, HTML, XML, Word) → Pass 0a OCR → Pass 0b structuring. Markdown + metadata.', kpi: '385 docs' },
|
||||
{ n: '02', t: 'Chunking + Embed', d: de ? 'Semantisches Chunking, Sentence-Transformers (bge-m3), Qdrant Vector DB, Multi-Index.' : 'Semantic chunking, sentence-transformers (bge-m3), Qdrant vector DB, multi-index.', kpi: '~280k chunks' },
|
||||
{ n: '03', t: 'Control Extraction', d: de ? 'BatchDedup → LLM-Extraktor (Qwen3-32B) → Strukturierte Controls mit Quellenangabe + Confidence.' : 'BatchDedup → LLM extractor (Qwen3-32B) → structured controls with source + confidence.', kpi: '25k+ controls' },
|
||||
{ n: '04', t: 'Quality Assurance', d: de ? 'BQAS (Batch Quality Assessment System): Cross-Validation, Inkonsistenz-Detection, Audit-Sampling.' : 'BQAS (Batch Quality Assessment System): cross-validation, inconsistency detection, audit sampling.', kpi: '>99% precision' },
|
||||
].map((s, i) => (
|
||||
<div key={i} style={{ borderLeft: `2px solid ${COLORS.indigo600}`, paddingLeft: '4mm', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}>
|
||||
<span style={{ fontSize: '14pt', fontWeight: 800, color: COLORS.indigo600, lineHeight: 1, fontVariantNumeric: 'tabular-nums' }}>{s.n}</span>
|
||||
<span style={{ fontSize: '7.5pt', color: COLORS.emerald700, fontWeight: 700, fontVariantNumeric: 'tabular-nums' }}>{s.kpi}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900, marginBottom: '2mm', lineHeight: 1.2 }}>{s.t}</div>
|
||||
<div style={{ fontSize: '8pt', color: COLORS.slate700, lineHeight: 1.5 }}>{s.d}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Agent system */}
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import React from 'react'
|
||||
import { COLORS } from './PrintLayout'
|
||||
|
||||
/* ====================================================================== */
|
||||
/* CHARTS */
|
||||
/* ====================================================================== */
|
||||
|
||||
interface BarSeries {
|
||||
label: string
|
||||
value: number
|
||||
/** Optional secondary label rendered above the bar value (e.g. "Mio."). */
|
||||
unit?: string
|
||||
tone?: 'default' | 'positive' | 'negative' | 'accent'
|
||||
}
|
||||
|
||||
export function BarChart({
|
||||
data, height = 36, maxOverride, formatValue, title, yAxisHint,
|
||||
}: {
|
||||
data: BarSeries[]
|
||||
height?: number // mm
|
||||
maxOverride?: number
|
||||
formatValue?: (n: number) => string
|
||||
title?: string
|
||||
yAxisHint?: string
|
||||
}) {
|
||||
const max = maxOverride ?? Math.max(...data.map(d => d.value), 1)
|
||||
const fmt = formatValue ?? ((n: number) => n.toLocaleString('de-DE'))
|
||||
const ticks = [0, 0.25, 0.5, 0.75, 1].map(t => Math.round(max * t))
|
||||
|
||||
return (
|
||||
<div>
|
||||
{(title || yAxisHint) && (
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}>
|
||||
{title && <div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate600, textTransform: 'uppercase', letterSpacing: '0.1em' }}>{title}</div>}
|
||||
{yAxisHint && <div style={{ fontSize: '7pt', color: COLORS.slate400 }}>{yAxisHint}</div>}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', alignItems: 'stretch', gap: '4mm', position: 'relative' }}>
|
||||
{/* Y-axis ticks */}
|
||||
<div style={{ width: '14mm', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', height: `${height}mm`, fontSize: '6.5pt', color: COLORS.slate400, textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>
|
||||
{ticks.slice().reverse().map((t, i) => (
|
||||
<div key={i}>{fmt(t)}</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Bars + grid */}
|
||||
<div style={{ flex: 1, position: 'relative', height: `${height}mm`, borderLeft: `1px solid ${COLORS.slate300}`, borderBottom: `1px solid ${COLORS.slate300}` }}>
|
||||
{/* Grid lines */}
|
||||
{ticks.slice(1).map((_, i) => (
|
||||
<div key={i} style={{ position: 'absolute', left: 0, right: 0, top: `${(1 - (i + 1) / 4) * 100}%`, height: '0.5pt', background: COLORS.slate100, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
|
||||
))}
|
||||
{/* Bars */}
|
||||
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'flex-end', gap: '3mm', padding: '0 2mm' }}>
|
||||
{data.map((d, i) => {
|
||||
const h = (d.value / max) * 100
|
||||
const color = d.tone === 'positive' ? COLORS.emerald600
|
||||
: d.tone === 'negative' ? COLORS.red600
|
||||
: d.tone === 'accent' ? COLORS.amber600
|
||||
: COLORS.indigo600
|
||||
return (
|
||||
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', height: '100%', position: 'relative' }}>
|
||||
<div style={{ fontSize: '7pt', fontWeight: 700, color: color, marginBottom: '0.8mm', fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' }}>{fmt(d.value)}</div>
|
||||
<div style={{ width: '100%', height: `${h}%`, minHeight: '1pt', background: color, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* X-axis labels */}
|
||||
<div style={{ display: 'flex', gap: '3mm', paddingLeft: '18mm', paddingRight: '2mm', marginTop: '1mm' }}>
|
||||
{data.map((d, i) => (
|
||||
<div key={i} style={{ flex: 1, fontSize: '7pt', color: COLORS.slate500, textAlign: 'center', fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>{d.label}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface LinePoint { label: string; value: number }
|
||||
|
||||
export function LineChart({
|
||||
data, height = 36, formatValue, color = COLORS.indigo600, title, fill = true,
|
||||
}: {
|
||||
data: LinePoint[]
|
||||
height?: number
|
||||
formatValue?: (n: number) => string
|
||||
color?: string
|
||||
title?: string
|
||||
fill?: boolean
|
||||
}) {
|
||||
if (data.length < 2) return null
|
||||
const max = Math.max(...data.map(d => d.value), 1)
|
||||
const fmt = formatValue ?? ((n: number) => n.toLocaleString('de-DE'))
|
||||
const w = 100
|
||||
const points = data.map((d, i) => ({
|
||||
x: (i / (data.length - 1)) * w,
|
||||
y: 100 - (d.value / max) * 100,
|
||||
v: d.value,
|
||||
label: d.label,
|
||||
}))
|
||||
const pathD = points.map((p, i) => (i === 0 ? `M${p.x},${p.y}` : `L${p.x},${p.y}`)).join(' ')
|
||||
const areaD = `${pathD} L100,100 L0,100 Z`
|
||||
|
||||
return (
|
||||
<div>
|
||||
{title && <div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate600, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '2mm' }}>{title}</div>}
|
||||
<div style={{ position: 'relative', height: `${height}mm`, borderLeft: `1px solid ${COLORS.slate300}`, borderBottom: `1px solid ${COLORS.slate300}` }}>
|
||||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}>
|
||||
{/* Grid */}
|
||||
{[0.25, 0.5, 0.75].map(t => (
|
||||
<line key={t} x1="0" x2="100" y1={t * 100} y2={t * 100} stroke={COLORS.slate100} strokeWidth="0.3" />
|
||||
))}
|
||||
{fill && <path d={areaD} fill={color} fillOpacity="0.12" />}
|
||||
<path d={pathD} fill="none" stroke={color} strokeWidth="0.6" strokeLinejoin="round" strokeLinecap="round" />
|
||||
{points.map((p, i) => (
|
||||
<circle key={i} cx={p.x} cy={p.y} r="1.2" fill={color} />
|
||||
))}
|
||||
</svg>
|
||||
{/* Value labels above each point */}
|
||||
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
|
||||
{points.map((p, i) => (
|
||||
<div key={i} style={{ position: 'absolute', left: `${p.x}%`, top: `${p.y}%`, transform: 'translate(-50%, -120%)', fontSize: '6.5pt', fontWeight: 700, color, whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' }}>{fmt(p.v)}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* X labels */}
|
||||
<div style={{ display: 'flex', marginTop: '1mm' }}>
|
||||
{points.map((p, i) => (
|
||||
<div key={i} style={{ flex: 1, fontSize: '7pt', color: COLORS.slate500, textAlign: 'center', fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>{p.label}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Horizontal stacked-bar comparison (e.g. "you pay" vs "you save") */
|
||||
export function ComparisonBars({
|
||||
rows, formatValue,
|
||||
}: {
|
||||
rows: { label: string; bars: { tone: 'positive' | 'negative' | 'accent' | 'default'; value: number; cap?: string }[] }[]
|
||||
formatValue?: (n: number) => string
|
||||
}) {
|
||||
const max = Math.max(...rows.flatMap(r => r.bars.map(b => b.value)), 1)
|
||||
const fmt = formatValue ?? ((n: number) => n.toLocaleString('de-DE'))
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '3mm' }}>
|
||||
{rows.map((row, i) => (
|
||||
<div key={i}>
|
||||
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate700, marginBottom: '1.5mm' }}>{row.label}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1mm' }}>
|
||||
{row.bars.map((b, j) => {
|
||||
const w = (b.value / max) * 100
|
||||
const color = b.tone === 'positive' ? COLORS.emerald600
|
||||
: b.tone === 'negative' ? COLORS.red600
|
||||
: b.tone === 'accent' ? COLORS.amber600
|
||||
: COLORS.indigo600
|
||||
return (
|
||||
<div key={j} style={{ display: 'flex', alignItems: 'center', gap: '3mm' }}>
|
||||
<div style={{ flex: 1, position: 'relative', height: '4mm', background: COLORS.slate50, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
<div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: `${w}%`, background: color, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
|
||||
{b.cap && <div style={{ position: 'absolute', left: '2mm', top: 0, bottom: 0, display: 'flex', alignItems: 'center', fontSize: '7pt', color: '#ffffff', fontWeight: 700, letterSpacing: '0.04em' }}>{b.cap}</div>}
|
||||
</div>
|
||||
<div style={{ width: '20mm', textAlign: 'right', fontSize: '9pt', fontWeight: 800, color, fontVariantNumeric: 'tabular-nums' }}>{fmt(b.value)}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Donut chart for percentages (use-of-funds, equity, etc.) */
|
||||
export function DonutChart({
|
||||
segments, size = 32, thickness = 6,
|
||||
}: {
|
||||
segments: { label: string; pct: number; color: string }[]
|
||||
size?: number // mm
|
||||
thickness?: number // mm
|
||||
}) {
|
||||
const R = 50
|
||||
const r = R - (thickness / size) * 50
|
||||
let acc = 0
|
||||
const arcs = segments.map(s => {
|
||||
const start = acc / 100 * Math.PI * 2 - Math.PI / 2
|
||||
acc += s.pct
|
||||
const end = acc / 100 * Math.PI * 2 - Math.PI / 2
|
||||
const x1 = 50 + R * Math.cos(start), y1 = 50 + R * Math.sin(start)
|
||||
const x2 = 50 + R * Math.cos(end), y2 = 50 + R * Math.sin(end)
|
||||
const x3 = 50 + r * Math.cos(end), y3 = 50 + r * Math.sin(end)
|
||||
const x4 = 50 + r * Math.cos(start), y4 = 50 + r * Math.sin(start)
|
||||
const large = s.pct > 50 ? 1 : 0
|
||||
const d = `M${x1},${y1} A${R},${R} 0 ${large} 1 ${x2},${y2} L${x3},${y3} A${r},${r} 0 ${large} 0 ${x4},${y4} Z`
|
||||
return { d, color: s.color, pct: s.pct, label: s.label }
|
||||
})
|
||||
return (
|
||||
<svg viewBox="0 0 100 100" style={{ width: `${size}mm`, height: `${size}mm`, display: 'block' }}>
|
||||
{arcs.map((a, i) => (
|
||||
<path key={i} d={a.d} fill={a.color} />
|
||||
))}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
/** Progress meter (0-100%) — horizontal */
|
||||
export function ProgressBar({ pct, color = COLORS.indigo600, label, value }: { pct: number; color?: string; label?: string; value?: string }) {
|
||||
return (
|
||||
<div>
|
||||
{(label || value) && (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '1mm', fontSize: '8pt' }}>
|
||||
{label && <span style={{ color: COLORS.slate700, fontWeight: 500 }}>{label}</span>}
|
||||
{value && <span style={{ color: COLORS.slate900, fontWeight: 700, fontVariantNumeric: 'tabular-nums' }}>{value}</span>}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ height: '3mm', background: COLORS.slate100, position: 'relative', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
<div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: `${Math.min(100, Math.max(0, pct))}%`, background: color, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Nested market-size visual (TAM/SAM/SOM) */
|
||||
export function MarketFunnel({
|
||||
tam, sam, som, fmt,
|
||||
}: {
|
||||
tam: { value: number; label: string; growth?: number; note?: string }
|
||||
sam: { value: number; label: string; growth?: number; note?: string }
|
||||
som: { value: number; label: string; growth?: number; note?: string }
|
||||
fmt: (v: number) => string
|
||||
}) {
|
||||
const samPct = sam.value / tam.value
|
||||
const somPct = som.value / tam.value
|
||||
return (
|
||||
<div>
|
||||
{/* TAM outer */}
|
||||
<div style={{ border: `1px solid ${COLORS.slate300}`, padding: '5mm', position: 'relative' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '1.5mm' }}>
|
||||
<span style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.14em' }}>TAM · {tam.label}</span>
|
||||
{tam.growth != null && <span style={{ fontSize: '7.5pt', color: COLORS.slate500, fontWeight: 600 }}>+{tam.growth}% p.a.</span>}
|
||||
</div>
|
||||
<div style={{ fontSize: '32pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 1, letterSpacing: '-0.025em', fontVariantNumeric: 'tabular-nums' }}>{fmt(tam.value)}</div>
|
||||
{tam.note && <div style={{ fontSize: '8pt', color: COLORS.slate600, marginTop: '2mm', maxWidth: '120mm' }}>{tam.note}</div>}
|
||||
|
||||
{/* SAM inner */}
|
||||
<div style={{ marginTop: '4mm', marginLeft: '10mm', border: `1px solid ${COLORS.indigo600}`, background: COLORS.indigo50, padding: '4mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '1.5mm' }}>
|
||||
<span style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.indigo700, textTransform: 'uppercase', letterSpacing: '0.14em' }}>SAM · {sam.label}</span>
|
||||
{sam.growth != null && <span style={{ fontSize: '7.5pt', color: COLORS.indigo700, fontWeight: 600 }}>+{sam.growth}% p.a. · {Math.round(samPct * 100)}% TAM</span>}
|
||||
</div>
|
||||
<div style={{ fontSize: '26pt', fontWeight: 800, color: COLORS.indigo700, lineHeight: 1, letterSpacing: '-0.02em', fontVariantNumeric: 'tabular-nums' }}>{fmt(sam.value)}</div>
|
||||
{sam.note && <div style={{ fontSize: '8pt', color: COLORS.slate700, marginTop: '2mm' }}>{sam.note}</div>}
|
||||
|
||||
{/* SOM inner-inner */}
|
||||
<div style={{ marginTop: '3mm', marginLeft: '8mm', border: `2px solid ${COLORS.emerald600}`, background: COLORS.emerald50, padding: '4mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '1.5mm' }}>
|
||||
<span style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.emerald700, textTransform: 'uppercase', letterSpacing: '0.14em' }}>SOM · {som.label}</span>
|
||||
{som.growth != null && <span style={{ fontSize: '7.5pt', color: COLORS.emerald700, fontWeight: 600 }}>+{som.growth}% p.a. · {(somPct * 100).toFixed(1)}% TAM</span>}
|
||||
</div>
|
||||
<div style={{ fontSize: '24pt', fontWeight: 800, color: COLORS.emerald700, lineHeight: 1, letterSpacing: '-0.02em', fontVariantNumeric: 'tabular-nums' }}>{fmt(som.value)}</div>
|
||||
{som.note && <div style={{ fontSize: '8pt', color: COLORS.slate700, marginTop: '2mm' }}>{som.note}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -46,12 +46,13 @@ export default function PrintDeck({ pitchData, versionName, fmResults, fmAssumpt
|
||||
const hasFinancialDetail = financial && annualRows.length > 0
|
||||
const de = lang === 'de'
|
||||
|
||||
// Base standard PDF: 28 pages (25 slides, 3 of them 2-page: exec-summary, usp, competition; finanzplan annex is 2 pages)
|
||||
// 1 (exec1) + 1 (exec2) + 1 (cover) + 1 (problem) + 1 (solution) + 2 (usp) + 1 (regL) + 1 (product) +
|
||||
// 1 (how) + 1 (market) + 1 (bm) + 1 (traction) + 2 (competition) + 1 (team) + 1 (ask) + 1 (savings) +
|
||||
// 1 (strategy) + 2 (finanzplan) + 1 (assumptions) + 1 (regulatory) + 1 (architecture) + 1 (engineering) +
|
||||
// 1 (aipipeline) + 1 (risks) + 1 (glossary) + 1 (disclaimer) = 28
|
||||
const BASE_PAGES = 28
|
||||
// Base standard PDF: 29 physical pages.
|
||||
// 2 (exec) + 1 (cover) + 1 (problem) + 1 (solution) + 2 (usp) + 1 (regL) +
|
||||
// 1 (product) + 1 (how) + 1 (market) + 1 (bm) + 1 (milestones) + 2 (competition) +
|
||||
// 1 (team) + 1 (ask) + 1 (savings) + 1 (strategy) + 2 (finanzplan) +
|
||||
// 1 (assumptions) + 1 (regulatory) + 1 (architecture) + 1 (engineering) +
|
||||
// 1 (aipipeline) + 1 (risks) + 1 (glossary) + 1 (disclaimer) = 29
|
||||
const BASE_PAGES = 29
|
||||
const totalPages = BASE_PAGES + (hasFinancialDetail ? 1 : 0) + (hasCapTable ? 1 : 0)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
import React from 'react'
|
||||
import { COLORS } from './PrintLayout'
|
||||
|
||||
/* ====================================================================== */
|
||||
/* DIAGRAMS */
|
||||
/* ====================================================================== */
|
||||
|
||||
/** A single "node" in a flow / architecture diagram. */
|
||||
export function FlowNode({
|
||||
title, subtitle, items, accent = COLORS.indigo600, icon, kicker, footer,
|
||||
}: {
|
||||
title: string
|
||||
subtitle?: string
|
||||
items?: string[]
|
||||
accent?: string
|
||||
icon?: React.ReactNode
|
||||
kicker?: string
|
||||
footer?: string
|
||||
}) {
|
||||
return (
|
||||
<div style={{
|
||||
border: `1px solid ${COLORS.slate200}`,
|
||||
borderTop: `3px solid ${accent}`,
|
||||
background: '#ffffff',
|
||||
padding: '3mm 4mm',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
WebkitPrintColorAdjust: 'exact',
|
||||
printColorAdjust: 'exact',
|
||||
}}>
|
||||
{kicker && (
|
||||
<div style={{ fontSize: '6.5pt', fontWeight: 700, color: accent, textTransform: 'uppercase', letterSpacing: '0.14em', marginBottom: '1.5mm' }}>{kicker}</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '3mm', marginBottom: '1mm' }}>
|
||||
{icon && <div style={{ flexShrink: 0, color: accent, marginTop: '0.5mm' }}>{icon}</div>}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.15 }}>{title}</div>
|
||||
{subtitle && <div style={{ fontSize: '8pt', color: accent, fontWeight: 600, marginTop: '0.5mm' }}>{subtitle}</div>}
|
||||
</div>
|
||||
</div>
|
||||
{items && items.length > 0 && (
|
||||
<div style={{ marginTop: '2mm', display: 'flex', flexDirection: 'column' }}>
|
||||
{items.map((it, i) => (
|
||||
<div key={i} style={{
|
||||
fontSize: '7.5pt',
|
||||
color: COLORS.slate700,
|
||||
padding: '1mm 0',
|
||||
borderTop: i > 0 ? `1px solid ${COLORS.slate100}` : 'none',
|
||||
lineHeight: 1.35,
|
||||
}}>{it}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{footer && (
|
||||
<div style={{ marginTop: '2mm', paddingTop: '1.5mm', borderTop: `1px solid ${COLORS.slate100}`, fontSize: '6.5pt', color: COLORS.slate500, fontFamily: 'monospace', lineHeight: 1.4 }}>{footer}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Vertical down arrow (between rows of a flow diagram). */
|
||||
export function VArrow({ color = COLORS.slate400, label }: { color?: string; label?: string }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '1.5mm 0' }}>
|
||||
<svg viewBox="0 0 24 32" style={{ width: '6mm', height: '6mm' }}>
|
||||
<line x1="12" y1="0" x2="12" y2="24" stroke={color} strokeWidth="1.5" />
|
||||
<polyline points="6,22 12,30 18,22" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
{label && <div style={{ fontSize: '6.5pt', color: COLORS.slate500, marginTop: '0.5mm', fontWeight: 600 }}>{label}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Horizontal right arrow (between steps of a horizontal flow). */
|
||||
export function HArrow({ color = COLORS.slate400, label }: { color?: string; label?: string }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minWidth: '6mm', position: 'relative' }}>
|
||||
<svg viewBox="0 0 32 24" style={{ width: '8mm', height: '5mm' }}>
|
||||
<line x1="0" y1="12" x2="24" y2="12" stroke={color} strokeWidth="1.5" />
|
||||
<polyline points="20,6 30,12 20,18" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
{label && <div style={{ fontSize: '6.5pt', color: COLORS.slate500, marginTop: '0.5mm', fontWeight: 600, textAlign: 'center', whiteSpace: 'nowrap' }}>{label}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Horizontal step strip with built-in arrows between items. */
|
||||
export function StepStrip({
|
||||
steps, accent = COLORS.indigo600,
|
||||
}: {
|
||||
steps: { n: string; t: string; d: string }[]
|
||||
accent?: string
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'stretch' }}>
|
||||
{steps.map((s, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<div style={{ flex: 1, padding: '0 2mm', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: '3mm', marginBottom: '2mm' }}>
|
||||
<span style={{ fontSize: '24pt', fontWeight: 800, color: accent, lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>{s.n}</span>
|
||||
<div style={{ flex: 1, height: '1px', background: COLORS.slate200, alignSelf: 'center' }} />
|
||||
</div>
|
||||
<div style={{ fontSize: '12pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2, marginBottom: '2mm', letterSpacing: '-0.005em' }}>{s.t}</div>
|
||||
<div style={{ fontSize: '8.5pt', color: COLORS.slate700, lineHeight: 1.5, flex: 1 }}>{s.d}</div>
|
||||
</div>
|
||||
{i < steps.length - 1 && (
|
||||
<div style={{ width: '6mm', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<svg viewBox="0 0 24 24" style={{ width: '6mm', height: '6mm', color: COLORS.slate300 }}>
|
||||
<line x1="2" y1="12" x2="18" y2="12" stroke={COLORS.slate300} strokeWidth="1.5" />
|
||||
<polyline points="14,6 20,12 14,18" fill="none" stroke={COLORS.slate300} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 3-tier architecture diagram (product → proxy → inference) */
|
||||
export function ArchitectureDiagram({
|
||||
product, proxy, inference, lang,
|
||||
}: {
|
||||
product: { kicker: string; title: string; subtitle: string; tech: string; services: string[] }[]
|
||||
proxy: { title: string; subtitle: string; features: string[] }
|
||||
inference: { title: string; subtitle: string; tech: string; desc: string }[]
|
||||
lang: 'de' | 'en'
|
||||
}) {
|
||||
const de = lang === 'de'
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{/* PRODUCT TIER */}
|
||||
<div style={{ marginBottom: '2mm' }}>
|
||||
<div style={{ fontSize: '7pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.14em', marginBottom: '2mm' }}>{de ? 'Produkt-Schicht' : 'Product Tier'}</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm' }}>
|
||||
{product.map((p, i) => (
|
||||
<FlowNode key={i} kicker={p.kicker} title={p.title} subtitle={p.subtitle} items={p.services} accent={COLORS.indigo600} footer={p.tech} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Down arrows between product and proxy */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm', marginBottom: '1mm' }}>
|
||||
{[0, 1, 2].map(i => <VArrow key={i} color={COLORS.indigo500} />)}
|
||||
</div>
|
||||
|
||||
{/* PROXY */}
|
||||
<div style={{ marginBottom: '1mm' }}>
|
||||
<div style={{
|
||||
border: `1.5px solid ${COLORS.amber600}`,
|
||||
borderTop: `3px solid ${COLORS.amber600}`,
|
||||
background: COLORS.amber50,
|
||||
padding: '3mm 5mm',
|
||||
WebkitPrintColorAdjust: 'exact',
|
||||
printColorAdjust: 'exact',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}>
|
||||
<div>
|
||||
<span style={{ fontSize: '6.5pt', fontWeight: 700, color: COLORS.amber700, textTransform: 'uppercase', letterSpacing: '0.14em', marginRight: '3mm' }}>{de ? 'Gateway' : 'Gateway'}</span>
|
||||
<span style={{ fontSize: '12pt', fontWeight: 800, color: COLORS.slate900 }}>{proxy.title}</span>
|
||||
<span style={{ fontSize: '8.5pt', color: COLORS.amber700, marginLeft: '3mm', fontWeight: 600 }}>{proxy.subtitle}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '3mm', fontSize: '7.5pt', color: COLORS.slate700 }}>
|
||||
{proxy.features.map((f, i) => (
|
||||
<div key={i} style={{ paddingLeft: '2mm', borderLeft: `1px solid ${COLORS.amber600}`, lineHeight: 1.3 }}>{f}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Down arrows between proxy and inference */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm', marginBottom: '1mm' }}>
|
||||
{[0, 1, 2].map(i => <VArrow key={i} color={COLORS.amber600} />)}
|
||||
</div>
|
||||
|
||||
{/* INFERENCE TIER */}
|
||||
<div>
|
||||
<div style={{ fontSize: '7pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.14em', marginBottom: '2mm' }}>{de ? 'Inferenz-Schicht (lokal, air-gap-fähig)' : 'Inference Tier (local, air-gap capable)'}</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm' }}>
|
||||
{inference.map((p, i) => (
|
||||
<FlowNode key={i} title={p.title} subtitle={p.subtitle} items={[p.desc]} accent={COLORS.slate700} footer={p.tech} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Connected loop diagram for the USP "Compliance ↔ Code always in sync" closing card */
|
||||
export function LoopDiagram({ lang }: { lang: 'de' | 'en' }) {
|
||||
const de = lang === 'de'
|
||||
return (
|
||||
<svg viewBox="0 0 320 80" style={{ width: '100%', height: '24mm', display: 'block' }}>
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill={COLORS.indigo600} />
|
||||
</marker>
|
||||
</defs>
|
||||
{/* Compliance box */}
|
||||
<rect x="6" y="20" width="80" height="40" rx="2" fill={COLORS.indigo50} stroke={COLORS.indigo600} strokeWidth="1" />
|
||||
<text x="46" y="38" fontSize="9" fontWeight="700" textAnchor="middle" fill={COLORS.indigo700}>Compliance</text>
|
||||
<text x="46" y="52" fontSize="7" textAnchor="middle" fill={COLORS.slate600}>{de ? 'Policies · Audits · VVT' : 'Policies · Audits · RoPA'}</text>
|
||||
|
||||
{/* Code box */}
|
||||
<rect x="234" y="20" width="80" height="40" rx="2" fill={COLORS.indigo50} stroke={COLORS.indigo600} strokeWidth="1" />
|
||||
<text x="274" y="38" fontSize="9" fontWeight="700" textAnchor="middle" fill={COLORS.indigo700}>Code</text>
|
||||
<text x="274" y="52" fontSize="7" textAnchor="middle" fill={COLORS.slate600}>{de ? 'Repos · CI/CD · Findings' : 'Repos · CI/CD · Findings'}</text>
|
||||
|
||||
{/* Top arrow: compliance → code */}
|
||||
<path d="M86,30 Q160,5 234,30" fill="none" stroke={COLORS.indigo600} strokeWidth="1.4" markerEnd="url(#arrR)" />
|
||||
<text x="160" y="12" fontSize="7" fontWeight="600" textAnchor="middle" fill={COLORS.indigo700}>{de ? 'Policies → Code (Real-time)' : 'Policies → Code (Real-time)'}</text>
|
||||
|
||||
{/* Bottom arrow: code → compliance */}
|
||||
<path d="M234,50 Q160,75 86,50" fill="none" stroke={COLORS.indigo600} strokeWidth="1.4" markerEnd="url(#arrR)" />
|
||||
<text x="160" y="72" fontSize="7" fontWeight="600" textAnchor="middle" fill={COLORS.indigo700}>{de ? 'Code-Δ → Evidence (Auto)' : 'Code-Δ → Evidence (Auto)'}</text>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
/** 4-stage horizontal pipeline (e.g. RAG pipeline ingestion → ... → QA) */
|
||||
export function PipelineFlow({
|
||||
stages, accent = COLORS.indigo600,
|
||||
}: {
|
||||
stages: { n: string; t: string; d: string; kpi?: string }[]
|
||||
accent?: string
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'stretch' }}>
|
||||
{stages.map((s, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{
|
||||
border: `1px solid ${COLORS.slate200}`,
|
||||
borderTop: `3px solid ${accent}`,
|
||||
padding: '3mm 4mm',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
WebkitPrintColorAdjust: 'exact',
|
||||
printColorAdjust: 'exact',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}>
|
||||
<span style={{ fontSize: '18pt', fontWeight: 800, color: accent, lineHeight: 1, fontVariantNumeric: 'tabular-nums' }}>{s.n}</span>
|
||||
{s.kpi && <span style={{ fontSize: '7pt', color: COLORS.emerald700, fontWeight: 700, fontVariantNumeric: 'tabular-nums' }}>{s.kpi}</span>}
|
||||
</div>
|
||||
<div style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2, marginBottom: '2mm' }}>{s.t}</div>
|
||||
<div style={{ fontSize: '8pt', color: COLORS.slate700, lineHeight: 1.5, flex: 1 }}>{s.d}</div>
|
||||
</div>
|
||||
</div>
|
||||
{i < stages.length - 1 && (
|
||||
<div style={{ width: '7mm', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<svg viewBox="0 0 24 24" style={{ width: '6mm', height: '6mm' }}>
|
||||
<line x1="2" y1="12" x2="18" y2="12" stroke={accent} strokeWidth="1.5" />
|
||||
<polyline points="14,6 20,12 14,18" fill="none" stroke={accent} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Language, FMResult, FMAssumption } from '@/lib/types'
|
||||
import { Page, COLORS, Callout, DataTable } from './PrintLayout'
|
||||
import { BarChart, LineChart, DonutChart } from './PrintCharts'
|
||||
import { computeAnnualKPIs } from '@/lib/finanzplan/annual-kpis'
|
||||
|
||||
interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string }
|
||||
@@ -140,91 +141,64 @@ export function PrintFinanzplanPage2({ fmResults, lang, pageNum, totalPages, ver
|
||||
</Page>
|
||||
}
|
||||
|
||||
const maxRev = Math.max(...kpis.map(k => k.totalRevenue), 1)
|
||||
const maxEmp = Math.max(...kpis.map(k => k.employees), 1)
|
||||
const fmtM = (n: number) => '€' + (n / 1e6).toFixed(1) + 'M'
|
||||
const fmtK = (n: number) => '€' + (n / 1e3).toFixed(0) + 'k'
|
||||
const pickFmt = (max: number) => max >= 1e6 ? fmtM : fmtK
|
||||
|
||||
return (
|
||||
<Page kicker="17" section={de ? 'ANHANG · FINANZPLAN · 2/2' : 'APPENDIX · FINANCIAL PLAN · 2/2'} title={de ? 'KPI-Dashboard und Wachstumskurve.' : 'KPI dashboard and growth trajectory.'} subtitle={de ? '19 KPIs pro Jahr aus den fp_*-Tabellen abgeleitet. Keine hardcodierten Werte. Quelle: aktuelles Base-Case-Szenario.' : '19 KPIs per year derived from fp_* tables. No hardcoded values. Source: current base-case scenario.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
|
||||
<Page kicker="17" section={de ? 'ANHANG · FINANZPLAN · 2/2' : 'APPENDIX · FINANCIAL PLAN · 2/2'} title={de ? 'KPI-Dashboard und Wachstumskurve.' : 'KPI dashboard and growth trajectory.'} subtitle={de ? '19 KPIs pro Jahr aus den fp_*-Tabellen abgeleitet. Keine hardcodierten Werte. Base-Case-Szenario.' : '19 KPIs per year derived from fp_* tables. No hardcoded values. Base-case scenario.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
|
||||
|
||||
{/* KPI table */}
|
||||
<div style={{ marginBottom: '4mm' }}>
|
||||
{/* Compact KPI table */}
|
||||
<div style={{ marginBottom: '5mm' }}>
|
||||
<DataTable
|
||||
dense
|
||||
cols={[
|
||||
{ header: de ? 'KPI' : 'KPI', width: '24%' },
|
||||
...kpis.map(k => ({ header: String(k.year), numeric: true, width: '15.2%' as string })),
|
||||
{ header: de ? 'KPI' : 'KPI', width: '22%' },
|
||||
...kpis.map(k => ({ header: String(k.year), numeric: true })),
|
||||
]}
|
||||
rows={[
|
||||
['ARR (Dez)', ...kpis.map(k => fmtEur(k.arr))],
|
||||
['MRR (Dez)', ...kpis.map(k => fmtEur(k.mrr))],
|
||||
[de ? 'ARPU (€/Monat)' : 'ARPU (€/mo)', ...kpis.map(k => fmtEur(k.arpu))],
|
||||
[de ? 'Kunden' : 'Customers', ...kpis.map(k => k.customers.toLocaleString('de-DE'))],
|
||||
[de ? 'Mitarbeiter' : 'Employees', ...kpis.map(k => k.employees.toLocaleString('de-DE'))],
|
||||
[de ? 'MRR · ARPU' : 'MRR · ARPU', ...kpis.map(k => fmtEur(k.mrr) + ' · ' + fmtEur(k.arpu))],
|
||||
[de ? 'Kunden · MA' : 'Customers · FTE', ...kpis.map(k => k.customers.toLocaleString('de-DE') + ' · ' + k.employees)],
|
||||
[de ? 'Umsatz / MA' : 'Revenue / FTE', ...kpis.map(k => fmtEur(k.revenuePerEmployee))],
|
||||
[de ? 'Bruttomarge %' : 'Gross margin %', ...kpis.map(k => k.grossMargin + '%')],
|
||||
[de ? 'EBIT' : 'EBIT', ...kpis.map(k => <span style={{ color: k.ebit >= 0 ? COLORS.emerald700 : COLORS.red700, fontWeight: 700 }}>{fmtEur(k.ebit)}</span>)],
|
||||
[de ? 'EBIT-Marge' : 'EBIT margin', ...kpis.map(k => <span style={{ color: k.ebitMargin >= 0 ? COLORS.emerald700 : COLORS.red700 }}>{k.ebitMargin}%</span>)],
|
||||
[de ? 'Steuern (~30%)' : 'Taxes (~30%)', ...kpis.map(k => fmtEur(k.taxes))],
|
||||
[de ? 'Netto-Ergebnis' : 'Net income', ...kpis.map(k => <strong style={{ color: k.netIncome >= 0 ? COLORS.emerald700 : COLORS.red700 }}>{fmtEur(k.netIncome)}</strong>)],
|
||||
[de ? 'Burn-Rate (Dez)' : 'Burn rate (Dec)', ...kpis.map(k => fmtEur(k.burnRate))],
|
||||
[de ? 'Runway (Monate)' : 'Runway (months)', ...kpis.map(k => k.runway == null ? '∞' : String(k.runway))],
|
||||
[de ? 'Cash-Bestand (Dez)' : 'Cash balance (Dec)', ...kpis.map(k => fmtEur(k.cashBalance))],
|
||||
[de ? 'Bruttomarge' : 'Gross margin', ...kpis.map(k => k.grossMargin + '%')],
|
||||
[de ? 'EBIT · Marge' : 'EBIT · margin', ...kpis.map(k => <span key="e" style={{ color: k.ebit >= 0 ? COLORS.emerald700 : COLORS.red700, fontWeight: 700 }}>{fmtEur(k.ebit)} · {k.ebitMargin}%</span>)],
|
||||
[de ? 'Netto-Ergebnis' : 'Net income', ...kpis.map(k => <strong key="ni" style={{ color: k.netIncome >= 0 ? COLORS.emerald700 : COLORS.red700 }}>{fmtEur(k.netIncome)}</strong>)],
|
||||
[de ? 'Burn · Runway' : 'Burn · runway', ...kpis.map(k => fmtEur(k.burnRate) + ' · ' + (k.runway == null ? '∞' : String(k.runway) + 'm'))],
|
||||
[de ? 'Cash-Bestand' : 'Cash balance', ...kpis.map(k => fmtEur(k.cashBalance))],
|
||||
]}
|
||||
highlightFirstCol
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div style={{ flex: 1, minHeight: 0, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6mm' }}>
|
||||
{/* Revenue chart */}
|
||||
<div>
|
||||
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '2mm' }}>{de ? 'Umsatz-Wachstum (Mio. €)' : 'Revenue growth (€M)'}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', height: '32mm', gap: '4mm', borderBottom: `1px solid ${COLORS.slate300}`, paddingBottom: '1mm' }}>
|
||||
{kpis.map((k, i) => {
|
||||
const h = (k.totalRevenue / maxRev) * 100
|
||||
return (
|
||||
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<div style={{ fontSize: '7pt', color: COLORS.indigo700, fontWeight: 700, marginBottom: '1mm', fontVariantNumeric: 'tabular-nums' }}>{(k.totalRevenue / 1e6).toFixed(1)}</div>
|
||||
<div style={{ width: '100%', height: `${h}%`, minHeight: '2pt', background: COLORS.indigo600, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '4mm', marginTop: '1mm' }}>
|
||||
{kpis.map((k, i) => (
|
||||
<div key={i} style={{ flex: 1, fontSize: '7pt', color: COLORS.slate500, textAlign: 'center', fontWeight: 600 }}>{k.year}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Headcount + customers chart */}
|
||||
<div>
|
||||
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '2mm' }}>{de ? 'Mitarbeiter & Kunden' : 'Employees & customers'}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', height: '32mm', gap: '4mm', borderBottom: `1px solid ${COLORS.slate300}`, paddingBottom: '1mm' }}>
|
||||
{kpis.map((k, i) => {
|
||||
const h = (k.employees / maxEmp) * 100
|
||||
return (
|
||||
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', position: 'relative' }}>
|
||||
<div style={{ fontSize: '7pt', color: COLORS.slate700, marginBottom: '1mm', fontVariantNumeric: 'tabular-nums', textAlign: 'center', lineHeight: 1.2 }}>
|
||||
<strong style={{ color: COLORS.amber700 }}>{k.employees}</strong>
|
||||
<br />
|
||||
<span style={{ fontSize: '6.5pt', color: COLORS.slate500 }}>{k.customers}c</span>
|
||||
</div>
|
||||
<div style={{ width: '100%', height: `${h}%`, minHeight: '2pt', background: COLORS.amber600, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '4mm', marginTop: '1mm' }}>
|
||||
{kpis.map((k, i) => (
|
||||
<div key={i} style={{ flex: 1, fontSize: '7pt', color: COLORS.slate500, textAlign: 'center', fontWeight: 600 }}>{k.year}</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: '2mm', fontSize: '7pt', color: COLORS.slate500, display: 'flex', gap: '6mm' }}>
|
||||
<span><span style={{ display: 'inline-block', width: '8pt', height: '8pt', background: COLORS.amber600 }} /> {de ? 'Mitarbeiter (FTE)' : 'Employees (FTE)'}</span>
|
||||
<span style={{ color: COLORS.slate500 }}>{de ? 'Zahl unten: Kunden' : 'Number below: customers'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Charts grid 2x2 */}
|
||||
<div style={{ flex: 1, minHeight: 0, display: 'grid', gridTemplateColumns: '1fr 1fr', gridTemplateRows: '1fr 1fr', gap: '5mm 8mm' }}>
|
||||
<BarChart
|
||||
title={de ? 'Umsatz (€ Mio.)' : 'Revenue (€M)'}
|
||||
data={kpis.map(k => ({ label: String(k.year), value: k.totalRevenue, tone: 'default' }))}
|
||||
height={26}
|
||||
formatValue={pickFmt(Math.max(...kpis.map(k => k.totalRevenue)))}
|
||||
/>
|
||||
<BarChart
|
||||
title={de ? 'EBIT (€)' : 'EBIT (€)'}
|
||||
data={kpis.map(k => ({ label: String(k.year), value: k.ebit, tone: k.ebit >= 0 ? 'positive' : 'negative' }))}
|
||||
height={26}
|
||||
formatValue={pickFmt(Math.max(...kpis.map(k => Math.abs(k.ebit))))}
|
||||
/>
|
||||
<LineChart
|
||||
title={de ? 'Cash-Bestand (€)' : 'Cash balance (€)'}
|
||||
data={kpis.map(k => ({ label: String(k.year), value: Math.max(k.cashBalance, 0) }))}
|
||||
height={26}
|
||||
color={COLORS.indigo600}
|
||||
formatValue={pickFmt(Math.max(...kpis.map(k => k.cashBalance)))}
|
||||
fill
|
||||
/>
|
||||
<BarChart
|
||||
title={de ? 'Mitarbeiter · FTE' : 'Employees · FTE'}
|
||||
data={kpis.map(k => ({ label: String(k.year), value: k.employees, tone: 'accent' }))}
|
||||
height={26}
|
||||
formatValue={(n) => String(Math.round(n))}
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
@@ -341,18 +315,19 @@ export function PrintCapTablePage({ lang, pageNum, totalPages, versionName }: Sl
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: '8mm', flex: 1, minHeight: 0 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Anteilsverteilung Post Pre-Seed' : 'Share distribution post pre-seed'}</div>
|
||||
<div style={{ display: 'flex', height: '10mm', border: `1px solid ${COLORS.slate300}`, marginBottom: '6mm' }}>
|
||||
{CAP_TABLE_DATA.map(d => (
|
||||
<div key={d.name} style={{ width: `${d.pct}%`, background: d.color, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
|
||||
))}
|
||||
</div>
|
||||
{CAP_TABLE_DATA.map(d => (
|
||||
<div key={d.name} style={{ display: 'flex', alignItems: 'center', gap: '4mm', padding: '2.5mm 0', borderBottom: `1px solid ${COLORS.slate100}` }}>
|
||||
<div style={{ width: '4mm', height: '4mm', background: d.color, flexShrink: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
|
||||
<span style={{ fontSize: '10pt', color: COLORS.slate800, flex: 1, fontWeight: 500 }}>{d.name}</span>
|
||||
<span style={{ fontSize: '14pt', fontWeight: 800, color: COLORS.slate900, fontVariantNumeric: 'tabular-nums' }}>{d.pct}%</span>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8mm', marginBottom: '5mm' }}>
|
||||
<DonutChart size={50} thickness={11} segments={CAP_TABLE_DATA.map(d => ({ label: d.name, pct: d.pct, color: d.color }))} />
|
||||
<div style={{ flex: 1 }}>
|
||||
{CAP_TABLE_DATA.map(d => (
|
||||
<div key={d.name} style={{ display: 'flex', alignItems: 'center', gap: '3mm', padding: '2mm 0', borderBottom: `1px solid ${COLORS.slate100}` }}>
|
||||
<div style={{ width: '4mm', height: '4mm', background: d.color, flexShrink: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
|
||||
<span style={{ fontSize: '9.5pt', color: COLORS.slate800, flex: 1, fontWeight: 500 }}>{d.name}</span>
|
||||
<span style={{ fontSize: '14pt', fontWeight: 800, color: COLORS.slate900, fontVariantNumeric: 'tabular-nums' }}>{d.pct}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -8,54 +8,95 @@ interface SlideBase { lang: Language; pageNum: number; totalPages: number; versi
|
||||
export function PrintCoverPage({ company, funding, lang, versionName }: { company: PitchCompany; funding: PitchFunding; lang: Language; versionName: string }) {
|
||||
const de = lang === 'de'
|
||||
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.')
|
||||
return (
|
||||
<div className="print-page-break">
|
||||
<div className="print-page" style={{ width: '297mm', height: '210mm', background: '#ffffff', color: COLORS.slate900, fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif", display: 'flex', flexDirection: 'column', boxSizing: 'border-box', padding: '14mm', margin: '0 auto 24px', boxShadow: '0 4px 24px rgba(15,23,42,0.10)', overflow: 'hidden' }}>
|
||||
{/* TOP META */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderBottom: `1px solid ${COLORS.slate200}`, paddingBottom: '3mm' }}>
|
||||
<span style={{ fontSize: '8pt', fontWeight: 700, letterSpacing: '0.16em', color: COLORS.indigo600, textTransform: 'uppercase' }}>BreakPilot · Investor Brief</span>
|
||||
<span style={{ fontSize: '8pt', color: COLORS.slate500, fontWeight: 500 }}>{versionName}</span>
|
||||
</div>
|
||||
<div className="print-page" style={{ width: '297mm', height: '210mm', background: '#ffffff', color: COLORS.slate900, fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif", display: 'flex', boxSizing: 'border-box', margin: '0 auto 24px', boxShadow: '0 4px 24px rgba(15,23,42,0.10)', overflow: 'hidden', padding: 0 }}>
|
||||
|
||||
{/* HERO */}
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', paddingTop: '8mm' }}>
|
||||
<p style={{ fontSize: '10pt', fontWeight: 700, color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.16em', margin: 0 }}>
|
||||
{instrument} · Q4 2026
|
||||
</p>
|
||||
<h1 style={{ fontSize: '72pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 0.95, letterSpacing: '-0.025em', margin: '6mm 0 4mm' }}>
|
||||
{company?.name || 'BreakPilot'}<span style={{ color: COLORS.indigo600 }}>.</span>
|
||||
</h1>
|
||||
<p style={{ fontSize: '14pt', fontWeight: 500, color: COLORS.slate700, lineHeight: 1.35, maxWidth: '180mm', margin: 0, letterSpacing: '-0.005em' }}>
|
||||
{tagline}
|
||||
</p>
|
||||
<div style={{ marginTop: '14mm', height: '1px', background: COLORS.slate200, maxWidth: '120mm' }} />
|
||||
<p style={{ fontSize: '11pt', color: COLORS.slate600, marginTop: '6mm', maxWidth: '170mm', lineHeight: 1.5, fontWeight: 400 }}>
|
||||
{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.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* META GRID */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '8mm', borderTop: `1px solid ${COLORS.slate200}`, borderBottom: `1px solid ${COLORS.slate200}`, padding: '5mm 0', marginBottom: '5mm' }}>
|
||||
{([
|
||||
[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 ? 'Runde' : 'Round', funding?.round_name || 'Pre-Seed'],
|
||||
] as [string, string][]).map(([label, val]) => (
|
||||
<div key={label}>
|
||||
<p style={{ fontSize: '7.5pt', color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', margin: 0, fontWeight: 600 }}>{label}</p>
|
||||
<p style={{ fontSize: '14pt', fontWeight: 700, color: COLORS.slate900, margin: '2mm 0 0', fontVariantNumeric: 'tabular-nums' }}>{val}</p>
|
||||
{/* LEFT INDIGO BLOCK */}
|
||||
<div style={{
|
||||
width: '95mm',
|
||||
background: COLORS.indigo600,
|
||||
color: '#ffffff',
|
||||
padding: '16mm 12mm',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
WebkitPrintColorAdjust: 'exact',
|
||||
printColorAdjust: 'exact',
|
||||
}}>
|
||||
<div>
|
||||
<div style={{ fontSize: '8pt', fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.7)' }}>
|
||||
{de ? 'Investor Brief' : 'Investor Brief'}
|
||||
</div>
|
||||
))}
|
||||
<div style={{ marginTop: '6mm', height: '1px', background: 'rgba(255,255,255,0.3)', width: '32mm' }} />
|
||||
<div style={{ marginTop: '6mm', fontSize: '11pt', color: 'rgba(255,255,255,0.85)', lineHeight: 1.55, fontWeight: 500 }}>
|
||||
{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.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero stat */}
|
||||
<div>
|
||||
<div style={{ fontSize: '7.5pt', fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.7)' }}>
|
||||
{de ? '25 000 + atomare Prüfaspekte' : '25 000 + atomic audit aspects'}
|
||||
</div>
|
||||
<div style={{ fontSize: '7.5pt', fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.7)', marginTop: '1.5mm' }}>
|
||||
{de ? '380 + Regularien · 10 Branchen' : '380 + regulations · 10 industries'}
|
||||
</div>
|
||||
<div style={{ fontSize: '7.5pt', fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.7)', marginTop: '1.5mm' }}>
|
||||
{de ? '500 K + Lines of Code · 45 Container' : '500 K + lines of code · 45 containers'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom: version + confidential */}
|
||||
<div>
|
||||
<div style={{ fontSize: '7pt', letterSpacing: '0.16em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.55)', fontWeight: 600 }}>{versionName}</div>
|
||||
<div style={{ marginTop: '2mm', fontSize: '7pt', letterSpacing: '0.16em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.55)', fontWeight: 600 }}>{de ? 'Vertraulich · Nur Investoren' : 'Confidential · Investors only'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '7.5pt', color: COLORS.slate500 }}>
|
||||
<span>{de ? 'Vertraulich, Nur für Investoren' : 'Confidential, For Investor Use Only'}</span>
|
||||
<span style={{ fontWeight: 700, letterSpacing: '0.14em', color: COLORS.indigo600 }}>CONFIDENTIAL</span>
|
||||
{/* RIGHT WHITE PANE */}
|
||||
<div style={{ flex: 1, padding: '16mm 16mm', display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '10pt', fontWeight: 700, color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.16em' }}>
|
||||
{instrument} · Q4 2026
|
||||
</div>
|
||||
<h1 style={{ fontSize: '88pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 0.92, letterSpacing: '-0.035em', margin: '8mm 0 6mm' }}>
|
||||
{company?.name || 'BreakPilot'}<span style={{ color: COLORS.indigo600 }}>.</span>
|
||||
</h1>
|
||||
<div style={{ height: '2px', width: '40mm', background: COLORS.indigo600, marginBottom: '6mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
|
||||
<p style={{ fontSize: '16pt', fontWeight: 500, color: COLORS.slate700, lineHeight: 1.3, maxWidth: '160mm', margin: 0, letterSpacing: '-0.008em' }}>
|
||||
{tagline}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Key terms */}
|
||||
<div>
|
||||
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: '3mm', paddingBottom: '2mm', borderBottom: `1px solid ${COLORS.slate200}` }}>
|
||||
{de ? 'Key Terms' : 'Key terms'}
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '6mm' }}>
|
||||
{([
|
||||
[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]) => (
|
||||
<div key={label}>
|
||||
<div style={{ fontSize: '7pt', color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', fontWeight: 700 }}>{label}</div>
|
||||
<div style={{ fontSize: '18pt', fontWeight: 800, color: COLORS.slate900, marginTop: '1.5mm', fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.01em' }}>{val}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: '5mm', fontSize: '8pt', color: COLORS.slate500, lineHeight: 1.5 }}>
|
||||
{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.'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Language, PitchMarket, PitchTeamMember, PitchMilestone, PitchFunding } from '@/lib/types'
|
||||
import { Page, TwoCol, Bullets, Callout, COLORS, DataTable, StatLine } from './PrintLayout'
|
||||
import { Page, Callout, COLORS, DataTable, StatLine } from './PrintLayout'
|
||||
import { MarketFunnel, ComparisonBars, DonutChart } from './PrintCharts'
|
||||
|
||||
interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string }
|
||||
|
||||
@@ -21,49 +22,19 @@ export function PrintMarketPage({ market, lang, pageNum, totalPages, versionName
|
||||
return (
|
||||
<Page kicker="09" section={de ? 'MARKT' : 'MARKET'} title={de ? 'Compliance & Code-Security für produzierende Unternehmen.' : 'Compliance & code security for manufacturing companies.'} subtitle={de ? 'Validierter Markt: Top-10 Compliance-Anbieter erwirtschaften >$1,1 Mrd. ARR. Kein Anbieter bedient den Maschinenbau spezifisch.' : 'Validated market: top-10 compliance vendors generate >$1.1B ARR. No vendor specifically serves manufacturing.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName} footnote={de ? 'Sacra · Bitkom Cloud Monitor 2024 · DIHK 2024 · VDMA · Statista' : 'Sacra · Bitkom Cloud Monitor 2024 · DIHK 2024 · VDMA · Statista'}>
|
||||
|
||||
{/* TAM/SAM/SOM as nested rectangles */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: '8mm', flex: 1, minHeight: 0 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.3fr 1fr', gap: '8mm', flex: 1, minHeight: 0 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Marktdimensionierung' : 'Market sizing'}</div>
|
||||
|
||||
{/* TAM */}
|
||||
{tam && (
|
||||
<div style={{ border: `1px solid ${COLORS.slate300}`, padding: '4mm', marginBottom: '3mm' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '1.5mm' }}>
|
||||
<span style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em' }}>TAM · {de ? 'Total Addressable Market' : 'Total Addressable Market'}</span>
|
||||
<span style={{ fontSize: '7.5pt', color: COLORS.slate500 }}>+{tam.growth_rate_pct ?? 14}% p.a.</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '28pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>{fmtEur(tam.value_eur, de)}</div>
|
||||
<div style={{ fontSize: '8pt', color: COLORS.slate600, marginTop: '2mm', lineHeight: 1.4 }}>{de ? 'Globaler Compliance- und GRC-Markt (alle Branchen, alle Größen).' : 'Global compliance and GRC market (all industries, all sizes).'}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SAM */}
|
||||
{sam && (
|
||||
<div style={{ border: `1px solid ${COLORS.indigo600}`, background: COLORS.indigo50, padding: '4mm', marginBottom: '3mm', marginLeft: '6mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '1.5mm' }}>
|
||||
<span style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.indigo700, textTransform: 'uppercase', letterSpacing: '0.12em' }}>SAM · {de ? 'Serviceable Addressable' : 'Serviceable Addressable'}</span>
|
||||
<span style={{ fontSize: '7.5pt', color: COLORS.indigo700 }}>+{sam.growth_rate_pct ?? 18}% p.a.</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '26pt', fontWeight: 800, color: COLORS.indigo700, lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>{fmtEur(sam.value_eur, de)}</div>
|
||||
<div style={{ fontSize: '8pt', color: COLORS.slate700, marginTop: '2mm', lineHeight: 1.4 }}>{de ? 'DACH + EU: regulierte Branchen, KMU + Enterprise.' : 'DACH + EU: regulated industries, SMB + enterprise.'}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SOM */}
|
||||
{som && (
|
||||
<div style={{ border: `2px solid ${COLORS.emerald600}`, background: COLORS.emerald50, padding: '4mm', marginLeft: '12mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '1.5mm' }}>
|
||||
<span style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.emerald700, textTransform: 'uppercase', letterSpacing: '0.12em' }}>SOM · {de ? 'Kernmarkt 5 Jahre' : 'Core market 5 yrs'}</span>
|
||||
<span style={{ fontSize: '7.5pt', color: COLORS.emerald700 }}>+{som.growth_rate_pct ?? 25}% p.a.</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '24pt', fontWeight: 800, color: COLORS.emerald700, lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>{fmtEur(som.value_eur, de)}</div>
|
||||
<div style={{ fontSize: '8pt', color: COLORS.slate700, marginTop: '2mm', lineHeight: 1.4 }}>{de ? 'Anlagen- und Maschinenbau DACH, unser Kernsegment.' : 'Machine & plant manufacturing DACH, our core segment.'}</div>
|
||||
</div>
|
||||
{tam && sam && som && (
|
||||
<MarketFunnel
|
||||
tam={{ value: tam.value_eur, label: de ? 'Total Addressable' : 'Total Addressable', growth: tam.growth_rate_pct ?? 14, note: de ? 'Globaler Compliance- und GRC-Markt (alle Branchen, alle Größen).' : 'Global compliance and GRC market (all industries, all sizes).' }}
|
||||
sam={{ value: sam.value_eur, label: de ? 'Serviceable Addressable' : 'Serviceable Addressable', growth: sam.growth_rate_pct ?? 18, note: de ? 'DACH + EU: regulierte Branchen, KMU und Enterprise.' : 'DACH + EU: regulated industries, SMB and enterprise.' }}
|
||||
som={{ value: som.value_eur, label: de ? 'Kernmarkt 5 Jahre' : 'Core market 5 yrs', growth: som.growth_rate_pct ?? 25, note: de ? 'Anlagen- und Maschinenbau DACH, unser Kernsegment.' : 'Machine and plant manufacturing DACH, our core segment.' }}
|
||||
fmt={(v) => fmtEur(v, de)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Segment context */}
|
||||
<div>
|
||||
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Kernsegment: Maschinen- und Anlagenbau DACH' : 'Core segment: Machine & plant manufacturing DACH'}</div>
|
||||
<DataTable
|
||||
@@ -191,43 +162,47 @@ export function PrintTeamPage({ team, lang, pageNum, totalPages, versionName }:
|
||||
return (
|
||||
<Page kicker="13" section={de ? 'TEAM' : 'TEAM'} title={de ? 'Gründer mit Domain-Expertise.' : 'Founders with domain expertise.'} subtitle={de ? 'Komplementäres Gründerduo: Vertrieb + Engineering. Beide haben bereits skaliert. Beide kennen den Markt.' : 'Complementary founding duo: sales + engineering. Both have scaled. Both know the market.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
|
||||
|
||||
<TwoCol ratio="1:1" gap="8mm" left={
|
||||
<div style={{ borderLeft: `2px solid ${COLORS.indigo600}`, paddingLeft: '5mm', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}>
|
||||
<span style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.12em' }}>CO-FOUNDER · 01 / 02</span>
|
||||
<span style={{ fontSize: '7.5pt', color: COLORS.slate500, fontWeight: 600 }}>Equity {members[0]?.equity_pct ?? 37.3}%</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '18pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 1.1, marginBottom: '1mm', letterSpacing: '-0.01em' }}>{members[0]?.name || 'Benjamin Bönisch'}</div>
|
||||
<div style={{ fontSize: '10pt', fontWeight: 600, color: COLORS.indigo600, marginBottom: '3mm' }}>{de ? (members[0]?.role_de || 'CEO & Co-Founder') : (members[0]?.role_en || 'CEO & Co-Founder')}</div>
|
||||
<div style={{ fontSize: '9pt', color: COLORS.slate700, lineHeight: 1.55, flex: 1 }}>{de ? (members[0]?.bio_de) : (members[0]?.bio_en)}</div>
|
||||
<div style={{ marginTop: '4mm', paddingTop: '3mm', borderTop: `1px solid ${COLORS.slate200}` }}>
|
||||
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: '2mm' }}>{de ? 'Expertise' : 'Expertise'}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1.5mm' }}>
|
||||
{(members[0]?.expertise || []).map((e, i) => (
|
||||
<span key={i} style={{ fontSize: '8pt', padding: '0.5mm 2mm', border: `1px solid ${COLORS.slate300}`, color: COLORS.slate700 }}>{e}</span>
|
||||
))}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8mm', flex: 1, minHeight: 0 }}>
|
||||
{[0, 1].map(idx => {
|
||||
const m = members[idx]
|
||||
if (!m) return null
|
||||
return (
|
||||
<div key={idx} style={{ border: `1px solid ${COLORS.slate200}`, borderTop: `3px solid ${COLORS.indigo600}`, padding: '5mm', display: 'flex', flexDirection: 'column', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
<div style={{ display: 'flex', gap: '5mm', alignItems: 'flex-start', marginBottom: '4mm' }}>
|
||||
{/* Photo */}
|
||||
<div style={{ width: '32mm', height: '32mm', flexShrink: 0, border: `1px solid ${COLORS.slate200}`, background: COLORS.slate100, overflow: 'hidden', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
{m.photo_url ? (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img src={m.photo_url} alt={m.name} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
|
||||
) : (
|
||||
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '24pt', fontWeight: 800, color: COLORS.slate400, letterSpacing: '-0.02em' }}>
|
||||
{m.name?.split(' ').map(s => s[0]).slice(0, 2).join('') || '?'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Header text */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}>
|
||||
<span style={{ fontSize: '7pt', fontWeight: 700, color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.14em' }}>CO-FOUNDER · {String(idx + 1).padStart(2, '0')} / 02</span>
|
||||
<span style={{ fontSize: '7pt', color: COLORS.slate500, fontWeight: 700, fontVariantNumeric: 'tabular-nums' }}>{(m.equity_pct ?? 37.3).toLocaleString('de-DE')}% Equity</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '18pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 1.05, marginBottom: '1mm', letterSpacing: '-0.015em' }}>{m.name}</div>
|
||||
<div style={{ fontSize: '10pt', fontWeight: 600, color: COLORS.indigo600 }}>{de ? m.role_de : m.role_en}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: '9pt', color: COLORS.slate700, lineHeight: 1.55, flex: 1 }}>{de ? m.bio_de : m.bio_en}</div>
|
||||
<div style={{ marginTop: '4mm', paddingTop: '3mm', borderTop: `1px solid ${COLORS.slate200}` }}>
|
||||
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '2mm' }}>{de ? 'Expertise' : 'Expertise'}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1.5mm' }}>
|
||||
{(m.expertise || []).map((e, i) => (
|
||||
<span key={i} style={{ fontSize: '8pt', padding: '0.5mm 2mm', background: COLORS.indigo50, color: COLORS.indigo700, fontWeight: 600, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>{e}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} right={
|
||||
<div style={{ borderLeft: `2px solid ${COLORS.indigo600}`, paddingLeft: '5mm', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}>
|
||||
<span style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.12em' }}>CO-FOUNDER · 02 / 02</span>
|
||||
<span style={{ fontSize: '7.5pt', color: COLORS.slate500, fontWeight: 600 }}>Equity {members[1]?.equity_pct ?? 37.3}%</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '18pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 1.1, marginBottom: '1mm', letterSpacing: '-0.01em' }}>{members[1]?.name || 'Sharang Parnerkar'}</div>
|
||||
<div style={{ fontSize: '10pt', fontWeight: 600, color: COLORS.indigo600, marginBottom: '3mm' }}>{de ? (members[1]?.role_de || 'CTO & Co-Founder') : (members[1]?.role_en || 'CTO & Co-Founder')}</div>
|
||||
<div style={{ fontSize: '9pt', color: COLORS.slate700, lineHeight: 1.55, flex: 1 }}>{de ? (members[1]?.bio_de) : (members[1]?.bio_en)}</div>
|
||||
<div style={{ marginTop: '4mm', paddingTop: '3mm', borderTop: `1px solid ${COLORS.slate200}` }}>
|
||||
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: '2mm' }}>{de ? 'Expertise' : 'Expertise'}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1.5mm' }}>
|
||||
{(members[1]?.expertise || []).map((e, i) => (
|
||||
<span key={i} style={{ fontSize: '8pt', padding: '0.5mm 2mm', border: `1px solid ${COLORS.slate300}`, color: COLORS.slate700 }}>{e}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '5mm', flexShrink: 0 }}>
|
||||
<Callout tone="accent" label={de ? 'Equity-Struktur' : 'Equity structure'}>
|
||||
@@ -292,27 +267,31 @@ export function PrintTheAskPage({ funding, lang, pageNum, totalPages, versionNam
|
||||
|
||||
{/* Use of funds */}
|
||||
<div>
|
||||
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Use of Funds' : 'Use of Funds'}</div>
|
||||
{/* Horizontal bar */}
|
||||
<div style={{ display: 'flex', height: '8mm', border: `1px solid ${COLORS.slate300}`, marginBottom: '5mm' }}>
|
||||
{useOfFunds.map((u, i) => {
|
||||
const colors = [COLORS.indigo600, COLORS.indigo500, COLORS.amber600, COLORS.slate600, COLORS.slate400]
|
||||
return (
|
||||
<div key={i} style={{ width: `${u.percentage}%`, background: colors[i % colors.length], WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{useOfFunds.map((u, i) => {
|
||||
const colors = [COLORS.indigo600, COLORS.indigo500, COLORS.amber600, COLORS.slate600, COLORS.slate400]
|
||||
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Use of Funds' : 'Use of funds'}</div>
|
||||
{(() => {
|
||||
const palette = [COLORS.indigo600, COLORS.indigo500, COLORS.amber600, COLORS.slate600, COLORS.slate400]
|
||||
return (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '3mm', padding: '2.5mm 0', borderBottom: `1px solid ${COLORS.slate100}` }}>
|
||||
<div style={{ width: '3mm', height: '3mm', background: colors[i % colors.length], flexShrink: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
|
||||
<div style={{ flex: 1, fontSize: '9pt', color: COLORS.slate800, fontWeight: 500 }}>{de ? u.label_de : u.label_en}</div>
|
||||
<div style={{ fontSize: '11pt', fontWeight: 800, color: COLORS.slate900, fontVariantNumeric: 'tabular-nums' }}>{u.percentage}%</div>
|
||||
<div style={{ fontSize: '8pt', color: COLORS.slate500, fontVariantNumeric: 'tabular-nums', minWidth: '14mm', textAlign: 'right' }}>€{(amount * u.percentage / 100 / 1000).toFixed(0)}k</div>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '6mm' }}>
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<DonutChart
|
||||
size={48}
|
||||
thickness={9}
|
||||
segments={useOfFunds.map((u, i) => ({ label: de ? u.label_de : u.label_en, pct: u.percentage, color: palette[i % palette.length] }))}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
{useOfFunds.map((u, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '3mm', padding: '2mm 0', borderBottom: `1px solid ${COLORS.slate100}` }}>
|
||||
<div style={{ width: '3mm', height: '3mm', background: palette[i % palette.length], flexShrink: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
|
||||
<div style={{ flex: 1, fontSize: '8.5pt', color: COLORS.slate800, fontWeight: 500 }}>{de ? u.label_de : u.label_en}</div>
|
||||
<div style={{ fontSize: '11pt', fontWeight: 800, color: COLORS.slate900, fontVariantNumeric: 'tabular-nums', minWidth: '12mm', textAlign: 'right' }}>{u.percentage}%</div>
|
||||
<div style={{ fontSize: '7.5pt', color: COLORS.slate500, fontVariantNumeric: 'tabular-nums', minWidth: '14mm', textAlign: 'right' }}>€{(amount * u.percentage / 100 / 1000).toFixed(0)}k</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
@@ -323,68 +302,70 @@ export function PrintTheAskPage({ funding, lang, pageNum, totalPages, versionNam
|
||||
|
||||
export function PrintCustomerSavingsPage({ lang, pageNum, totalPages, versionName }: SlideBase) {
|
||||
const de = lang === 'de'
|
||||
const items = [
|
||||
{ l: de ? 'Pentests' : 'Pentests', today: 15000, bp: 13000 },
|
||||
{ l: de ? 'CE-SW-Risiko' : 'CE software risk', today: 12000, bp: 9000 },
|
||||
{ l: de ? 'Compliance-Zeit' : 'Compliance time', today: 18000, bp: 15000 },
|
||||
{ l: de ? 'Audit-Vorber.' : 'Audit prep', today: 9000, bp: 9000 },
|
||||
{ l: de ? 'Legal (DSGVO/AI Act)' : 'Legal (GDPR/AI Act)', today: 8000, bp: 5000 },
|
||||
{ l: de ? 'Auditmanager-Software' : 'Audit manager SW', today: 5000, bp: 0 },
|
||||
{ l: de ? 'Schulungen extern' : 'External training', today: 4000, bp: 4000 },
|
||||
]
|
||||
const totalToday = items.reduce((s, i) => s + i.today, 0)
|
||||
const totalSaved = items.reduce((s, i) => s + i.bp, 0)
|
||||
const totalBpCost = 25000
|
||||
|
||||
return (
|
||||
<Page kicker="15" section={de ? 'KUNDENERSPARNIS' : 'CUSTOMER SAVINGS'} title={de ? 'Kunden sparen mehr als sie zahlen, vom ersten Tag an.' : 'Customers save more than they pay, from day one.'} subtitle={de ? 'Detaillierte Aufschlüsselung der Einsparungen für ein typisches KMU (50 Mitarbeiter) im ersten Jahr.' : 'Detailed savings breakdown for a typical SME (50 employees) in year one.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
|
||||
<Page kicker="15" section={de ? 'KUNDENERSPARNIS' : 'CUSTOMER SAVINGS'} title={de ? 'Kunden sparen mehr als sie zahlen, vom ersten Tag an.' : 'Customers save more than they pay, from day one.'} subtitle={de ? 'Detaillierte Aufschlüsselung für ein typisches KMU (50 MA) im ersten Jahr. €25k Kosten, €55k Ersparnis.' : 'Detailed breakdown for a typical SME (50 emp.) in year one. €25k cost, €55k savings.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8mm', flex: 1, minHeight: 0 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '2mm' }}>{de ? 'Was der Kunde heute bezahlt (ohne BreakPilot)' : 'What customer pays today (without BreakPilot)'}</div>
|
||||
<DataTable
|
||||
cols={[
|
||||
{ header: de ? 'Position' : 'Item' },
|
||||
{ header: de ? 'Heute' : 'Today', numeric: true, width: '22%' },
|
||||
{ header: de ? 'Frequenz' : 'Frequency', width: '24%' },
|
||||
]}
|
||||
rows={[
|
||||
[de ? 'Externe Pentests' : 'External pentests', '€15.000', de ? 'jährlich' : 'annually'],
|
||||
[de ? 'CE-Software-Risikobeurteilung' : 'CE software risk assessment', '€12.000', de ? 'pro Produkt' : 'per product'],
|
||||
[de ? 'Compliance-Beauftragte/r (anteilig)' : 'Compliance officer (pro-rated)', '€18.000', de ? 'jährlich' : 'annually'],
|
||||
[de ? 'Audit-Vorbereitung extern' : 'External audit prep', '€9.000', de ? 'jährlich' : 'annually'],
|
||||
[de ? 'Rechtsanwälte (DSGVO, AI Act)' : 'Lawyers (GDPR, AI Act)', '€8.000', de ? 'jährlich' : 'annually'],
|
||||
[de ? 'Auditmanager-Software' : 'Audit manager software', '€5.000', de ? 'jährlich' : 'annually'],
|
||||
[de ? 'Schulungen extern' : 'External training', '€4.000', de ? 'jährlich' : 'annually'],
|
||||
[<strong key="sum">{de ? 'Summe heute' : 'Today total'}</strong>, <strong key="v">€71.000</strong>, ''],
|
||||
]}
|
||||
dense
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: '4mm' }}>
|
||||
<Callout tone="negative" label={de ? 'Versteckte Kosten' : 'Hidden costs'}>
|
||||
{de
|
||||
? 'Zeit der GF + Compliance-Beauftragten (~30 Tage/Jahr), DSGVO-Bußgelder bei Fehlern (bis zu 4% Jahresumsatz), verlorene RFQs durch fehlende Compliance-Nachweise.'
|
||||
: 'Time of management + compliance officer (~30 days/year), GDPR fines on errors (up to 4% annual revenue), lost RFQs due to missing compliance evidence.'}
|
||||
</Callout>
|
||||
{/* Big stat header */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '4mm', padding: '4mm 0', borderTop: `1px solid ${COLORS.slate200}`, borderBottom: `1px solid ${COLORS.slate200}`, marginBottom: '5mm' }}>
|
||||
{[
|
||||
{ l: de ? 'Heute (ohne BP)' : 'Today (without BP)', v: '€' + (totalToday / 1000).toFixed(0) + 'k', tone: COLORS.red700 },
|
||||
{ l: de ? 'BreakPilot Pro / Jahr' : 'BreakPilot Pro / year', v: '€' + (totalBpCost / 1000).toFixed(0) + 'k', tone: COLORS.indigo600 },
|
||||
{ l: de ? 'Ersparnis / KMU' : 'Savings / SME', v: '€' + (totalSaved / 1000).toFixed(0) + 'k', tone: COLORS.emerald700 },
|
||||
{ l: de ? 'Netto-Effekt' : 'Net effect', v: '+€' + ((totalSaved - totalBpCost) / 1000).toFixed(0) + 'k', tone: COLORS.emerald700 },
|
||||
].map((k, i) => (
|
||||
<div key={i}>
|
||||
<div style={{ fontSize: '26pt', fontWeight: 800, color: k.tone, lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>{k.v}</div>
|
||||
<div style={{ fontSize: '7.5pt', color: COLORS.slate500, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em', marginTop: '1.5mm' }}>{k.l}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bar comparison */}
|
||||
<div style={{ flex: 1, minHeight: 0, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8mm' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Heute vs. mit BreakPilot (€/Jahr/KMU)' : 'Today vs. with BreakPilot (€/yr/SME)'}</div>
|
||||
<ComparisonBars
|
||||
rows={items.map(it => ({
|
||||
label: it.l,
|
||||
bars: [
|
||||
{ tone: 'negative', value: it.today, cap: de ? 'Heute' : 'Today' },
|
||||
{ tone: 'positive', value: it.bp, cap: de ? 'gespart mit BP' : 'saved with BP' },
|
||||
],
|
||||
}))}
|
||||
formatValue={(n) => '€' + (n / 1000).toFixed(0) + 'k'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '2mm' }}>{de ? 'Mit BreakPilot' : 'With BreakPilot'}</div>
|
||||
<DataTable
|
||||
cols={[
|
||||
{ header: de ? 'Position' : 'Item' },
|
||||
{ header: de ? 'Mit BP' : 'With BP', numeric: true, width: '22%' },
|
||||
{ header: de ? 'Ersparnis' : 'Savings', numeric: true, width: '22%' },
|
||||
]}
|
||||
rows={[
|
||||
[de ? 'Pentests (kontinuierlich)' : 'Pentests (continuous)', de ? 'inklusive' : 'included', '€13.000'],
|
||||
[de ? 'CE-Risiko (Code-Basis)' : 'CE risk (code-based)', de ? 'inklusive' : 'included', '€9.000'],
|
||||
[de ? 'Compliance-Zeit' : 'Compliance time', '–80%', '€15.000'],
|
||||
[de ? 'Audit-Vorbereitung' : 'Audit prep', de ? 'auf Knopfdruck' : 'one-click', '€9.000'],
|
||||
[de ? 'Legal-Stunden' : 'Legal hours', '–60%', '€5.000'],
|
||||
[de ? 'Schulungen (Academy)' : 'Training (Academy)', de ? 'inklusive' : 'included', '€4.000'],
|
||||
[<strong key="cost">{de ? 'BreakPilot Pro' : 'BreakPilot Pro'}</strong>, <strong key="bp">€25.000</strong>, ''],
|
||||
[<strong key="ts">{de ? 'Gesamt-Ersparnis' : 'Total savings'}</strong>, '', <strong key="v" style={{ color: COLORS.emerald700 }}>€55.000</strong>],
|
||||
]}
|
||||
dense
|
||||
/>
|
||||
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Ersparnis-Aufschlüsselung' : 'Savings breakdown'}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0' }}>
|
||||
<StatLine label={de ? 'Pentests (kontinuierlich, inklusive)' : 'Pentests (continuous, included)'} value="€13.000" tone="positive" />
|
||||
<StatLine label={de ? 'CE-Risiko (Code-basiert, inkl.)' : 'CE risk (code-based, incl.)'} value="€9.000" tone="positive" />
|
||||
<StatLine label={de ? 'Compliance-Zeit (−80%)' : 'Compliance time (−80%)'} value="€15.000" tone="positive" />
|
||||
<StatLine label={de ? 'Audit-Vorber. (auf Knopfdruck)' : 'Audit prep (one-click)'} value="€9.000" tone="positive" />
|
||||
<StatLine label={de ? 'Legal-Stunden (−60%)' : 'Legal hours (−60%)'} value="€5.000" tone="positive" />
|
||||
<StatLine label={de ? 'Schulungen (Academy inkl.)' : 'Training (Academy incl.)'} value="€4.000" tone="positive" />
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '4mm', padding: '4mm', background: COLORS.emerald50, border: `1px solid ${COLORS.emerald600}`, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
<div style={{ fontSize: '7.5pt', color: COLORS.emerald700, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em' }}>{de ? 'Netto-Effekt Jahr 1' : 'Net effect year 1'}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: '4mm', marginTop: '2mm' }}>
|
||||
<div style={{ fontSize: '32pt', fontWeight: 800, color: COLORS.emerald700, lineHeight: 1, fontVariantNumeric: 'tabular-nums' }}>+€30k</div>
|
||||
<div style={{ fontSize: '11pt', color: COLORS.emerald700, fontWeight: 600 }}>{de ? 'pro KMU / Jahr' : 'per SME / year'}</div>
|
||||
</div>
|
||||
<div style={{ fontSize: '8pt', color: COLORS.slate700, marginTop: '2mm', lineHeight: 1.4 }}>{de ? 'Kunde spart €55k, zahlt €25k. ROI ab Tag 1. Zusätzlich: keine Bußgelder, RFQ-Win-Rate ↑, Schlaf-Frieden für die GF.' : 'Customer saves €55k, pays €25k. ROI from day 1. Plus: no fines, RFQ win-rate ↑, peace of mind for management.'}</div>
|
||||
<div style={{ marginTop: '5mm' }}>
|
||||
<Callout tone="negative" label={de ? 'Versteckte Kosten' : 'Hidden costs'}>
|
||||
{de
|
||||
? 'Zeit der GF + Compliance-Beauftragten (~30 Tage/Jahr), DSGVO-Bußgelder (bis 4% Jahresumsatz), verlorene RFQs durch fehlende Compliance-Nachweise. Nicht in obigen Zahlen enthalten.'
|
||||
: 'Time of management + compliance officer (~30 days/year), GDPR fines (up to 4% annual revenue), lost RFQs from missing compliance evidence. Not included in numbers above.'}
|
||||
</Callout>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { Language, PitchProduct } from '@/lib/types'
|
||||
import { Page, TwoCol, ThreeCol, FourCol, Bullets, Callout, COLORS, Divider, DataTable, StatLine } from './PrintLayout'
|
||||
import { Page, Bullets, Callout, COLORS, DataTable, StatLine } from './PrintLayout'
|
||||
import { StepStrip, LoopDiagram } from './PrintDiagrams'
|
||||
import { getDetails } from '@/components/slides/USPSlide.data'
|
||||
import {
|
||||
ScanLine, ShieldCheck, FileText, ClipboardCheck, Users, UserCheck,
|
||||
AlertTriangle, Brain, Target, GraduationCap, TrendingUp, MessageSquare,
|
||||
Shield, Layers, Globe, FileSearch, Sparkles, Repeat, ArrowLeftRight, Infinity,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string }
|
||||
|
||||
const USP_ICON: Record<string, LucideIcon> = {
|
||||
rfq: FileSearch, process: ClipboardCheck, bidir: ArrowLeftRight, cont: Repeat,
|
||||
trace: Layers, engine: Sparkles, opt: TrendingUp, stack: Globe, hub: Infinity,
|
||||
}
|
||||
|
||||
/* ===== USP, PAGE 1 (4 pillars) ===== */
|
||||
|
||||
export function PrintUSPPage1({ lang, pageNum, totalPages, versionName }: SlideBase) {
|
||||
@@ -17,19 +29,25 @@ export function PrintUSPPage1({ lang, pageNum, totalPages, versionName }: SlideB
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6mm', flex: 1, minHeight: 0 }}>
|
||||
{pillars.map((k, i) => {
|
||||
const p = d[k]
|
||||
const Icon = USP_ICON[k]
|
||||
return (
|
||||
<div key={k} style={{ borderLeft: `2px solid ${COLORS.indigo600}`, paddingLeft: '5mm', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}>
|
||||
<span style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.12em' }}>{p.kicker}</span>
|
||||
<span style={{ fontSize: '7.5pt', color: COLORS.slate400, fontVariantNumeric: 'tabular-nums' }}>{String(i + 1).padStart(2, '0')} / 04</span>
|
||||
<div key={k} style={{ border: `1px solid ${COLORS.slate200}`, borderTop: `3px solid ${COLORS.indigo600}`, padding: '4mm 5mm', display: 'flex', flexDirection: 'column', minHeight: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '2mm' }}>
|
||||
<div style={{ width: '12mm', height: '12mm', background: COLORS.indigo50, display: 'flex', alignItems: 'center', justifyContent: 'center', color: COLORS.indigo600, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
{Icon && <Icon size={26} strokeWidth={1.5} />}
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: '7pt', fontWeight: 700, color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.12em' }}>{p.kicker}</div>
|
||||
<div style={{ fontSize: '7pt', color: COLORS.slate400, fontVariantNumeric: 'tabular-nums', marginTop: '1mm' }}>{String(i + 1).padStart(2, '0')} / 04</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: '13pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2, letterSpacing: '-0.005em', marginBottom: '3mm' }}>{p.title}</div>
|
||||
<div style={{ fontSize: '13pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2, letterSpacing: '-0.005em', marginBottom: '3mm', marginTop: '1mm' }}>{p.title}</div>
|
||||
<div style={{ fontSize: '8.5pt', color: COLORS.slate700, lineHeight: 1.5, marginBottom: '3mm' }}>{p.body}</div>
|
||||
{p.bullets && <Bullets dense items={p.bullets} />}
|
||||
{p.stat && (
|
||||
<div style={{ marginTop: 'auto', paddingTop: '3mm', borderTop: `1px solid ${COLORS.slate200}`, display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: '7.5pt', color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600 }}>{p.stat.k}</span>
|
||||
<span style={{ fontSize: '11pt', fontWeight: 800, color: COLORS.emerald700, fontVariantNumeric: 'tabular-nums' }}>{p.stat.v}</span>
|
||||
<span style={{ fontSize: '12pt', fontWeight: 800, color: COLORS.emerald700, fontVariantNumeric: 'tabular-nums' }}>{p.stat.v}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -50,28 +68,32 @@ export function PrintUSPPage2({ lang, pageNum, totalPages, versionName }: SlideB
|
||||
return (
|
||||
<Page kicker="05" section={de ? 'USP · 2 / 2' : 'USP · 2 / 2'} title={de ? 'Under the Hood, was die Plattform technisch trägt.' : 'Under the Hood, what the platform is built on.'} subtitle={de ? 'Vier technische Differentiator: Traceability, Continuous Engine, Compliance Optimizer, EU-Trust Stack.' : 'Four technical differentiators: traceability, continuous engine, compliance optimizer, EU trust stack.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6mm' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '5mm' }}>
|
||||
{cards.map(k => {
|
||||
const p = d[k]
|
||||
const Icon = USP_ICON[k]
|
||||
return (
|
||||
<div key={k} style={{ borderLeft: `2px solid ${COLORS.amber600}`, paddingLeft: '5mm', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.amber700, textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: '2mm' }}>{p.kicker}</div>
|
||||
<div style={{ fontSize: '12pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2, marginBottom: '3mm' }}>{p.title}</div>
|
||||
<div style={{ fontSize: '8.5pt', color: COLORS.slate700, lineHeight: 1.5, marginBottom: '3mm' }}>{p.body}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '3mm', marginBottom: '2mm' }}>
|
||||
{Icon && <Icon size={18} strokeWidth={1.5} color={COLORS.amber700} />}
|
||||
<span style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.amber700, textTransform: 'uppercase', letterSpacing: '0.12em' }}>{p.kicker}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '12pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2, marginBottom: '2.5mm' }}>{p.title}</div>
|
||||
<div style={{ fontSize: '8.5pt', color: COLORS.slate700, lineHeight: 1.5, marginBottom: '2.5mm' }}>{p.body}</div>
|
||||
{p.bullets && <Bullets dense items={p.bullets} />}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '6mm' }}>
|
||||
<Callout tone="accent" label={de ? 'Die Schleife' : 'The Loop'}>
|
||||
<div style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900, marginBottom: '2mm' }}>{d.hub.title}</div>
|
||||
<div>{d.hub.body}</div>
|
||||
{d.hub.bullets && (
|
||||
<div style={{ marginTop: '3mm' }}><Bullets dense tone="accent" items={d.hub.bullets} /></div>
|
||||
)}
|
||||
</Callout>
|
||||
<div style={{ marginTop: '5mm', border: `1px solid ${COLORS.indigo600}`, background: COLORS.indigo50, padding: '4mm 5mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '3mm', marginBottom: '2mm' }}>
|
||||
<Infinity size={20} strokeWidth={1.5} color={COLORS.indigo700} />
|
||||
<span style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.indigo700, textTransform: 'uppercase', letterSpacing: '0.14em' }}>{de ? 'Die Schleife' : 'The Loop'}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900, marginBottom: '2mm' }}>{d.hub.title}</div>
|
||||
<div style={{ fontSize: '8.5pt', color: COLORS.slate700, lineHeight: 1.5, marginBottom: '3mm' }}>{d.hub.body}</div>
|
||||
<LoopDiagram lang={lang} />
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
@@ -168,33 +190,37 @@ export function PrintRegulatoryLandscapePage({ lang, pageNum, totalPages, versio
|
||||
|
||||
/* ===== PRODUCT / MODULAR TOOLKIT ===== */
|
||||
|
||||
const MODULE_ICONS: LucideIcon[] = [
|
||||
ScanLine, ShieldCheck, FileText, ClipboardCheck, Users, UserCheck,
|
||||
AlertTriangle, Brain, Target, GraduationCap, TrendingUp, MessageSquare,
|
||||
]
|
||||
const MODULES_FULL_DE = [
|
||||
{ name: 'Code Security', icon: '◇', desc: 'SAST · DAST · SBOM · Container · Secrets · Pentesting', features: ['Bei jedem Push', 'Auto-Fix LLM', 'CI/CD-integriert'] },
|
||||
{ name: 'CE-SW-Risikobeurteilung', icon: '◇', desc: 'CE-Kennzeichnung für Maschinen mit Software-Anteil', features: ['Maschinen-VO', 'CRA-konform', 'Code-Basis-Analyse'] },
|
||||
{ name: 'Compliance-Dokumente', icon: '◇', desc: 'VVT (Art. 30) · TOMs · DSFA (Art. 35) · Löschkonzept', features: ['Auto-Generiert', 'Versionsverlauf', 'Audit-tauglich'] },
|
||||
{ name: 'Audit Manager', icon: '◇', desc: 'Abweichungen End-to-End: Rollen · Stichtage · Eskalation', features: ['Tickets + Nachweise', 'GF-Eskalation', 'Compliance-SLA'] },
|
||||
{ name: 'DSR / Betroffenenrechte', icon: '◇', desc: 'Auskunft, Berichtigung, Löschung, Datenübertragbarkeit', features: ['Self-Service', 'Identitätsprüfung', 'Frist-Tracking'] },
|
||||
{ name: 'Consent', icon: '◇', desc: 'Einwilligungs-Management, Cookie-Banner, ePrivacy', features: ['CMP integriert', 'Audit-Log', 'Multi-Tenant'] },
|
||||
{ name: 'Incident Response', icon: '◇', desc: 'Vorfälle, Meldung (72h), Mitigation, Forensik', features: ['Art. 33/34 DSGVO', 'BSI-Meldepfade', 'Forensik-Hooks'] },
|
||||
{ name: 'Compliance LLM', icon: '◇', desc: 'GPT für Text + Audio, EU-gehostet, mit Quellenangabe', features: ['Self-Hosted', 'EU-souverän', 'Audit-zitierbar'] },
|
||||
{ name: 'Tender Matching', icon: '◇', desc: 'RFQ-Antworten automatisch gegen Codebase + Policies', features: ['Stunden statt Wochen', 'Win-ready', 'Klausel-Mapping'] },
|
||||
{ name: 'Academy', icon: '◇', desc: 'Online-Schulungen für Geschäftsführung und Mitarbeiter', features: ['Mandatory Training', 'Zertifikate', 'GF-Pflicht erfüllt'] },
|
||||
{ name: 'Compliance Optimizer', icon: '◇', desc: 'Maximale KI-Nutzung im legalen Rahmen, ersetzt 20-200k € Anwaltskosten', features: ['ROI-ranking', 'Sweet-Spot', 'Risikobalance'] },
|
||||
{ name: 'Kommunikation', icon: '◇', desc: 'Chat (Matrix) + Video (Jitsi) + KI-Support', features: ['Self-Hosted', 'EU-Hosting', 'Audit-Logs'] },
|
||||
{ name: 'Code Security', desc: 'SAST · DAST · SBOM · Container · Secrets · Pentesting', features: ['Bei jedem Push', 'Auto-Fix LLM', 'CI/CD-integriert'] },
|
||||
{ name: 'CE-SW-Risikobeurteilung', desc: 'CE-Kennzeichnung für Maschinen mit Software-Anteil', features: ['Maschinen-VO', 'CRA-konform', 'Code-Basis-Analyse'] },
|
||||
{ name: 'Compliance-Dokumente', desc: 'VVT (Art. 30) · TOMs · DSFA (Art. 35) · Löschkonzept', features: ['Auto-Generiert', 'Versionsverlauf', 'Audit-tauglich'] },
|
||||
{ name: 'Audit Manager', desc: 'Abweichungen End-to-End: Rollen · Stichtage · Eskalation', features: ['Tickets + Nachweise', 'GF-Eskalation', 'Compliance-SLA'] },
|
||||
{ name: 'DSR / Betroffenenrechte', desc: 'Auskunft, Berichtigung, Löschung, Datenübertragbarkeit', features: ['Self-Service', 'Identitätsprüfung', 'Frist-Tracking'] },
|
||||
{ name: 'Consent', desc: 'Einwilligungs-Management, Cookie-Banner, ePrivacy', features: ['CMP integriert', 'Audit-Log', 'Multi-Tenant'] },
|
||||
{ name: 'Incident Response', desc: 'Vorfälle, Meldung (72h), Mitigation, Forensik', features: ['Art. 33/34 DSGVO', 'BSI-Meldepfade', 'Forensik-Hooks'] },
|
||||
{ name: 'Compliance LLM', desc: 'GPT für Text + Audio, EU-gehostet, mit Quellenangabe', features: ['Self-Hosted', 'EU-souverän', 'Audit-zitierbar'] },
|
||||
{ name: 'Tender Matching', desc: 'RFQ-Antworten automatisch gegen Codebase + Policies', features: ['Stunden statt Wochen', 'Win-ready', 'Klausel-Mapping'] },
|
||||
{ name: 'Academy', desc: 'Online-Schulungen für Geschäftsführung und Mitarbeiter', features: ['Mandatory Training', 'Zertifikate', 'GF-Pflicht erfüllt'] },
|
||||
{ name: 'Compliance Optimizer', desc: 'Maximale KI-Nutzung im legalen Rahmen, ersetzt 20-200k € Anwaltskosten', features: ['ROI-ranking', 'Sweet-Spot', 'Risikobalance'] },
|
||||
{ name: 'Kommunikation', desc: 'Chat (Matrix) + Video (Jitsi) + KI-Support', features: ['Self-Hosted', 'EU-Hosting', 'Audit-Logs'] },
|
||||
]
|
||||
const MODULES_FULL_EN = [
|
||||
{ name: 'Code Security', icon: '◇', desc: 'SAST · DAST · SBOM · Container · Secrets · Pentesting', features: ['Every push', 'Auto-fix LLM', 'CI/CD integrated'] },
|
||||
{ name: 'CE SW Risk Assessment', icon: '◇', desc: 'CE marking for machinery with software', features: ['Machinery Reg.', 'CRA-compliant', 'Code-level analysis'] },
|
||||
{ name: 'Compliance Documents', icon: '◇', desc: 'RoPA (Art. 30) · TOMs · DPIA (Art. 35) · Retention', features: ['Auto-generated', 'Version history', 'Audit-ready'] },
|
||||
{ name: 'Audit Manager', icon: '◇', desc: 'Deviations end-to-end: roles · deadlines · escalation', features: ['Tickets + evidence', 'Mgmt escalation', 'Compliance SLA'] },
|
||||
{ name: 'DSR / Data Subject Rights', icon: '◇', desc: 'Access, rectification, erasure, portability', features: ['Self-service', 'Identity check', 'Deadline tracking'] },
|
||||
{ name: 'Consent', icon: '◇', desc: 'Consent mgmt, cookie banner, ePrivacy', features: ['CMP integrated', 'Audit log', 'Multi-tenant'] },
|
||||
{ name: 'Incident Response', icon: '◇', desc: 'Breaches, reporting (72h), mitigation, forensics', features: ['GDPR Art. 33/34', 'BSI channels', 'Forensic hooks'] },
|
||||
{ name: 'Compliance LLM', icon: '◇', desc: 'GPT for text + audio, EU-hosted, with citations', features: ['Self-hosted', 'EU-sovereign', 'Audit-citable'] },
|
||||
{ name: 'Tender Matching', icon: '◇', desc: 'RFQ answers automatically against codebase + policies', features: ['Hours not weeks', 'Win-ready', 'Clause mapping'] },
|
||||
{ name: 'Academy', icon: '◇', desc: 'Online training for management and staff', features: ['Mandatory training', 'Certificates', 'Mgmt duties fulfilled'] },
|
||||
{ name: 'Compliance Optimizer', icon: '◇', desc: 'Max AI use within legal limits, replaces €20-200k legal fees', features: ['ROI ranking', 'Sweet spot', 'Risk balance'] },
|
||||
{ name: 'Communication', icon: '◇', desc: 'Chat (Matrix) + video (Jitsi) + AI support', features: ['Self-hosted', 'EU hosting', 'Audit logs'] },
|
||||
{ name: 'Code Security', desc: 'SAST · DAST · SBOM · Container · Secrets · Pentesting', features: ['Every push', 'Auto-fix LLM', 'CI/CD integrated'] },
|
||||
{ name: 'CE SW Risk Assessment', desc: 'CE marking for machinery with software', features: ['Machinery Reg.', 'CRA-compliant', 'Code-level analysis'] },
|
||||
{ name: 'Compliance Documents', desc: 'RoPA (Art. 30) · TOMs · DPIA (Art. 35) · Retention', features: ['Auto-generated', 'Version history', 'Audit-ready'] },
|
||||
{ name: 'Audit Manager', desc: 'Deviations end-to-end: roles · deadlines · escalation', features: ['Tickets + evidence', 'Mgmt escalation', 'Compliance SLA'] },
|
||||
{ name: 'DSR / Data Subject Rights', desc: 'Access, rectification, erasure, portability', features: ['Self-service', 'Identity check', 'Deadline tracking'] },
|
||||
{ name: 'Consent', desc: 'Consent mgmt, cookie banner, ePrivacy', features: ['CMP integrated', 'Audit log', 'Multi-tenant'] },
|
||||
{ name: 'Incident Response', desc: 'Breaches, reporting (72h), mitigation, forensics', features: ['GDPR Art. 33/34', 'BSI channels', 'Forensic hooks'] },
|
||||
{ name: 'Compliance LLM', desc: 'GPT for text + audio, EU-hosted, with citations', features: ['Self-hosted', 'EU-sovereign', 'Audit-citable'] },
|
||||
{ name: 'Tender Matching', desc: 'RFQ answers automatically against codebase + policies', features: ['Hours not weeks', 'Win-ready', 'Clause mapping'] },
|
||||
{ name: 'Academy', desc: 'Online training for management and staff', features: ['Mandatory training', 'Certificates', 'Mgmt duties fulfilled'] },
|
||||
{ name: 'Compliance Optimizer', desc: 'Max AI use within legal limits, replaces €20-200k legal fees', features: ['ROI ranking', 'Sweet spot', 'Risk balance'] },
|
||||
{ name: 'Communication', desc: 'Chat (Matrix) + video (Jitsi) + AI support', features: ['Self-hosted', 'EU hosting', 'Audit logs'] },
|
||||
]
|
||||
|
||||
export function PrintProductPage({ products, lang, pageNum, totalPages, versionName }: SlideBase & { products: PitchProduct[] }) {
|
||||
@@ -205,16 +231,22 @@ export function PrintProductPage({ products, lang, pageNum, totalPages, versionN
|
||||
<Page kicker="07" section={de ? 'MODULARER BAUKASTEN' : 'MODULAR TOOLKIT'} title={de ? '12 Module, einzeln oder als Bundle.' : '12 modules, individual or bundled.'} subtitle={de ? 'Kunden wählen die Module, die sie brauchen. Jedes Modul liefert eigene Compliance-Werte; die Plattform integriert sie zu einem Audit-Trail.' : 'Customers pick the modules they need. Each module delivers compliance value on its own; the platform unifies them into a single audit trail.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm', flex: 1, minHeight: 0 }}>
|
||||
{modules.map((m, i) => (
|
||||
<div key={i} style={{ border: `1px solid ${COLORS.slate200}`, borderTop: `2px solid ${COLORS.indigo600}`, padding: '3mm 4mm', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '1mm' }}>
|
||||
<div style={{ fontSize: '10pt', fontWeight: 700, color: COLORS.slate900 }}>{m.name}</div>
|
||||
<div style={{ fontSize: '7pt', color: COLORS.slate400, fontVariantNumeric: 'tabular-nums' }}>{String(i + 1).padStart(2, '0')}</div>
|
||||
{modules.map((m, i) => {
|
||||
const Icon = MODULE_ICONS[i]
|
||||
return (
|
||||
<div key={i} style={{ border: `1px solid ${COLORS.slate200}`, borderTop: `2px solid ${COLORS.indigo600}`, padding: '3mm 4mm', display: 'flex', flexDirection: 'column', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '3mm', marginBottom: '1.5mm' }}>
|
||||
<div style={{ width: '8mm', height: '8mm', background: COLORS.indigo50, display: 'flex', alignItems: 'center', justifyContent: 'center', color: COLORS.indigo600, flexShrink: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
|
||||
<Icon size={16} strokeWidth={1.5} />
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0, fontSize: '10pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.15 }}>{m.name}</div>
|
||||
<div style={{ fontSize: '7pt', color: COLORS.slate400, fontVariantNumeric: 'tabular-nums' }}>{String(i + 1).padStart(2, '0')}</div>
|
||||
</div>
|
||||
<div style={{ fontSize: '8pt', color: COLORS.slate600, lineHeight: 1.4, marginBottom: '2mm' }}>{m.desc}</div>
|
||||
<div style={{ marginTop: 'auto', borderTop: `1px solid ${COLORS.slate100}`, paddingTop: '2mm', fontSize: '7pt', color: COLORS.slate500 }}>{m.features.join(' · ')}</div>
|
||||
</div>
|
||||
<div style={{ fontSize: '8pt', color: COLORS.slate600, lineHeight: 1.45, marginBottom: '2mm' }}>{m.desc}</div>
|
||||
<div style={{ marginTop: 'auto', borderTop: `1px solid ${COLORS.slate100}`, paddingTop: '2mm', fontSize: '7pt', color: COLORS.slate500 }}>{m.features.join(' · ')}</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '4mm', flexShrink: 0 }}>
|
||||
@@ -249,18 +281,8 @@ export function PrintHowItWorksPage({ lang, pageNum, totalPages, versionName }:
|
||||
return (
|
||||
<Page kicker="08" section={de ? 'SO FUNKTIONIERT\'S' : 'HOW IT WORKS'} title={de ? 'In 4 Schritten zur kontinuierlichen Compliance.' : 'Continuous compliance in 4 steps.'} subtitle={de ? 'Vom Vertrag bis zur Audit-Bereitschaft in der Regel <30 Tage. Kein Excel, kein Pentest-Vendor, keine manuelle Dokumentenpflege.' : 'From contract to audit-ready typically in <30 days. No Excel, no pentest vendor, no manual document maintenance.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
|
||||
|
||||
{/* horizontal step flow */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '6mm', flex: 1, minHeight: 0 }}>
|
||||
{steps.map((s, i) => (
|
||||
<div key={i} style={{ display: 'flex', flexDirection: 'column', position: 'relative' }}>
|
||||
{i < steps.length - 1 && (
|
||||
<div style={{ position: 'absolute', top: '14mm', right: '-4mm', width: '2mm', height: '1px', background: COLORS.slate300 }} />
|
||||
)}
|
||||
<div style={{ fontSize: '32pt', fontWeight: 800, color: COLORS.indigo600, lineHeight: 1, marginBottom: '4mm', fontVariantNumeric: 'tabular-nums' }}>{s.n}</div>
|
||||
<div style={{ fontSize: '13pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2, marginBottom: '3mm', letterSpacing: '-0.005em' }}>{s.t}</div>
|
||||
<div style={{ fontSize: '9pt', color: COLORS.slate700, lineHeight: 1.55, flex: 1 }}>{s.d}</div>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ flex: 1, minHeight: 0, display: 'flex', alignItems: 'stretch' }}>
|
||||
<StepStrip steps={steps} />
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '6mm', flexShrink: 0 }}>
|
||||
|
||||
@@ -65,29 +65,28 @@ export default async function PitchPrintPage({ params, searchParams }: Ctx) {
|
||||
fp_scenarios: (map.fm_scenarios || []) as FpScenarioRef[],
|
||||
}
|
||||
|
||||
// Financial variant: fetch FM results + parse assumptions
|
||||
// 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[] = []
|
||||
|
||||
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<Record<string, unknown>>
|
||||
fmAssumptions = rawAssumptions.map(a => ({
|
||||
...a,
|
||||
value: typeof a.value === 'string' ? JSON.parse(a.value as string) : a.value,
|
||||
})) as 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}
|
||||
|
||||
Reference in New Issue
Block a user