feat(pitch-deck): full pitch versioning with git-style history + bug fixes
Some checks failed
CI / go-lint (pull_request) Failing after 13s
CI / python-lint (pull_request) Failing after 13s
CI / nodejs-lint (pull_request) Failing after 8s
CI / test-go-consent (pull_request) Failing after 3s
CI / test-python-voice (pull_request) Failing after 10s
CI / test-bqas (pull_request) Failing after 11s
CI / Deploy (pull_request) Has been skipped
Some checks failed
CI / go-lint (pull_request) Failing after 13s
CI / python-lint (pull_request) Failing after 13s
CI / nodejs-lint (pull_request) Failing after 8s
CI / test-go-consent (pull_request) Failing after 3s
CI / test-python-voice (pull_request) Failing after 10s
CI / test-bqas (pull_request) Failing after 11s
CI / Deploy (pull_request) Has been skipped
Adds a complete version management system where every piece of pitch data (all 12 tables: company, team, financials, market, competitors, features, milestones, metrics, funding, products, fm_scenarios, fm_assumptions) can be versioned, diffed, and assigned per-investor. Version lifecycle: create draft → edit freely → commit (immutable) → fork to create new draft. Parent chain gives full git-style history. Backend: - Migration 003: pitch_versions, pitch_version_data tables + investor assigned_version_id column - lib/version-helpers.ts: snapshot base tables, copy between versions - lib/version-diff.ts: per-table row+field diffing engine - 7 new API routes: versions CRUD, commit, fork, per-table data GET/PUT, diff endpoint - /api/data + /api/financial-model: version-aware loading (check investor's assigned_version_id, serve version data or fall back to base tables) - Investor PATCH: accepts assigned_version_id (validates committed) Frontend: - /pitch-admin/versions: list with status badges, fork/commit/delete - /pitch-admin/versions/new: create from base tables or fork existing - /pitch-admin/versions/[id]: 12-tab JSON editor (one per data table) with save-per-table, commit button, fork button - /pitch-admin/versions/[id]/diff/[otherId]: side-by-side diff view with added/removed/changed highlighting per field - Investors list: version column showing assigned version name - Investor detail: version selector dropdown (committed versions only) - AdminShell: Versions nav item added Bug fixes: - FM editor: [object Object] for JSONB array values → JSON.stringify - Admin pages not scrollable → h-screen + overflow-hidden on shell, min-h-0 on flex column Also includes migration 000 for fresh installs (pitch data tables). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,8 +14,12 @@ export async function GET(request: NextRequest, ctx: RouteContext) {
|
||||
|
||||
const [investor, sessions, snapshots, audit] = await Promise.all([
|
||||
pool.query(
|
||||
`SELECT id, email, name, company, status, last_login_at, login_count, created_at, updated_at
|
||||
FROM pitch_investors WHERE id = $1`,
|
||||
`SELECT i.id, i.email, i.name, i.company, i.status, i.last_login_at, i.login_count,
|
||||
i.created_at, i.updated_at, i.assigned_version_id,
|
||||
v.name AS version_name, v.status AS version_status
|
||||
FROM pitch_investors i
|
||||
LEFT JOIN pitch_versions v ON v.id = i.assigned_version_id
|
||||
WHERE i.id = $1`,
|
||||
[id],
|
||||
),
|
||||
pool.query(
|
||||
@@ -60,36 +64,58 @@ export async function PATCH(request: NextRequest, ctx: RouteContext) {
|
||||
|
||||
const { id } = await ctx.params
|
||||
const body = await request.json().catch(() => ({}))
|
||||
const { name, company } = body
|
||||
const { name, company, assigned_version_id } = body
|
||||
|
||||
if (name === undefined && company === undefined) {
|
||||
return NextResponse.json({ error: 'name or company required' }, { status: 400 })
|
||||
if (name === undefined && company === undefined && assigned_version_id === undefined) {
|
||||
return NextResponse.json({ error: 'name, company, or assigned_version_id required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const before = await pool.query(
|
||||
`SELECT name, company FROM pitch_investors WHERE id = $1`,
|
||||
`SELECT name, company, assigned_version_id FROM pitch_investors WHERE id = $1`,
|
||||
[id],
|
||||
)
|
||||
if (before.rows.length === 0) {
|
||||
return NextResponse.json({ error: 'Investor not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Validate version exists and is committed (if assigning)
|
||||
if (assigned_version_id !== undefined && assigned_version_id !== null) {
|
||||
const ver = await pool.query(
|
||||
`SELECT id, status FROM pitch_versions WHERE id = $1`,
|
||||
[assigned_version_id],
|
||||
)
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
// Use null to clear version assignment, undefined to leave unchanged
|
||||
const versionValue = assigned_version_id === undefined ? before.rows[0].assigned_version_id : (assigned_version_id || null)
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`UPDATE pitch_investors SET
|
||||
name = COALESCE($1, name),
|
||||
company = COALESCE($2, company),
|
||||
assigned_version_id = $4,
|
||||
updated_at = NOW()
|
||||
WHERE id = $3
|
||||
RETURNING id, email, name, company, status`,
|
||||
[name ?? null, company ?? null, id],
|
||||
RETURNING id, email, name, company, status, assigned_version_id`,
|
||||
[name ?? null, company ?? null, id, versionValue],
|
||||
)
|
||||
|
||||
const action = assigned_version_id !== undefined && assigned_version_id !== before.rows[0].assigned_version_id
|
||||
? 'investor_version_assigned'
|
||||
: 'investor_edited'
|
||||
|
||||
await logAdminAudit(
|
||||
adminId,
|
||||
'investor_edited',
|
||||
action,
|
||||
{
|
||||
before: before.rows[0],
|
||||
after: { name: rows[0].name, company: rows[0].company },
|
||||
after: { name: rows[0].name, company: rows[0].company, assigned_version_id: rows[0].assigned_version_id },
|
||||
},
|
||||
request,
|
||||
id,
|
||||
|
||||
@@ -8,9 +8,11 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`SELECT i.id, i.email, i.name, i.company, i.status, i.last_login_at, i.login_count, i.created_at,
|
||||
i.assigned_version_id, v.name AS version_name,
|
||||
(SELECT COUNT(*) FROM pitch_audit_logs a WHERE a.investor_id = i.id AND a.action = 'slide_viewed') as slides_viewed,
|
||||
(SELECT MAX(a.created_at) FROM pitch_audit_logs a WHERE a.investor_id = i.id) as last_activity
|
||||
FROM pitch_investors i
|
||||
LEFT JOIN pitch_versions v ON v.id = i.assigned_version_id
|
||||
ORDER BY i.created_at DESC`,
|
||||
)
|
||||
|
||||
|
||||
31
pitch-deck/app/api/admin/versions/[id]/commit/route.ts
Normal file
31
pitch-deck/app/api/admin/versions/[id]/commit/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import pool from '@/lib/db'
|
||||
import { requireAdmin, logAdminAudit } from '@/lib/admin-auth'
|
||||
|
||||
interface Ctx { params: Promise<{ id: string }> }
|
||||
|
||||
export async function POST(request: NextRequest, ctx: Ctx) {
|
||||
const guard = await requireAdmin(request)
|
||||
if (guard.kind === 'response') return guard.response
|
||||
const adminId = guard.kind === 'admin' ? guard.admin.id : null
|
||||
|
||||
const { id } = await ctx.params
|
||||
|
||||
const ver = await pool.query(`SELECT status, name FROM pitch_versions WHERE id = $1`, [id])
|
||||
if (ver.rows.length === 0) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
if (ver.rows[0].status === 'committed') {
|
||||
return NextResponse.json({ error: 'Already committed' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`UPDATE pitch_versions SET status = 'committed', committed_at = NOW() WHERE id = $1 RETURNING *`,
|
||||
[id],
|
||||
)
|
||||
|
||||
await logAdminAudit(adminId, 'version_committed', {
|
||||
version_id: id,
|
||||
name: rows[0].name,
|
||||
}, request)
|
||||
|
||||
return NextResponse.json({ version: rows[0] })
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import pool from '@/lib/db'
|
||||
import { requireAdmin, logAdminAudit } from '@/lib/admin-auth'
|
||||
import { VERSION_TABLES, VersionTableName } from '@/lib/version-helpers'
|
||||
|
||||
interface Ctx { params: Promise<{ id: string; tableName: string }> }
|
||||
|
||||
export async function GET(request: NextRequest, ctx: Ctx) {
|
||||
const guard = await requireAdmin(request)
|
||||
if (guard.kind === 'response') return guard.response
|
||||
|
||||
const { id, tableName } = await ctx.params
|
||||
|
||||
if (!VERSION_TABLES.includes(tableName as VersionTableName)) {
|
||||
return NextResponse.json({ error: `Invalid table: ${tableName}` }, { status: 400 })
|
||||
}
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`SELECT data, updated_at, updated_by FROM pitch_version_data
|
||||
WHERE version_id = $1 AND table_name = $2`,
|
||||
[id, tableName],
|
||||
)
|
||||
|
||||
if (rows.length === 0) {
|
||||
return NextResponse.json({ data: [], updated_at: null })
|
||||
}
|
||||
|
||||
const data = typeof rows[0].data === 'string' ? JSON.parse(rows[0].data) : rows[0].data
|
||||
return NextResponse.json({ data, updated_at: rows[0].updated_at })
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest, ctx: Ctx) {
|
||||
const guard = await requireAdmin(request)
|
||||
if (guard.kind === 'response') return guard.response
|
||||
const adminId = guard.kind === 'admin' ? guard.admin.id : null
|
||||
|
||||
const { id, tableName } = await ctx.params
|
||||
|
||||
if (!VERSION_TABLES.includes(tableName as VersionTableName)) {
|
||||
return NextResponse.json({ error: `Invalid table: ${tableName}` }, { status: 400 })
|
||||
}
|
||||
|
||||
// Verify version is a draft
|
||||
const ver = await pool.query(`SELECT status FROM pitch_versions WHERE id = $1`, [id])
|
||||
if (ver.rows.length === 0) return NextResponse.json({ error: 'Version not found' }, { status: 404 })
|
||||
if (ver.rows[0].status === 'committed') {
|
||||
return NextResponse.json({ error: 'Cannot edit a committed version' }, { status: 400 })
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => ({}))
|
||||
const { data } = body
|
||||
if (!Array.isArray(data) && typeof data !== 'object') {
|
||||
return NextResponse.json({ error: 'data must be an array or object' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Wrap single-record tables in array for consistency
|
||||
const normalizedData = Array.isArray(data) ? data : [data]
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO pitch_version_data (version_id, table_name, data, updated_by)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (version_id, table_name) DO UPDATE SET
|
||||
data = $3, updated_at = NOW(), updated_by = $4`,
|
||||
[id, tableName, JSON.stringify(normalizedData), adminId],
|
||||
)
|
||||
|
||||
await logAdminAudit(adminId, 'version_data_edited', {
|
||||
version_id: id,
|
||||
table_name: tableName,
|
||||
}, request)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import pool from '@/lib/db'
|
||||
import { requireAdmin } from '@/lib/admin-auth'
|
||||
import { loadVersionData, VERSION_TABLES } from '@/lib/version-helpers'
|
||||
import { diffTable } from '@/lib/version-diff'
|
||||
|
||||
interface Ctx { params: Promise<{ id: string; otherId: string }> }
|
||||
|
||||
export async function GET(request: NextRequest, ctx: Ctx) {
|
||||
const guard = await requireAdmin(request)
|
||||
if (guard.kind === 'response') return guard.response
|
||||
|
||||
const { id, otherId } = await ctx.params
|
||||
|
||||
// Verify both versions exist
|
||||
const [vA, vB] = await Promise.all([
|
||||
pool.query(`SELECT id, name, status, created_at FROM pitch_versions WHERE id = $1`, [id]),
|
||||
pool.query(`SELECT id, name, status, created_at FROM pitch_versions WHERE id = $1`, [otherId]),
|
||||
])
|
||||
if (vA.rows.length === 0 || vB.rows.length === 0) {
|
||||
return NextResponse.json({ error: 'One or both versions not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const [dataA, dataB] = await Promise.all([
|
||||
loadVersionData(id),
|
||||
loadVersionData(otherId),
|
||||
])
|
||||
|
||||
const diffs = VERSION_TABLES.map(tableName =>
|
||||
diffTable(tableName, dataA[tableName] || [], dataB[tableName] || [])
|
||||
).filter(d => d.hasChanges)
|
||||
|
||||
return NextResponse.json({
|
||||
versionA: vA.rows[0],
|
||||
versionB: vB.rows[0],
|
||||
diffs,
|
||||
total_changes: diffs.reduce((sum, d) => sum + d.rows.filter(r => r.status !== 'unchanged').length, 0),
|
||||
})
|
||||
}
|
||||
38
pitch-deck/app/api/admin/versions/[id]/fork/route.ts
Normal file
38
pitch-deck/app/api/admin/versions/[id]/fork/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import pool from '@/lib/db'
|
||||
import { requireAdmin, logAdminAudit } from '@/lib/admin-auth'
|
||||
import { copyVersionData } from '@/lib/version-helpers'
|
||||
|
||||
interface Ctx { params: Promise<{ id: string }> }
|
||||
|
||||
export async function POST(request: NextRequest, ctx: Ctx) {
|
||||
const guard = await requireAdmin(request)
|
||||
if (guard.kind === 'response') return guard.response
|
||||
const adminId = guard.kind === 'admin' ? guard.admin.id : null
|
||||
|
||||
const { id } = await ctx.params
|
||||
const body = await request.json().catch(() => ({}))
|
||||
const name = body.name || ''
|
||||
|
||||
const parent = await pool.query(`SELECT id, name, status FROM pitch_versions WHERE id = $1`, [id])
|
||||
if (parent.rows.length === 0) return NextResponse.json({ error: 'Parent version not found' }, { status: 404 })
|
||||
|
||||
const forkName = name.trim() || `${parent.rows[0].name} (fork)`
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO pitch_versions (name, parent_id, status, created_by)
|
||||
VALUES ($1, $2, 'draft', $3) RETURNING *`,
|
||||
[forkName, id, adminId],
|
||||
)
|
||||
const version = rows[0]
|
||||
|
||||
await copyVersionData(id, version.id, adminId)
|
||||
await logAdminAudit(adminId, 'version_forked', {
|
||||
version_id: version.id,
|
||||
parent_id: id,
|
||||
parent_name: parent.rows[0].name,
|
||||
name: forkName,
|
||||
}, request)
|
||||
|
||||
return NextResponse.json({ version })
|
||||
}
|
||||
85
pitch-deck/app/api/admin/versions/[id]/route.ts
Normal file
85
pitch-deck/app/api/admin/versions/[id]/route.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import pool from '@/lib/db'
|
||||
import { requireAdmin, logAdminAudit } from '@/lib/admin-auth'
|
||||
import { loadVersionData } from '@/lib/version-helpers'
|
||||
|
||||
interface Ctx { params: Promise<{ id: string }> }
|
||||
|
||||
export async function GET(request: NextRequest, ctx: Ctx) {
|
||||
const guard = await requireAdmin(request)
|
||||
if (guard.kind === 'response') return guard.response
|
||||
|
||||
const { id } = await ctx.params
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`SELECT v.*, a.name AS created_by_name, a.email AS created_by_email,
|
||||
(SELECT COUNT(*)::int FROM pitch_investors i WHERE i.assigned_version_id = v.id) AS assigned_count
|
||||
FROM pitch_versions v
|
||||
LEFT JOIN pitch_admins a ON a.id = v.created_by
|
||||
WHERE v.id = $1`,
|
||||
[id],
|
||||
)
|
||||
if (rows.length === 0) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
|
||||
const data = await loadVersionData(id)
|
||||
|
||||
return NextResponse.json({ version: rows[0], data })
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest, ctx: Ctx) {
|
||||
const guard = await requireAdmin(request)
|
||||
if (guard.kind === 'response') return guard.response
|
||||
const adminId = guard.kind === 'admin' ? guard.admin.id : null
|
||||
|
||||
const { id } = await ctx.params
|
||||
const body = await request.json().catch(() => ({}))
|
||||
const { name, description } = body
|
||||
|
||||
const before = await pool.query(`SELECT name, description, status FROM pitch_versions WHERE id = $1`, [id])
|
||||
if (before.rows.length === 0) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
if (before.rows[0].status === 'committed') {
|
||||
return NextResponse.json({ error: 'Cannot edit a committed version' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`UPDATE pitch_versions SET name = COALESCE($1, name), description = COALESCE($2, description)
|
||||
WHERE id = $3 RETURNING *`,
|
||||
[name ?? null, description ?? null, id],
|
||||
)
|
||||
|
||||
await logAdminAudit(adminId, 'version_edited', {
|
||||
version_id: id,
|
||||
before: { name: before.rows[0].name, description: before.rows[0].description },
|
||||
after: { name: rows[0].name, description: rows[0].description },
|
||||
}, request)
|
||||
|
||||
return NextResponse.json({ version: rows[0] })
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest, ctx: Ctx) {
|
||||
const guard = await requireAdmin(request)
|
||||
if (guard.kind === 'response') return guard.response
|
||||
const adminId = guard.kind === 'admin' ? guard.admin.id : null
|
||||
|
||||
const { id } = await ctx.params
|
||||
|
||||
const ver = await pool.query(`SELECT status, name FROM pitch_versions WHERE id = $1`, [id])
|
||||
if (ver.rows.length === 0) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
|
||||
// Prevent deleting committed versions that have children or assigned investors
|
||||
if (ver.rows[0].status === 'committed') {
|
||||
const children = await pool.query(`SELECT id FROM pitch_versions WHERE parent_id = $1 LIMIT 1`, [id])
|
||||
if (children.rows.length > 0) {
|
||||
return NextResponse.json({ error: 'Cannot delete: has child versions' }, { status: 400 })
|
||||
}
|
||||
const investors = await pool.query(`SELECT id FROM pitch_investors WHERE assigned_version_id = $1 LIMIT 1`, [id])
|
||||
if (investors.rows.length > 0) {
|
||||
return NextResponse.json({ error: 'Cannot delete: assigned to investors' }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
await pool.query(`DELETE FROM pitch_versions WHERE id = $1`, [id])
|
||||
await logAdminAudit(adminId, 'version_deleted', { version_id: id, name: ver.rows[0].name }, request)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
56
pitch-deck/app/api/admin/versions/route.ts
Normal file
56
pitch-deck/app/api/admin/versions/route.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import pool from '@/lib/db'
|
||||
import { requireAdmin, logAdminAudit } from '@/lib/admin-auth'
|
||||
import { snapshotBaseTables, copyVersionData } from '@/lib/version-helpers'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const guard = await requireAdmin(request)
|
||||
if (guard.kind === 'response') return guard.response
|
||||
|
||||
const { rows } = await pool.query(`
|
||||
SELECT v.*,
|
||||
a.name AS created_by_name, a.email AS created_by_email,
|
||||
(SELECT COUNT(*)::int FROM pitch_investors i WHERE i.assigned_version_id = v.id) AS assigned_count
|
||||
FROM pitch_versions v
|
||||
LEFT JOIN pitch_admins a ON a.id = v.created_by
|
||||
ORDER BY v.created_at DESC
|
||||
`)
|
||||
|
||||
return NextResponse.json({ versions: rows })
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const guard = await requireAdmin(request)
|
||||
if (guard.kind === 'response') return guard.response
|
||||
const adminId = guard.kind === 'admin' ? guard.admin.id : null
|
||||
|
||||
const body = await request.json().catch(() => ({}))
|
||||
const { name, description, parent_id } = body
|
||||
|
||||
if (!name || typeof name !== 'string') {
|
||||
return NextResponse.json({ error: 'name required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Create the version row
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO pitch_versions (name, description, parent_id, status, created_by)
|
||||
VALUES ($1, $2, $3, 'draft', $4) RETURNING *`,
|
||||
[name.trim(), description || null, parent_id || null, adminId],
|
||||
)
|
||||
const version = rows[0]
|
||||
|
||||
// Copy data from parent or snapshot base tables
|
||||
if (parent_id) {
|
||||
await copyVersionData(parent_id, version.id, adminId)
|
||||
} else {
|
||||
await snapshotBaseTables(version.id, adminId)
|
||||
}
|
||||
|
||||
await logAdminAudit(adminId, 'version_created', {
|
||||
version_id: version.id,
|
||||
name: version.name,
|
||||
parent_id: parent_id || null,
|
||||
}, request)
|
||||
|
||||
return NextResponse.json({ version })
|
||||
}
|
||||
@@ -1,24 +1,53 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import pool from '@/lib/db'
|
||||
import { getSessionFromCookie } from '@/lib/auth'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const client = await pool.connect()
|
||||
// Check if investor has an assigned version
|
||||
const session = await getSessionFromCookie()
|
||||
let versionId: string | null = null
|
||||
|
||||
if (session) {
|
||||
const inv = await pool.query(
|
||||
`SELECT assigned_version_id FROM pitch_investors WHERE id = $1`,
|
||||
[session.sub],
|
||||
)
|
||||
versionId = inv.rows[0]?.assigned_version_id || null
|
||||
}
|
||||
|
||||
// If version assigned, load from pitch_version_data
|
||||
if (versionId) {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT table_name, data FROM pitch_version_data WHERE version_id = $1`,
|
||||
[versionId],
|
||||
)
|
||||
const map: Record<string, unknown[]> = {}
|
||||
for (const row of rows) {
|
||||
map[row.table_name] = typeof row.data === 'string' ? JSON.parse(row.data) : row.data
|
||||
}
|
||||
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 || [],
|
||||
})
|
||||
}
|
||||
|
||||
// Fallback: read from base tables (backward compatible)
|
||||
const client = await pool.connect()
|
||||
try {
|
||||
const [
|
||||
companyRes,
|
||||
teamRes,
|
||||
financialsRes,
|
||||
marketRes,
|
||||
competitorsRes,
|
||||
featuresRes,
|
||||
milestonesRes,
|
||||
metricsRes,
|
||||
fundingRes,
|
||||
productsRes,
|
||||
companyRes, teamRes, financialsRes, marketRes, competitorsRes,
|
||||
featuresRes, milestonesRes, metricsRes, fundingRes, productsRes,
|
||||
] = await Promise.all([
|
||||
client.query('SELECT * FROM pitch_company LIMIT 1'),
|
||||
client.query('SELECT * FROM pitch_team ORDER BY sort_order'),
|
||||
@@ -49,9 +78,6 @@ export async function GET() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Database query error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to load pitch data' },
|
||||
{ status: 500 }
|
||||
)
|
||||
return NextResponse.json({ error: 'Failed to load pitch data' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,63 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import pool from '@/lib/db'
|
||||
import { getSessionFromCookie } from '@/lib/auth'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET: Load all scenarios with their assumptions
|
||||
function assembleScenarios(scenarioRows: Record<string, unknown>[], assumptionRows: Record<string, unknown>[]) {
|
||||
return scenarioRows.map(s => ({
|
||||
...s,
|
||||
assumptions: assumptionRows
|
||||
.filter((a: Record<string, unknown>) => a.scenario_id === (s as Record<string, unknown>).id)
|
||||
.map((a: Record<string, unknown>) => ({
|
||||
...a,
|
||||
value: typeof a.value === 'string' ? JSON.parse(a.value as string) : a.value,
|
||||
})),
|
||||
}))
|
||||
}
|
||||
|
||||
// GET: Load all scenarios with their assumptions (version-aware)
|
||||
export async function GET() {
|
||||
try {
|
||||
// Check if investor has an assigned version with FM data
|
||||
const session = await getSessionFromCookie()
|
||||
let versionId: string | null = null
|
||||
|
||||
if (session) {
|
||||
const inv = await pool.query(
|
||||
`SELECT assigned_version_id FROM pitch_investors WHERE id = $1`,
|
||||
[session.sub],
|
||||
)
|
||||
versionId = inv.rows[0]?.assigned_version_id || null
|
||||
}
|
||||
|
||||
if (versionId) {
|
||||
const [scenarioData, assumptionData] = await Promise.all([
|
||||
pool.query(`SELECT data FROM pitch_version_data WHERE version_id = $1 AND table_name = 'fm_scenarios'`, [versionId]),
|
||||
pool.query(`SELECT data FROM pitch_version_data WHERE version_id = $1 AND table_name = 'fm_assumptions'`, [versionId]),
|
||||
])
|
||||
|
||||
if (scenarioData.rows.length > 0) {
|
||||
const scenarios = typeof scenarioData.rows[0].data === 'string'
|
||||
? JSON.parse(scenarioData.rows[0].data) : scenarioData.rows[0].data
|
||||
const assumptions = assumptionData.rows.length > 0
|
||||
? (typeof assumptionData.rows[0].data === 'string'
|
||||
? JSON.parse(assumptionData.rows[0].data) : assumptionData.rows[0].data)
|
||||
: []
|
||||
return NextResponse.json(assembleScenarios(scenarios, assumptions))
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: base tables
|
||||
const client = await pool.connect()
|
||||
try {
|
||||
const scenarios = await client.query(
|
||||
'SELECT * FROM pitch_fm_scenarios ORDER BY is_default DESC, name'
|
||||
)
|
||||
|
||||
const assumptions = await client.query(
|
||||
'SELECT * FROM pitch_fm_assumptions ORDER BY sort_order'
|
||||
)
|
||||
|
||||
const result = scenarios.rows.map(s => ({
|
||||
...s,
|
||||
assumptions: assumptions.rows
|
||||
.filter(a => a.scenario_id === s.id)
|
||||
.map(a => ({
|
||||
...a,
|
||||
value: typeof a.value === 'string' ? JSON.parse(a.value) : a.value,
|
||||
})),
|
||||
}))
|
||||
|
||||
return NextResponse.json(result)
|
||||
return NextResponse.json(assembleScenarios(scenarios.rows, assumptions.rows))
|
||||
} finally {
|
||||
client.release()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user