From b0d273d3ab99649a36d9e93782fb317c6a884c94 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Wed, 6 May 2026 23:27:23 +0200 Subject: [PATCH] feat(pitch-deck): add pitch version selection to investor invite form - Version dropdown on the invite form shows all committed versions - Selected version is assigned to the investor at creation time (no separate step needed) - API validates version is committed before upserting - Leaving the dropdown empty keeps any existing assignment (COALESCE behavior) - version_id included in audit log Co-Authored-By: Claude Sonnet 4.6 --- pitch-deck/app/api/admin/invite/route.ts | 25 ++++++++++-- .../(authed)/investors/new/page.tsx | 40 ++++++++++++++++++- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/pitch-deck/app/api/admin/invite/route.ts b/pitch-deck/app/api/admin/invite/route.ts index 332d52a..fbbc4a2 100644 --- a/pitch-deck/app/api/admin/invite/route.ts +++ b/pitch-deck/app/api/admin/invite/route.ts @@ -11,7 +11,7 @@ export async function POST(request: NextRequest) { const adminId = guard.kind === 'admin' ? guard.admin.id : null const body = await request.json().catch(() => ({})) - const { email, name, company, greeting, message, closing, lang = 'de', send_email = true } = body + const { email, name, company, greeting, message, closing, lang = 'de', send_email = true, version_id } = body if (!email || typeof email !== 'string') { return NextResponse.json({ error: 'Email required' }, { status: 400 }) @@ -27,18 +27,34 @@ export async function POST(request: NextRequest) { const normalizedLang = lang === 'en' ? 'en' : 'de' + // Validate version if provided + const normalizedVersionId = version_id || null + if (normalizedVersionId) { + const ver = await pool.query( + `SELECT id, status FROM pitch_versions WHERE id = $1`, + [normalizedVersionId], + ) + if (ver.rows.length === 0) { + return NextResponse.json({ error: 'Version not found' }, { status: 404 }) + } + if (ver.rows[0].status !== 'committed') { + return NextResponse.json({ error: 'Can only assign committed versions' }, { status: 400 }) + } + } + // Upsert investor const { rows } = await pool.query( - `INSERT INTO pitch_investors (email, name, company, preferred_lang) - VALUES ($1, $2, $3, $4) + `INSERT INTO pitch_investors (email, name, company, preferred_lang, assigned_version_id) + VALUES ($1, $2, $3, $4, $5) ON CONFLICT (email) DO UPDATE SET name = COALESCE(EXCLUDED.name, pitch_investors.name), company = COALESCE(EXCLUDED.company, pitch_investors.company), preferred_lang = EXCLUDED.preferred_lang, + assigned_version_id = COALESCE(EXCLUDED.assigned_version_id, pitch_investors.assigned_version_id), status = CASE WHEN pitch_investors.status = 'revoked' THEN 'invited' ELSE pitch_investors.status END, updated_at = NOW() RETURNING id, status`, - [normalizedEmail, name || null, company || null, normalizedLang], + [normalizedEmail, name || null, company || null, normalizedLang, normalizedVersionId], ) const investor = rows[0] @@ -71,6 +87,7 @@ export async function POST(request: NextRequest) { expires_at: expiresAt.toISOString(), lang: normalizedLang, send_email: !!send_email, + version_id: normalizedVersionId, }, request, investor.id, diff --git a/pitch-deck/app/pitch-admin/(authed)/investors/new/page.tsx b/pitch-deck/app/pitch-admin/(authed)/investors/new/page.tsx index 71e146a..86a6407 100644 --- a/pitch-deck/app/pitch-admin/(authed)/investors/new/page.tsx +++ b/pitch-deck/app/pitch-admin/(authed)/investors/new/page.tsx @@ -3,7 +3,7 @@ import { useState, useMemo, useEffect, useRef } from 'react' import { useRouter } from 'next/navigation' import Link from 'next/link' -import { ArrowLeft, Eye, Send, Link2, Copy, Check, Mail, MailX } from 'lucide-react' +import { ArrowLeft, Eye, Send, Link2, Copy, Check, Mail, MailX, Layers } from 'lucide-react' import { DEFAULT_MESSAGE, DEFAULT_CLOSING, DEFAULT_MESSAGE_EN, DEFAULT_CLOSING_EN, @@ -12,6 +12,13 @@ import { type Lang = 'de' | 'en' +interface Version { + id: string + name: string + status: string + created_at: string +} + export default function NewInvestorPage() { const router = useRouter() const [email, setEmail] = useState('') @@ -19,6 +26,8 @@ export default function NewInvestorPage() { const [company, setCompany] = useState('') const [lang, setLang] = useState('de') const [sendEmail, setSendEmail] = useState(true) + const [versionId, setVersionId] = useState('') + const [versions, setVersions] = useState([]) const [greeting, setGreeting] = useState('') const [message, setMessage] = useState(DEFAULT_MESSAGE) const [closing, setClosing] = useState(DEFAULT_CLOSING) @@ -33,6 +42,13 @@ export default function NewInvestorPage() { const ttl = process.env.NEXT_PUBLIC_MAGIC_LINK_TTL_HOURS || '72' const effectiveGreeting = greeting || getDefaultGreeting(name || null, lang) + useEffect(() => { + fetch('/api/admin/versions') + .then(r => r.json()) + .then(d => setVersions((d.versions ?? []).filter((v: Version) => v.status === 'committed'))) + .catch(() => {}) + }, []) + // When language changes, swap defaults unless the user has manually edited useEffect(() => { if (lang === prevLang.current) return @@ -58,6 +74,7 @@ export default function NewInvestorPage() { company, lang, send_email: sendEmail, + version_id: versionId || null, ...(sendEmail && { greeting: effectiveGreeting, message, @@ -207,6 +224,27 @@ export default function NewInvestorPage() { /> + {/* Pitch Version */} +
+ + + {versions.length === 0 && ( +

Keine committed Versionen vorhanden

+ )} +
+
{/* Language toggle + Send email toggle */}