feat(pitch-deck): add pitch version selection to investor invite form
Build pitch-deck / build-push-deploy (push) Successful in 1m33s
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 33s
CI / test-python-voice (push) Successful in 36s
CI / test-bqas (push) Successful in 32s

- 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 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-05-06 23:27:23 +02:00
parent 17b9006b88
commit b0d273d3ab
2 changed files with 60 additions and 5 deletions
+21 -4
View File
@@ -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,
@@ -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<Lang>('de')
const [sendEmail, setSendEmail] = useState(true)
const [versionId, setVersionId] = useState('')
const [versions, setVersions] = useState<Version[]>([])
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() {
/>
</div>
{/* Pitch Version */}
<div>
<label htmlFor="version" className="block text-xs font-medium text-white/60 uppercase tracking-wider mb-2">
<span className="inline-flex items-center gap-1.5"><Layers className="w-3.5 h-3.5" /> Pitch-Version <span className="text-white/30 text-[10px] normal-case">(optional)</span></span>
</label>
<select
id="version"
value={versionId}
onChange={(e) => setVersionId(e.target.value)}
className="w-full bg-black/30 border border-white/10 rounded-lg px-3 py-2.5 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 appearance-none"
>
<option value=""> Standard (keine spezifische Version) </option>
{versions.map(v => (
<option key={v.id} value={v.id}>{v.name}</option>
))}
</select>
{versions.length === 0 && (
<p className="text-[10px] text-white/25 mt-1">Keine committed Versionen vorhanden</p>
)}
</div>
<hr className="border-white/[0.06]" />
{/* Language toggle + Send email toggle */}