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
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:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user