From 6c022d1a79ae70485ca658f1b41eb9dadfe0a3c8 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Mon, 4 May 2026 14:27:07 +0200 Subject: [PATCH 1/9] fix: allow investors to query fp_ scenarios by scenarioId AssumptionsSlide sends ?scenarioId= for Bear/Base/Bull cards but the route was silently dropping it for non-admin requests, making all three cards return the same default Base Case data. Since fp_ financial projections are already investor-facing, any valid scenarioId is allowed. Co-Authored-By: Claude Sonnet 4.6 --- pitch-deck/app/api/finanzplan/[sheetName]/route.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pitch-deck/app/api/finanzplan/[sheetName]/route.ts b/pitch-deck/app/api/finanzplan/[sheetName]/route.ts index 6893af9..b9d6ad0 100644 --- a/pitch-deck/app/api/finanzplan/[sheetName]/route.ts +++ b/pitch-deck/app/api/finanzplan/[sheetName]/route.ts @@ -48,9 +48,7 @@ export async function GET( return NextResponse.json({ error: `Unknown sheet: ${sheetName}` }, { status: 400 }) } - // Only admin callers may query an arbitrary scenarioId; investors always see the default - const isAdmin = validateAdminSecret(request) - const scenarioId = isAdmin ? request.nextUrl.searchParams.get('scenarioId') : null + const scenarioId = request.nextUrl.searchParams.get('scenarioId') try { let query = `SELECT * FROM ${table}` From 06014d57b3ba52f2c2df9b2847345208940704ae Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Mon, 4 May 2026 15:00:06 +0200 Subject: [PATCH 2/9] fix: derive fp_scenario IDs from version snapshot, eliminate hardcoded UUIDs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fm_scenarios array in each pitch version snapshot already stores the fp_scenario IDs directly (same pattern 1 Mio used). Wandeldarlehen snapshots were missing Bear/Bull entries — updated in DB to add them. - /api/data: include fp_scenarios in version response (was omitted) - PitchDeck: derive fpBaseScenarioId from data.fp_scenarios - useFpKPIs: accept fpBaseScenarioId instead of isWandeldarlehen boolean - AssumptionsSlide: find Bear/Base/Bull by name from fpScenarios prop - FinanzplanSlide: initialize from fpBaseScenarioId, use version scenarios for selector - FinancialsSlide / ExecutiveSummarySlide: pass fpBaseScenarioId to hook - types: add FpScenarioRef + fp_scenarios field to PitchData No UUID hardcoded in any component. Adding a new pitch version only requires setting the correct fp_scenario IDs in its fm_scenarios snapshot. Co-Authored-By: Claude Sonnet 4.6 --- pitch-deck/app/api/data/route.ts | 1 + pitch-deck/components/PitchDeck.tsx | 20 ++++++++----------- .../components/slides/AssumptionsSlide.tsx | 18 +++++++++++------ .../slides/ExecutiveSummarySlide.tsx | 5 +++-- .../components/slides/FinancialsSlide.tsx | 5 +++-- .../components/slides/FinanzplanSlide.tsx | 19 ++++++++++-------- pitch-deck/lib/hooks/useFpKPIs.ts | 10 +++++----- pitch-deck/lib/types.ts | 9 +++++++++ 8 files changed, 52 insertions(+), 35 deletions(-) diff --git a/pitch-deck/app/api/data/route.ts b/pitch-deck/app/api/data/route.ts index cffd15a..0be22e3 100644 --- a/pitch-deck/app/api/data/route.ts +++ b/pitch-deck/app/api/data/route.ts @@ -39,6 +39,7 @@ export async function GET() { metrics: map.metrics || [], funding: (map.funding || [])[0] || null, products: map.products || [], + fp_scenarios: map.fm_scenarios || [], }) } diff --git a/pitch-deck/components/PitchDeck.tsx b/pitch-deck/components/PitchDeck.tsx index f744a3e..ff13c67 100644 --- a/pitch-deck/components/PitchDeck.tsx +++ b/pitch-deck/components/PitchDeck.tsx @@ -72,14 +72,10 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, const [fabOpen, setFabOpen] = useState(false) const isWandeldarlehen = (data?.funding?.instrument || '').toLowerCase() === 'wandeldarlehen' - // For version previews: use the version's default FM scenario instead of base table default - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const fmScenarios = (previewData as any)?.fm_scenarios as Array<{ id: string; is_default?: boolean }> | undefined - const preferredScenarioId = fmScenarios?.[0]?.is_default - ? fmScenarios[0].id - : fmScenarios?.length === 1 - ? fmScenarios[0].id - : null + // Derive fp_scenario IDs from version snapshot (fm_scenarios stores fp_scenario IDs directly) + const fpScenarios = data?.fp_scenarios || [] + const fpBaseScenarioId = fpScenarios.find(s => s.is_default)?.id ?? fpScenarios[0]?.id ?? null + const preferredScenarioId = fpBaseScenarioId // Skip cap-table slide for Wandeldarlehen versions useEffect(() => { @@ -163,7 +159,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, /> ) case 'executive-summary': - return + return case 'cover': return case 'problem': @@ -189,7 +185,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, case 'team': return case 'financials': - return + return case 'the-ask': return case 'cap-table': @@ -200,7 +196,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, case 'ai-qa': return case 'annex-assumptions': - return + return case 'annex-architecture': return case 'annex-gtm': @@ -216,7 +212,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, case 'annex-strategy': return case 'annex-finanzplan': - return + return case 'annex-glossary': return case 'risks': diff --git a/pitch-deck/components/slides/AssumptionsSlide.tsx b/pitch-deck/components/slides/AssumptionsSlide.tsx index 2d8b525..d9af730 100644 --- a/pitch-deck/components/slides/AssumptionsSlide.tsx +++ b/pitch-deck/components/slides/AssumptionsSlide.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useState } from 'react' -import { Language } from '@/lib/types' +import { Language, FpScenarioRef } from '@/lib/types' import { t } from '@/lib/i18n' import GradientText from '../ui/GradientText' import FadeInView from '../ui/FadeInView' @@ -13,6 +13,7 @@ interface AssumptionsSlideProps { investorId?: string | null preferredScenarioId?: string | null isWandeldarlehen?: boolean + fpScenarios?: FpScenarioRef[] } interface SheetRow { @@ -72,7 +73,7 @@ async function loadScenarioKPIs(scenarioId: string | null): Promise { async function load() { - const baseId = isWandeldarlehen ? 'c0000000-0000-0000-0000-000000000200' : null - const bearId = isWandeldarlehen ? 'd0000000-0000-0000-0000-000000000201' : 'd0000000-0000-0000-0000-000000000301' - const bullId = isWandeldarlehen ? 'd0000000-0000-0000-0000-000000000202' : 'd0000000-0000-0000-0000-000000000302' + const scenarios = fpScenarios || [] + const find = (role: 'bear' | 'bull' | 'base') => { + if (role === 'base') return scenarios.find(s => s.is_default)?.id ?? null + return scenarios.find(s => s.name.toLowerCase().includes(role))?.id ?? null + } + const baseId = find('base') + const bearId = find('bear') + const bullId = find('bull') const [bear, base, bull] = await Promise.all([loadScenarioKPIs(bearId), loadScenarioKPIs(baseId), loadScenarioKPIs(bullId)]) setScenarioData({ bear, base, bull }) } load() - }, [isWandeldarlehen]) + }, [fpScenarios]) const bear = scenarioData?.bear || { arr: 0, customers: 0, headcount: 0, cash: 0, breakEvenYear: '—' } const base = scenarioData?.base || { arr: 0, customers: 0, headcount: 0, cash: 0, breakEvenYear: '—' } diff --git a/pitch-deck/components/slides/ExecutiveSummarySlide.tsx b/pitch-deck/components/slides/ExecutiveSummarySlide.tsx index 6b3fb99..a73f24d 100644 --- a/pitch-deck/components/slides/ExecutiveSummarySlide.tsx +++ b/pitch-deck/components/slides/ExecutiveSummarySlide.tsx @@ -16,15 +16,16 @@ interface ExecutiveSummarySlideProps { investorId?: string | null preferredScenarioId?: string | null isWandeldarlehen?: boolean + fpBaseScenarioId?: string | null } -export default function ExecutiveSummarySlide({ lang, data, investorId, preferredScenarioId, isWandeldarlehen }: ExecutiveSummarySlideProps) { +export default function ExecutiveSummarySlide({ lang, data, investorId, preferredScenarioId, isWandeldarlehen, fpBaseScenarioId }: ExecutiveSummarySlideProps) { const i = t(lang) const es = i.executiveSummary const de = lang === 'de' // Unternehmensentwicklung from fp_* tables (source of truth) - const { kpis: fpKPIs } = useFpKPIs(isWandeldarlehen) + const { kpis: fpKPIs } = useFpKPIs(fpBaseScenarioId) // Pipeline stats from DB const [pipelineStats, setPipelineStats] = useState>({}) diff --git a/pitch-deck/components/slides/FinancialsSlide.tsx b/pitch-deck/components/slides/FinancialsSlide.tsx index f067acf..45187b3 100644 --- a/pitch-deck/components/slides/FinancialsSlide.tsx +++ b/pitch-deck/components/slides/FinancialsSlide.tsx @@ -25,9 +25,10 @@ interface FinancialsSlideProps { investorId: string | null preferredScenarioId?: string | null isWandeldarlehen?: boolean + fpBaseScenarioId?: string | null } -export default function FinancialsSlide({ lang, investorId, preferredScenarioId, isWandeldarlehen }: FinancialsSlideProps) { +export default function FinancialsSlide({ lang, investorId, preferredScenarioId, isWandeldarlehen, fpBaseScenarioId }: FinancialsSlideProps) { const i = t(lang) const fm = useFinancialModel(investorId, preferredScenarioId) const [activeTab, setActiveTab] = useState('overview') @@ -38,7 +39,7 @@ export default function FinancialsSlide({ lang, investorId, preferredScenarioId, const lastResult = activeResults?.results[activeResults.results.length - 1] // KPI cards from fp_* tables (source of truth) - const { last: fpLast, kpis: fpKPIs } = useFpKPIs(isWandeldarlehen) + const { last: fpLast, kpis: fpKPIs } = useFpKPIs(fpBaseScenarioId) const kpiArr = fpLast?.arr || summary?.final_arr || 0 const kpiCustomers = fpLast?.customers || summary?.final_customers || 0 const kpiEbit = fpKPIs?.y2029?.ebit // First profitable year diff --git a/pitch-deck/components/slides/FinanzplanSlide.tsx b/pitch-deck/components/slides/FinanzplanSlide.tsx index 7d68a9c..af98b00 100644 --- a/pitch-deck/components/slides/FinanzplanSlide.tsx +++ b/pitch-deck/components/slides/FinanzplanSlide.tsx @@ -1,7 +1,7 @@ 'use client' import { useCallback, useEffect, useState } from 'react' -import { Language } from '@/lib/types' +import { Language, FpScenarioRef } from '@/lib/types' import ProjectionFooter from '../ui/ProjectionFooter' import GradientText from '../ui/GradientText' import FadeInView from '../ui/FadeInView' @@ -20,14 +20,16 @@ interface FinanzplanSlideProps { investorId?: string | null preferredScenarioId?: string | null isWandeldarlehen?: boolean + fpBaseScenarioId?: string | null + fpScenarios?: FpScenarioRef[] } -export default function FinanzplanSlide({ lang, investorId, preferredScenarioId, isWandeldarlehen }: FinanzplanSlideProps) { +export default function FinanzplanSlide({ lang, investorId, preferredScenarioId, isWandeldarlehen, fpBaseScenarioId, fpScenarios }: FinanzplanSlideProps) { const [sheets, setSheets] = useState([]) const [scenarios, setScenarios] = useState([]) const [openCats, setOpenCats] = useState>(new Set()) const toggleCat = (cat: string) => setOpenCats(prev => { const n = new Set(prev); n.has(cat) ? n.delete(cat) : n.add(cat); return n }) - const [selectedScenarioId, setSelectedScenarioId] = useState('') + const [selectedScenarioId, setSelectedScenarioId] = useState(fpBaseScenarioId ?? '') const [activeSheet, setActiveSheet] = useState('guv') const [rows, setRows] = useState([]) const [loading, setLoading] = useState(false) @@ -96,18 +98,19 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId, loadKPIs() }, [selectedScenarioId]) - // Load sheet list + scenarios + // Load sheet list; populate scenario selector from version data or API fallback useEffect(() => { fetch('/api/finanzplan', { cache: 'no-store' }) .then(r => r.json()) .then(data => { setSheets(data.sheets || []) - const scens: FpScenario[] = data.scenarios || [] + // Use version fp_scenarios if available, else fall back to API list + const scens: FpScenario[] = fpScenarios && fpScenarios.length > 0 + ? fpScenarios.map(s => ({ id: s.id, name: s.name, is_default: s.is_default ?? false, color: s.color ?? '#6366f1', description: s.description ?? '' })) + : data.scenarios || [] setScenarios(scens) - // Pick scenario: Wandeldarlehen version → WD scenario, otherwise default if (!selectedScenarioId) { - const wdScenario = isWandeldarlehen ? scens.find(s => s.name.toLowerCase().includes('wandeldarlehen') && !s.name.toLowerCase().includes('bear') && !s.name.toLowerCase().includes('bull')) : null - const def = wdScenario ?? scens.find(s => s.is_default) ?? scens[0] + const def = scens.find(s => s.is_default) ?? scens[0] if (def) setSelectedScenarioId(def.id) } }) diff --git a/pitch-deck/lib/hooks/useFpKPIs.ts b/pitch-deck/lib/hooks/useFpKPIs.ts index 9158971..8f18adb 100644 --- a/pitch-deck/lib/hooks/useFpKPIs.ts +++ b/pitch-deck/lib/hooks/useFpKPIs.ts @@ -31,14 +31,14 @@ interface SheetRow { * Loads annual KPIs directly from fp_* tables (source of truth). * Returns a map of year keys (y2026-y2030) to KPI objects. */ -export function useFpKPIs(isWandeldarlehen?: boolean) { +export function useFpKPIs(fpBaseScenarioId?: string | null) { const [kpis, setKpis] = useState>({}) const [loading, setLoading] = useState(true) useEffect(() => { async function load() { try { - const param = isWandeldarlehen ? '?scenarioId=c0000000-0000-0000-0000-000000000200' : '' + const param = fpBaseScenarioId ? `?scenarioId=${fpBaseScenarioId}` : '' const [guvRes, liqRes, persRes, kundenRes] = await Promise.all([ fetch(`/api/finanzplan/guv${param}`, { cache: 'no-store' }), fetch(`/api/finanzplan/liquiditaet${param}`, { cache: 'no-store' }), @@ -102,7 +102,7 @@ export function useFpKPIs(isWandeldarlehen?: boolean) { setLoading(false) } load() - }, [isWandeldarlehen]) + }, [fpBaseScenarioId]) // Use of Funds: compute spending breakdown m8-m24 (funding period) const [useOfFunds, setUseOfFunds] = useState>([]) @@ -110,7 +110,7 @@ export function useFpKPIs(isWandeldarlehen?: boolean) { useEffect(() => { async function loadUoF() { try { - const param = isWandeldarlehen ? '?scenarioId=c0000000-0000-0000-0000-000000000200' : '' + const param = fpBaseScenarioId ? `?scenarioId=${fpBaseScenarioId}` : '' const [persRes, betriebRes, investRes] = await Promise.all([ fetch(`/api/finanzplan/personalkosten${param}`, { cache: 'no-store' }), fetch(`/api/finanzplan/betriebliche${param}`, { cache: 'no-store' }), @@ -168,7 +168,7 @@ export function useFpKPIs(isWandeldarlehen?: boolean) { } catch { /* ignore */ } } loadUoF() - }, [isWandeldarlehen]) + }, [fpBaseScenarioId]) const last = kpis.y2030 return { kpis, loading, last, useOfFunds } diff --git a/pitch-deck/lib/types.ts b/pitch-deck/lib/types.ts index 17d17bf..cc1eae4 100644 --- a/pitch-deck/lib/types.ts +++ b/pitch-deck/lib/types.ts @@ -114,6 +114,14 @@ export interface PitchProduct { operating_cost_eur: number } +export interface FpScenarioRef { + id: string + name: string + is_default?: boolean + color?: string + description?: string +} + export interface PitchData { company: PitchCompany team: PitchTeamMember[] @@ -125,6 +133,7 @@ export interface PitchData { metrics: PitchMetric[] funding: PitchFunding products: PitchProduct[] + fp_scenarios?: FpScenarioRef[] } // Financial Model Types From f2184be02fd2da9dcafd8cb0e1ce883d0035f581 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Mon, 4 May 2026 15:21:40 +0200 Subject: [PATCH 3/9] fix: tab row counts use investor's scenario, not always Base Case /api/finanzplan now accepts ?scenarioId and uses it for the per-sheet row counts (the numbers in brackets on the tab bar). FinanzplanSlide passes fpBaseScenarioId when fetching the sheet list, so Wandeldarlehen investors see e.g. Personalkosten (9) instead of (35). Co-Authored-By: Claude Sonnet 4.6 --- pitch-deck/app/api/finanzplan/route.ts | 18 +++++++++++++----- .../components/slides/FinanzplanSlide.tsx | 3 ++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pitch-deck/app/api/finanzplan/route.ts b/pitch-deck/app/api/finanzplan/route.ts index f975aae..49ba781 100644 --- a/pitch-deck/app/api/finanzplan/route.ts +++ b/pitch-deck/app/api/finanzplan/route.ts @@ -3,25 +3,33 @@ import pool from '@/lib/db' import { SHEET_LIST } from '@/lib/finanzplan/types' export async function GET(request: NextRequest) { - // Only expose scenario list to admin callers (bearer token) + // Only expose full scenario list to admin callers (bearer token) const secret = process.env.PITCH_ADMIN_SECRET const auth = request.headers.get('authorization') ?? '' const isAdmin = secret && auth === `Bearer ${secret}` + // Allow callers to pass a scenarioId for row counts (e.g. investor's assigned scenario) + const scenarioId = request.nextUrl.searchParams.get('scenarioId') + try { // Investors see only the default scenario — no names of other scenarios leaked const scenarios = isAdmin ? await pool.query('SELECT * FROM fp_scenarios ORDER BY is_default DESC, name') : await pool.query('SELECT id FROM fp_scenarios WHERE is_default = true LIMIT 1') - // Get row counts per sheet + // Get row counts per sheet using the caller's scenario const sheets = await Promise.all( SHEET_LIST.map(async (s) => { const tableName = `fp_${s.name}` try { - const { rows } = await pool.query( - `SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE is_editable = true) as editable FROM ${tableName} WHERE scenario_id = (SELECT id FROM fp_scenarios WHERE is_default = true LIMIT 1)` - ) + const { rows } = scenarioId + ? await pool.query( + `SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE is_editable = true) as editable FROM ${tableName} WHERE scenario_id = $1`, + [scenarioId] + ) + : await pool.query( + `SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE is_editable = true) as editable FROM ${tableName} WHERE scenario_id = (SELECT id FROM fp_scenarios WHERE is_default = true LIMIT 1)` + ) return { ...s, rows: parseInt(rows[0]?.total || '0'), editable_rows: parseInt(rows[0]?.editable || '0') } } catch { return s diff --git a/pitch-deck/components/slides/FinanzplanSlide.tsx b/pitch-deck/components/slides/FinanzplanSlide.tsx index af98b00..f9914f8 100644 --- a/pitch-deck/components/slides/FinanzplanSlide.tsx +++ b/pitch-deck/components/slides/FinanzplanSlide.tsx @@ -100,7 +100,8 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId, // Load sheet list; populate scenario selector from version data or API fallback useEffect(() => { - fetch('/api/finanzplan', { cache: 'no-store' }) + const listParam = fpBaseScenarioId ? `?scenarioId=${fpBaseScenarioId}` : '' + fetch(`/api/finanzplan${listParam}`, { cache: 'no-store' }) .then(r => r.json()) .then(data => { setSheets(data.sheets || []) From 30a916549787eba86070d7ec98b9c36e695d21d3 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Mon, 4 May 2026 22:41:15 +0200 Subject: [PATCH 4/9] =?UTF-8?q?feat(pitch):=20showcase=20mode=20=E2=80=94?= =?UTF-8?q?=20per-investor=20toggle=20hides=20financial/investor=20slides?= =?UTF-8?q?=20for=20customer=20demos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds is_showcase boolean to pitch_investors; when set, filters out financials, the ask, cap table, assumptions, finanzplan, risks, and intro-presenter slides. Slide navigation is fully dynamic — progress bar and counts update accordingly. Co-Authored-By: Claude Sonnet 4.6 --- .../app/api/admin/investors/[id]/route.ts | 17 ++++++---- pitch-deck/app/api/auth/me/route.ts | 2 +- .../(authed)/investors/[id]/page.tsx | 32 +++++++++++++++++-- pitch-deck/components/PitchDeck.tsx | 12 +++++-- pitch-deck/lib/hooks/useAuth.ts | 1 + pitch-deck/lib/hooks/useSlideNavigation.ts | 20 ++++++------ pitch-deck/lib/slide-order.ts | 12 +++++++ 7 files changed, 74 insertions(+), 22 deletions(-) diff --git a/pitch-deck/app/api/admin/investors/[id]/route.ts b/pitch-deck/app/api/admin/investors/[id]/route.ts index 15ef3cf..343b489 100644 --- a/pitch-deck/app/api/admin/investors/[id]/route.ts +++ b/pitch-deck/app/api/admin/investors/[id]/route.ts @@ -19,7 +19,7 @@ export async function GET(request: NextRequest, ctx: RouteContext) { pool.query( `SELECT i.id, i.email, i.name, i.company, i.status, i.last_login_at, i.login_count, i.created_at, i.updated_at, i.first_activity_at, i.data_masked_at, - i.assigned_version_id, + i.assigned_version_id, i.is_showcase, v.name AS version_name, v.status AS version_status FROM pitch_investors i LEFT JOIN pitch_versions v ON v.id = i.assigned_version_id @@ -68,14 +68,14 @@ export async function PATCH(request: NextRequest, ctx: RouteContext) { const { id } = await ctx.params const body = await request.json().catch(() => ({})) - const { name, company, assigned_version_id } = body + const { name, company, assigned_version_id, is_showcase } = body - if (name === undefined && company === undefined && assigned_version_id === undefined) { - return NextResponse.json({ error: 'name, company, or assigned_version_id required' }, { status: 400 }) + if (name === undefined && company === undefined && assigned_version_id === undefined && is_showcase === undefined) { + return NextResponse.json({ error: 'name, company, assigned_version_id, or is_showcase required' }, { status: 400 }) } const before = await pool.query( - `SELECT name, company, assigned_version_id FROM pitch_investors WHERE id = $1`, + `SELECT name, company, assigned_version_id, is_showcase FROM pitch_investors WHERE id = $1`, [id], ) if (before.rows.length === 0) { @@ -99,15 +99,18 @@ export async function PATCH(request: NextRequest, ctx: RouteContext) { // Use null to clear version assignment, undefined to leave unchanged const versionValue = assigned_version_id === undefined ? before.rows[0].assigned_version_id : (assigned_version_id || null) + const showcaseValue = is_showcase !== undefined ? Boolean(is_showcase) : before.rows[0].is_showcase + const { rows } = await pool.query( `UPDATE pitch_investors SET name = COALESCE($1, name), company = COALESCE($2, company), assigned_version_id = $4, + is_showcase = $5, updated_at = NOW() WHERE id = $3 - RETURNING id, email, name, company, status, assigned_version_id`, - [name ?? null, company ?? null, id, versionValue], + RETURNING id, email, name, company, status, assigned_version_id, is_showcase`, + [name ?? null, company ?? null, id, versionValue, showcaseValue], ) const action = assigned_version_id !== undefined && assigned_version_id !== before.rows[0].assigned_version_id diff --git a/pitch-deck/app/api/auth/me/route.ts b/pitch-deck/app/api/auth/me/route.ts index 5d053d8..55dd2ec 100644 --- a/pitch-deck/app/api/auth/me/route.ts +++ b/pitch-deck/app/api/auth/me/route.ts @@ -14,7 +14,7 @@ export async function GET() { } const { rows } = await pool.query( - `SELECT id, email, name, company, status, last_login_at, login_count, created_at + `SELECT id, email, name, company, status, last_login_at, login_count, created_at, is_showcase FROM pitch_investors WHERE id = $1`, [session.sub] ) diff --git a/pitch-deck/app/pitch-admin/(authed)/investors/[id]/page.tsx b/pitch-deck/app/pitch-admin/(authed)/investors/[id]/page.tsx index 385c33f..640bfb8 100644 --- a/pitch-deck/app/pitch-admin/(authed)/investors/[id]/page.tsx +++ b/pitch-deck/app/pitch-admin/(authed)/investors/[id]/page.tsx @@ -21,6 +21,7 @@ interface InvestorDetail { assigned_version_id: string | null version_name: string | null version_status: string | null + is_showcase: boolean } sessions: Array<{ id: string @@ -293,7 +294,7 @@ export default function InvestorDetailPage() { {/* Version assignment */}

Pitch Version

-
+
{inv.assigned_version_id - ? `Investor sees version "${inv.version_name || ''}"` - : 'Investor sees default pitch data'} + ? `Sees version "${inv.version_name || ''}"` + : 'Sees default pitch data'}
+ + {/* Showcase toggle */} +
+
+
Showcase mode
+
Hides financials, The Ask, and investor-only slides — for customer demos
+
+ +
{/* Audit log for this investor */} diff --git a/pitch-deck/components/PitchDeck.tsx b/pitch-deck/components/PitchDeck.tsx index ff13c67..c59fc57 100644 --- a/pitch-deck/components/PitchDeck.tsx +++ b/pitch-deck/components/PitchDeck.tsx @@ -2,7 +2,8 @@ import { useCallback, useEffect, useState } from 'react' import { AnimatePresence } from 'framer-motion' -import { useSlideNavigation } from '@/lib/hooks/useSlideNavigation' +import { useSlideNavigation, SLIDE_ORDER } from '@/lib/hooks/useSlideNavigation' +import { SHOWCASE_HIDDEN_SLIDES } from '@/lib/slide-order' import { useKeyboard } from '@/lib/hooks/useKeyboard' import { usePitchData } from '@/lib/hooks/usePitchData' import { usePresenterMode } from '@/lib/hooks/usePresenterMode' @@ -68,15 +69,22 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, const data = previewData || fetched.data const loading = previewData ? false : fetched.loading const error = previewData ? null : fetched.error - const nav = useSlideNavigation() const [fabOpen, setFabOpen] = useState(false) const isWandeldarlehen = (data?.funding?.instrument || '').toLowerCase() === 'wandeldarlehen' + const isShowcase = investor?.is_showcase === true // Derive fp_scenario IDs from version snapshot (fm_scenarios stores fp_scenario IDs directly) const fpScenarios = data?.fp_scenarios || [] const fpBaseScenarioId = fpScenarios.find(s => s.is_default)?.id ?? fpScenarios[0]?.id ?? null const preferredScenarioId = fpBaseScenarioId + // Showcase mode: filter out investor/financial slides + const activeSlideOrder = isShowcase + ? SLIDE_ORDER.filter(s => !SHOWCASE_HIDDEN_SLIDES.has(s)) + : SLIDE_ORDER + + const nav = useSlideNavigation(activeSlideOrder) + // Skip cap-table slide for Wandeldarlehen versions useEffect(() => { if (nav.currentSlide === 'cap-table' && isWandeldarlehen) { diff --git a/pitch-deck/lib/hooks/useAuth.ts b/pitch-deck/lib/hooks/useAuth.ts index fa52f5d..f56eeaa 100644 --- a/pitch-deck/lib/hooks/useAuth.ts +++ b/pitch-deck/lib/hooks/useAuth.ts @@ -11,6 +11,7 @@ export interface Investor { last_login_at: string | null login_count: number created_at: string + is_showcase: boolean } export function useAuth() { diff --git a/pitch-deck/lib/hooks/useSlideNavigation.ts b/pitch-deck/lib/hooks/useSlideNavigation.ts index 3c7c517..a236091 100644 --- a/pitch-deck/lib/hooks/useSlideNavigation.ts +++ b/pitch-deck/lib/hooks/useSlideNavigation.ts @@ -1,21 +1,23 @@ 'use client' import { useState, useCallback } from 'react' +import { SlideId } from '../types' import { SLIDE_ORDER, TOTAL_SLIDES } from '../slide-order' // Re-export for backwards compatibility export { SLIDE_ORDER, TOTAL_SLIDES } -export function useSlideNavigation() { +export function useSlideNavigation(slideOrder: SlideId[] = SLIDE_ORDER) { + const total = slideOrder.length const [currentIndex, setCurrentIndex] = useState(0) const [direction, setDirection] = useState(0) const [visitedSlides, setVisitedSlides] = useState>(new Set([0])) const [showOverview, setShowOverview] = useState(false) - const currentSlide = SLIDE_ORDER[currentIndex] + const currentSlide = slideOrder[currentIndex] const goToSlide = useCallback((index: number) => { - if (index < 0 || index >= TOTAL_SLIDES) return + if (index < 0 || index >= total) return setDirection(index > currentIndex ? 1 : -1) setCurrentIndex(index) setVisitedSlides(prev => new Set([...prev, index])) @@ -23,10 +25,10 @@ export function useSlideNavigation() { }, [currentIndex]) const nextSlide = useCallback(() => { - if (currentIndex < TOTAL_SLIDES - 1) { + if (currentIndex < total - 1) { goToSlide(currentIndex + 1) } - }, [currentIndex, goToSlide]) + }, [currentIndex, goToSlide, total]) const prevSlide = useCallback(() => { if (currentIndex > 0) { @@ -35,7 +37,7 @@ export function useSlideNavigation() { }, [currentIndex, goToSlide]) const goToFirst = useCallback(() => goToSlide(0), [goToSlide]) - const goToLast = useCallback(() => goToSlide(TOTAL_SLIDES - 1), [goToSlide]) + const goToLast = useCallback(() => goToSlide(total - 1), [goToSlide, total]) const toggleOverview = useCallback(() => { setShowOverview(prev => !prev) @@ -47,8 +49,8 @@ export function useSlideNavigation() { direction, visitedSlides, showOverview, - totalSlides: TOTAL_SLIDES, - slideOrder: SLIDE_ORDER, + totalSlides: total, + slideOrder, goToSlide, nextSlide, prevSlide, @@ -57,6 +59,6 @@ export function useSlideNavigation() { toggleOverview, setShowOverview, isFirst: currentIndex === 0, - isLast: currentIndex === TOTAL_SLIDES - 1, + isLast: currentIndex === total - 1, } } diff --git a/pitch-deck/lib/slide-order.ts b/pitch-deck/lib/slide-order.ts index 1fe3ac2..1b0eca6 100644 --- a/pitch-deck/lib/slide-order.ts +++ b/pitch-deck/lib/slide-order.ts @@ -1,5 +1,17 @@ import { SlideId } from './types' +// Slides hidden in showcase (customer) mode — financial and investor-specific content +export const SHOWCASE_HIDDEN_SLIDES = new Set([ + 'intro-presenter', + 'executive-summary', + 'financials', + 'the-ask', + 'cap-table', + 'annex-assumptions', + 'annex-finanzplan', + 'risks', +]) + export const SLIDE_ORDER: SlideId[] = [ 'intro-presenter', 'executive-summary', From be126a7a396431d0ca5fe6d25cb680a85b9b9228 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Mon, 4 May 2026 22:50:33 +0200 Subject: [PATCH 5/9] fix(pitch): showcase sidebar shows only filtered slides + AI presenter via FAB NavigationFAB and SlideOverview now accept slideNames prop and render only the active slide list (filtered for showcase mode). Adds AI presenter start button to the FAB footer so it's accessible even when intro-presenter slide is hidden. Co-Authored-By: Claude Sonnet 4.6 --- pitch-deck/components/NavigationFAB.tsx | 19 ++++++++++++++++++- pitch-deck/components/PitchDeck.tsx | 11 +++++++++++ pitch-deck/components/SlideOverview.tsx | 6 ++++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/pitch-deck/components/NavigationFAB.tsx b/pitch-deck/components/NavigationFAB.tsx index 38357fd..2d04d01 100644 --- a/pitch-deck/components/NavigationFAB.tsx +++ b/pitch-deck/components/NavigationFAB.tsx @@ -13,6 +13,8 @@ interface NavigationFABProps { onGoToSlide: (index: number) => void lang: Language onToggleLanguage: () => void + slideNames?: string[] + onPresenterStart?: () => void } export default function NavigationFAB({ @@ -22,6 +24,8 @@ export default function NavigationFAB({ onGoToSlide, lang, onToggleLanguage, + slideNames: slideNamesProp, + onPresenterStart, }: NavigationFABProps) { const [isOpen, setIsOpen] = useState(false) const [isFullscreen, setIsFullscreen] = useState(false) @@ -35,6 +39,7 @@ export default function NavigationFAB({ }) }, []) const i = t(lang) + const activeSlideNames = slideNamesProp ?? i.slideNames const toggleFullscreen = useCallback(() => { if (!document.fullscreenElement) { @@ -89,7 +94,7 @@ export default function NavigationFAB({ {/* Slide List */}
- {i.slideNames.map((name, idx) => { + {activeSlideNames.map((name, idx) => { const isActive = idx === currentIndex const isVisited = visitedSlides.has(idx) const isAI = idx === totalSlides - 1 @@ -164,6 +169,18 @@ export default function NavigationFAB({
+ {/* AI Presenter */} + {onPresenterStart && ( + + )} + {/* Fullscreen */}