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 adminId = guard.kind === 'admin' ? guard.admin.id : null
|
||||||
|
|
||||||
const body = await request.json().catch(() => ({}))
|
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') {
|
if (!email || typeof email !== 'string') {
|
||||||
return NextResponse.json({ error: 'Email required' }, { status: 400 })
|
return NextResponse.json({ error: 'Email required' }, { status: 400 })
|
||||||
@@ -27,18 +27,34 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const normalizedLang = lang === 'en' ? 'en' : 'de'
|
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
|
// Upsert investor
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`INSERT INTO pitch_investors (email, name, company, preferred_lang)
|
`INSERT INTO pitch_investors (email, name, company, preferred_lang, assigned_version_id)
|
||||||
VALUES ($1, $2, $3, $4)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
ON CONFLICT (email) DO UPDATE SET
|
ON CONFLICT (email) DO UPDATE SET
|
||||||
name = COALESCE(EXCLUDED.name, pitch_investors.name),
|
name = COALESCE(EXCLUDED.name, pitch_investors.name),
|
||||||
company = COALESCE(EXCLUDED.company, pitch_investors.company),
|
company = COALESCE(EXCLUDED.company, pitch_investors.company),
|
||||||
preferred_lang = EXCLUDED.preferred_lang,
|
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,
|
status = CASE WHEN pitch_investors.status = 'revoked' THEN 'invited' ELSE pitch_investors.status END,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
RETURNING id, status`,
|
RETURNING id, status`,
|
||||||
[normalizedEmail, name || null, company || null, normalizedLang],
|
[normalizedEmail, name || null, company || null, normalizedLang, normalizedVersionId],
|
||||||
)
|
)
|
||||||
|
|
||||||
const investor = rows[0]
|
const investor = rows[0]
|
||||||
@@ -71,6 +87,7 @@ export async function POST(request: NextRequest) {
|
|||||||
expires_at: expiresAt.toISOString(),
|
expires_at: expiresAt.toISOString(),
|
||||||
lang: normalizedLang,
|
lang: normalizedLang,
|
||||||
send_email: !!send_email,
|
send_email: !!send_email,
|
||||||
|
version_id: normalizedVersionId,
|
||||||
},
|
},
|
||||||
request,
|
request,
|
||||||
investor.id,
|
investor.id,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useMemo, useEffect, useRef } from 'react'
|
import { useState, useMemo, useEffect, useRef } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import Link from 'next/link'
|
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 {
|
import {
|
||||||
DEFAULT_MESSAGE, DEFAULT_CLOSING,
|
DEFAULT_MESSAGE, DEFAULT_CLOSING,
|
||||||
DEFAULT_MESSAGE_EN, DEFAULT_CLOSING_EN,
|
DEFAULT_MESSAGE_EN, DEFAULT_CLOSING_EN,
|
||||||
@@ -12,6 +12,13 @@ import {
|
|||||||
|
|
||||||
type Lang = 'de' | 'en'
|
type Lang = 'de' | 'en'
|
||||||
|
|
||||||
|
interface Version {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
status: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function NewInvestorPage() {
|
export default function NewInvestorPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
@@ -19,6 +26,8 @@ export default function NewInvestorPage() {
|
|||||||
const [company, setCompany] = useState('')
|
const [company, setCompany] = useState('')
|
||||||
const [lang, setLang] = useState<Lang>('de')
|
const [lang, setLang] = useState<Lang>('de')
|
||||||
const [sendEmail, setSendEmail] = useState(true)
|
const [sendEmail, setSendEmail] = useState(true)
|
||||||
|
const [versionId, setVersionId] = useState('')
|
||||||
|
const [versions, setVersions] = useState<Version[]>([])
|
||||||
const [greeting, setGreeting] = useState('')
|
const [greeting, setGreeting] = useState('')
|
||||||
const [message, setMessage] = useState(DEFAULT_MESSAGE)
|
const [message, setMessage] = useState(DEFAULT_MESSAGE)
|
||||||
const [closing, setClosing] = useState(DEFAULT_CLOSING)
|
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 ttl = process.env.NEXT_PUBLIC_MAGIC_LINK_TTL_HOURS || '72'
|
||||||
const effectiveGreeting = greeting || getDefaultGreeting(name || null, lang)
|
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
|
// When language changes, swap defaults unless the user has manually edited
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lang === prevLang.current) return
|
if (lang === prevLang.current) return
|
||||||
@@ -58,6 +74,7 @@ export default function NewInvestorPage() {
|
|||||||
company,
|
company,
|
||||||
lang,
|
lang,
|
||||||
send_email: sendEmail,
|
send_email: sendEmail,
|
||||||
|
version_id: versionId || null,
|
||||||
...(sendEmail && {
|
...(sendEmail && {
|
||||||
greeting: effectiveGreeting,
|
greeting: effectiveGreeting,
|
||||||
message,
|
message,
|
||||||
@@ -207,6 +224,27 @@ export default function NewInvestorPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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]" />
|
<hr className="border-white/[0.06]" />
|
||||||
|
|
||||||
{/* Language toggle + Send email toggle */}
|
{/* Language toggle + Send email toggle */}
|
||||||
|
|||||||
Reference in New Issue
Block a user