feat(dataroom): bilingual descriptions, drag-drop multi-file upload, edit existing upload descriptions
Build pitch-deck / build-push-deploy (push) Successful in 1m47s
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 39s
CI / test-python-voice (push) Successful in 32s
CI / test-bqas (push) Successful in 32s
Build pitch-deck / build-push-deploy (push) Successful in 1m47s
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 39s
CI / test-python-voice (push) Successful in 32s
CI / test-bqas (push) Successful in 32s
- lib/translate.ts: LiteLLM DE<>EN translation utility - Migration 006: description_de/description_en on both dataroom tables - Admin + investor upload APIs: accept description+lang, auto-translate the other language on save - PATCH /api/admin/dataroom/documents/[id]: description path in addition to display_name path - PATCH /api/dataroom/uploads/[id]: investor can edit their own upload descriptions - PATCH /api/admin/dataroom/investors/[id]/uploads: admin can edit investor upload descriptions - All GET queries updated to return description fields - Admin dataroom: drop zone replaces upload button, multi-file, inline description editor per doc and per investor upload - Investor dataroom: drop zone, multi-file, description+lang textarea before upload, inline description editing on existing uploads Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import pool from '@/lib/db'
|
||||
import { requireAdmin, getAdminFromCookie } from '@/lib/admin-auth'
|
||||
import { adminDocDir, removeDir } from '@/lib/dataroom-storage'
|
||||
import { logAudit } from '@/lib/auth'
|
||||
import { translateText } from '@/lib/translate'
|
||||
|
||||
interface Ctx { params: Promise<{ id: string }> }
|
||||
|
||||
@@ -11,8 +12,25 @@ export async function PATCH(request: NextRequest, ctx: Ctx) {
|
||||
if (guard.kind === 'response') return guard.response
|
||||
|
||||
const { id } = await ctx.params
|
||||
const { display_name } = await request.json()
|
||||
const body = await request.json()
|
||||
|
||||
if ('description' in body) {
|
||||
const { description, description_lang } = body as { description: string | null; description_lang: 'de' | 'en' }
|
||||
const lang = description_lang || 'en'
|
||||
const translated = description ? await translateText(description, lang) : null
|
||||
const desc_de = lang === 'de' ? (description || null) : translated
|
||||
const desc_en = lang === 'en' ? (description || null) : translated
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`UPDATE dataroom_documents SET description_de = $1, description_en = $2, updated_at = NOW()
|
||||
WHERE id = $3 RETURNING *`,
|
||||
[desc_de, desc_en, id],
|
||||
)
|
||||
if (rows.length === 0) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
return NextResponse.json({ document: rows[0] })
|
||||
}
|
||||
|
||||
const { display_name } = body
|
||||
const { rows } = await pool.query(
|
||||
`UPDATE dataroom_documents SET display_name = $1, updated_at = NOW()
|
||||
WHERE id = $2 RETURNING *`,
|
||||
|
||||
@@ -3,6 +3,7 @@ import pool from '@/lib/db'
|
||||
import { requireAdmin, getAdminFromCookie } from '@/lib/admin-auth'
|
||||
import { adminDocDir, saveFile, safeName } from '@/lib/dataroom-storage'
|
||||
import { logAudit } from '@/lib/auth'
|
||||
import { translateText } from '@/lib/translate'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -12,7 +13,8 @@ export async function GET(request: NextRequest) {
|
||||
if (guard.kind === 'response') return guard.response
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`SELECT d.id, d.filename, d.display_name, d.mime_type, d.file_size, d.uploaded_by, d.created_at,
|
||||
`SELECT d.id, d.filename, d.display_name, d.description_de, d.description_en,
|
||||
d.mime_type, d.file_size, d.uploaded_by, d.created_at,
|
||||
COUNT(r.id)::int AS release_count
|
||||
FROM dataroom_documents d
|
||||
LEFT JOIN dataroom_releases r ON r.document_id = d.id
|
||||
@@ -28,25 +30,40 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const admin = await getAdminFromCookie()
|
||||
const formData = await request.formData()
|
||||
const file = formData.get('file') as File | null
|
||||
const displayName = (formData.get('display_name') as string | null) || null
|
||||
|
||||
if (!file || file.size === 0) {
|
||||
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
|
||||
const files = formData.getAll('file') as File[]
|
||||
const validFiles = files.filter(f => f && f.size > 0)
|
||||
if (validFiles.length === 0) return NextResponse.json({ error: 'No files provided' }, { status: 400 })
|
||||
|
||||
const description = (formData.get('description') as string | null) || null
|
||||
const descLang = (formData.get('description_lang') as 'de' | 'en' | null) || 'en'
|
||||
|
||||
let desc_de: string | null = null
|
||||
let desc_en: string | null = null
|
||||
if (description) {
|
||||
const translated = await translateText(description, descLang)
|
||||
desc_de = descLang === 'de' ? description : (translated || null)
|
||||
desc_en = descLang === 'en' ? description : (translated || null)
|
||||
}
|
||||
|
||||
const documentId = randomUUID()
|
||||
const filename = safeName(file.name)
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
const filePath = await saveFile(adminDocDir(documentId), filename, buffer)
|
||||
const inserted = []
|
||||
for (const file of validFiles) {
|
||||
const documentId = randomUUID()
|
||||
const filename = safeName(file.name)
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
const filePath = await saveFile(adminDocDir(documentId), filename, buffer)
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO dataroom_documents (id, filename, file_path, display_name, mime_type, file_size, uploaded_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *`,
|
||||
[documentId, filename, filePath, displayName || file.name, file.type || 'application/octet-stream', file.size, admin?.email ?? 'admin'],
|
||||
)
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO dataroom_documents
|
||||
(id, filename, file_path, display_name, description_de, description_en, mime_type, file_size, uploaded_by)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
||||
RETURNING *`,
|
||||
[documentId, filename, filePath, file.name, desc_de, desc_en,
|
||||
file.type || 'application/octet-stream', file.size, admin?.email ?? 'admin'],
|
||||
)
|
||||
await logAudit(null, 'dataroom_document_uploaded', { document_id: documentId, filename, file_size: file.size }, request, undefined, undefined, admin?.id)
|
||||
inserted.push(rows[0])
|
||||
}
|
||||
|
||||
await logAudit(null, 'dataroom_document_uploaded', { document_id: documentId, filename, file_size: file.size }, request, undefined, undefined, admin?.id)
|
||||
return NextResponse.json({ document: rows[0] }, { status: 201 })
|
||||
return NextResponse.json({ documents: inserted }, { status: 201 })
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import pool from '@/lib/db'
|
||||
import { requireAdmin } from '@/lib/admin-auth'
|
||||
import { streamFile } from '@/lib/dataroom-storage'
|
||||
import { translateText } from '@/lib/translate'
|
||||
import path from 'path'
|
||||
|
||||
interface Ctx { params: Promise<{ id: string }> }
|
||||
@@ -31,7 +32,7 @@ export async function GET(request: NextRequest, ctx: Ctx) {
|
||||
}
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`SELECT id, filename, display_name, mime_type, file_size, created_at
|
||||
`SELECT id, filename, display_name, description_de, description_en, mime_type, file_size, created_at
|
||||
FROM dataroom_investor_uploads
|
||||
WHERE investor_id = $1
|
||||
ORDER BY created_at DESC`,
|
||||
@@ -39,3 +40,25 @@ export async function GET(request: NextRequest, ctx: Ctx) {
|
||||
)
|
||||
return NextResponse.json({ uploads: rows })
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest, ctx: Ctx) {
|
||||
const guard = await requireAdmin(request)
|
||||
if (guard.kind === 'response') return guard.response
|
||||
|
||||
const { id: investorId } = await ctx.params
|
||||
const { upload_id, description, description_lang } = await request.json()
|
||||
const lang: 'de' | 'en' = description_lang || 'en'
|
||||
|
||||
const translated = description ? await translateText(description, lang) : null
|
||||
const desc_de = lang === 'de' ? (description || null) : translated
|
||||
const desc_en = lang === 'en' ? (description || null) : translated
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`UPDATE dataroom_investor_uploads SET description_de = $1, description_en = $2
|
||||
WHERE id = $3 AND investor_id = $4
|
||||
RETURNING id, filename, display_name, description_de, description_en, mime_type, file_size, created_at`,
|
||||
[desc_de, desc_en, upload_id, investorId],
|
||||
)
|
||||
if (rows.length === 0) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
return NextResponse.json({ upload: rows[0] })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user