diff --git a/pitch-deck/app/api/preview-data/[versionId]/route.ts b/pitch-deck/app/api/preview-data/[versionId]/route.ts new file mode 100644 index 0000000..6b6d3e8 --- /dev/null +++ b/pitch-deck/app/api/preview-data/[versionId]/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db' +import { getAdminFromCookie } from '@/lib/admin-auth' + +interface Ctx { params: Promise<{ versionId: string }> } + +export async function GET(request: NextRequest, ctx: Ctx) { + // Admin-only: verify admin session + const admin = await getAdminFromCookie() + if (!admin) { + return NextResponse.json({ error: 'Admin access required for preview' }, { status: 401 }) + } + + const { versionId } = await ctx.params + + // Load version data + const { rows } = await pool.query( + `SELECT table_name, data FROM pitch_version_data WHERE version_id = $1`, + [versionId], + ) + + if (rows.length === 0) { + return NextResponse.json({ error: 'Version not found or has no data' }, { status: 404 }) + } + + const map: Record = {} + for (const row of rows) { + map[row.table_name] = typeof row.data === 'string' ? JSON.parse(row.data) : row.data + } + + // Return PitchData format + return NextResponse.json({ + company: (map.company || [])[0] || null, + team: map.team || [], + financials: map.financials || [], + market: map.market || [], + competitors: map.competitors || [], + features: map.features || [], + milestones: map.milestones || [], + metrics: map.metrics || [], + funding: (map.funding || [])[0] || null, + products: map.products || [], + }) +} diff --git a/pitch-deck/app/pitch-admin/(authed)/financial-model/[scenarioId]/page.tsx b/pitch-deck/app/pitch-admin/(authed)/financial-model/[scenarioId]/page.tsx index cc16f93..b471286 100644 --- a/pitch-deck/app/pitch-admin/(authed)/financial-model/[scenarioId]/page.tsx +++ b/pitch-deck/app/pitch-admin/(authed)/financial-model/[scenarioId]/page.tsx @@ -132,7 +132,7 @@ export default function EditScenarioPage() { const isObjectArray = Array.isArray(a.value) && a.value.length > 0 && typeof a.value[0] === 'object' && a.value[0] !== null if (isObjectArray) { - const rows = isEdited ? (JSON.parse(edits[a.id]) as Record[]) : (a.value as Record[]) + const rows = isEdited ? (JSON.parse(edits[a.id]) as Record[]) : (a.value as unknown as Record[]) const cols = Object.keys(rows[0] || {}) return ( diff --git a/pitch-deck/app/pitch-admin/(authed)/versions/[id]/page.tsx b/pitch-deck/app/pitch-admin/(authed)/versions/[id]/page.tsx index 2697406..d898d81 100644 --- a/pitch-deck/app/pitch-admin/(authed)/versions/[id]/page.tsx +++ b/pitch-deck/app/pitch-admin/(authed)/versions/[id]/page.tsx @@ -3,34 +3,28 @@ import { useEffect, useState, useCallback } from 'react' import { useParams, useRouter } from 'next/navigation' import Link from 'next/link' -import { ArrowLeft, Lock, Save, GitFork } from 'lucide-react' +import { ArrowLeft, Lock, Save, GitFork, Eye, Code } from 'lucide-react' +import BilingualField from '@/components/pitch-admin/editors/BilingualField' +import FormField from '@/components/pitch-admin/editors/FormField' +import ArrayField from '@/components/pitch-admin/editors/ArrayField' +import RowTable from '@/components/pitch-admin/editors/RowTable' +import CardList from '@/components/pitch-admin/editors/CardList' const TABLE_LABELS: Record = { - company: 'Company', - team: 'Team', - financials: 'Financials', - market: 'Market', - competitors: 'Competitors', - features: 'Features', - milestones: 'Milestones', - metrics: 'Metrics', - funding: 'Funding', - products: 'Products', - fm_scenarios: 'FM Scenarios', - fm_assumptions: 'FM Assumptions', + company: 'Company', team: 'Team', financials: 'Financials', market: 'Market', + competitors: 'Competitors', features: 'Features', milestones: 'Milestones', + metrics: 'Metrics', funding: 'Funding', products: 'Products', + fm_scenarios: 'FM Scenarios', fm_assumptions: 'FM Assumptions', } - const TABLE_NAMES = Object.keys(TABLE_LABELS) interface Version { - id: string - name: string - description: string | null - status: 'draft' | 'committed' - parent_id: string | null - committed_at: string | null + id: string; name: string; description: string | null + status: 'draft' | 'committed'; parent_id: string | null; committed_at: string | null } +type R = Record + export default function VersionEditorPage() { const { id } = useParams<{ id: string }>() const router = useRouter() @@ -38,9 +32,10 @@ export default function VersionEditorPage() { const [allData, setAllData] = useState>({}) const [activeTab, setActiveTab] = useState('company') const [loading, setLoading] = useState(true) - const [editorValue, setEditorValue] = useState('') const [dirty, setDirty] = useState(false) const [saving, setSaving] = useState(false) + const [jsonMode, setJsonMode] = useState(false) + const [jsonText, setJsonText] = useState('') const [toast, setToast] = useState(null) function flashToast(msg: string) { setToast(msg); setTimeout(() => setToast(null), 3000) } @@ -48,53 +43,55 @@ export default function VersionEditorPage() { const load = useCallback(async () => { setLoading(true) const res = await fetch(`/api/admin/versions/${id}`) - if (res.ok) { - const d = await res.json() - setVersion(d.version) - setAllData(d.data) - } + if (res.ok) { const d = await res.json(); setVersion(d.version); setAllData(d.data) } setLoading(false) }, [id]) useEffect(() => { if (id) load() }, [id, load]) - // When tab changes, set editor value + // Sync JSON text when switching tabs or toggling JSON mode useEffect(() => { - const data = allData[activeTab] - if (data !== undefined) { - setEditorValue(JSON.stringify(data, null, 2)) - setDirty(false) - } - }, [activeTab, allData]) + if (jsonMode) setJsonText(JSON.stringify(allData[activeTab] || [], null, 2)) + }, [activeTab, jsonMode, allData]) + + function updateData(newData: unknown[]) { + setAllData(prev => ({ ...prev, [activeTab]: newData })) + setDirty(true) + } + + function updateRecord(index: number, key: string, value: unknown) { + const arr = [...(allData[activeTab] as R[] || [])] + arr[index] = { ...arr[index], [key]: value } + updateData(arr) + } + + // For single-record tables (company, funding) + function updateSingle(key: string, value: unknown) { updateRecord(0, key, value) } async function saveTable() { - let parsed: unknown - try { - parsed = JSON.parse(editorValue) - } catch { - flashToast('Invalid JSON') - return + let data: unknown + if (jsonMode) { + try { data = JSON.parse(jsonText) } catch { flashToast('Invalid JSON'); return } + } else { + data = allData[activeTab] } setSaving(true) const res = await fetch(`/api/admin/versions/${id}/data/${activeTab}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ data: parsed }), + body: JSON.stringify({ data }), }) setSaving(false) if (res.ok) { setDirty(false) - setAllData(prev => ({ ...prev, [activeTab]: Array.isArray(parsed) ? parsed : [parsed] })) + if (jsonMode) setAllData(prev => ({ ...prev, [activeTab]: Array.isArray(data) ? data : [data] })) flashToast('Saved') - } else { - const d = await res.json().catch(() => ({})) - flashToast(d.error || 'Save failed') - } + } else { const d = await res.json().catch(() => ({})); flashToast(d.error || 'Save failed') } } async function commitVersion() { - if (!confirm('Commit this version? It becomes immutable and available for investor assignment.')) return + if (!confirm('Commit this version? It becomes immutable.')) return const res = await fetch(`/api/admin/versions/${id}/commit`, { method: 'POST' }) if (res.ok) { flashToast('Committed'); load() } else { const d = await res.json().catch(() => ({})); flashToast(d.error || 'Failed') } @@ -104,20 +101,328 @@ export default function VersionEditorPage() { const name = prompt('Name for the new draft:') if (!name) return const res = await fetch(`/api/admin/versions/${id}/fork`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name }), + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), }) - if (res.ok) { - const d = await res.json() - router.push(`/pitch-admin/versions/${d.version.id}`) - } else { const d = await res.json().catch(() => ({})); flashToast(d.error || 'Failed') } + if (res.ok) { const d = await res.json(); router.push(`/pitch-admin/versions/${d.version.id}`) } + else { const d = await res.json().catch(() => ({})); flashToast(d.error || 'Failed') } } if (loading) return
if (!version) return
Version not found
const isDraft = version.status === 'draft' + const data = allData[activeTab] || [] + const single = (data as R[])[0] || {} as R + + function renderEditor() { + if (jsonMode) { + return ( +