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

- 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:
Sharang Parnerkar
2026-05-01 21:00:36 +02:00
parent 370143b643
commit f130c45ca8
10 changed files with 521 additions and 198 deletions
@@ -4,13 +4,14 @@ import { getSessionFromCookie } from '@/lib/auth'
export const dynamic = 'force-dynamic'
export async function GET(request: NextRequest) {
export async function GET(_request: NextRequest) {
const session = await getSessionFromCookie()
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const investorId = session.sub
const { rows } = await pool.query(
`SELECT d.id, d.filename, d.display_name, d.mime_type, d.file_size, r.released_at
`SELECT d.id, d.filename, d.display_name, d.description_de, d.description_en,
d.mime_type, d.file_size, r.released_at
FROM dataroom_releases r
JOIN dataroom_documents d ON d.id = r.document_id
WHERE r.investor_id = $1
@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from 'next/server'
import pool from '@/lib/db'
import { getSessionFromCookie } from '@/lib/auth'
import { translateText } from '@/lib/translate'
interface Ctx { params: Promise<{ id: string }> }
export async function PATCH(request: NextRequest, ctx: Ctx) {
const session = await getSessionFromCookie()
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const { id } = await ctx.params
const { 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, id, session.sub],
)
if (rows.length === 0) return NextResponse.json({ error: 'Not found' }, { status: 404 })
return NextResponse.json({ upload: rows[0] })
}
+36 -17
View File
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
import pool from '@/lib/db'
import { investorUploadDir, saveFile, safeName } from '@/lib/dataroom-storage'
import { logAudit, getSessionFromCookie } from '@/lib/auth'
import { translateText } from '@/lib/translate'
import { randomUUID } from 'crypto'
export const dynamic = 'force-dynamic'
@@ -14,7 +15,7 @@ export async function GET(_request: NextRequest) {
const investorId = session.sub
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`,
@@ -30,24 +31,42 @@ export async function POST(request: NextRequest) {
const sessionId = session.sessionId
const formData = await request.formData()
const file = formData.get('file') as File | null
const displayName = (formData.get('display_name') as string | null) || null
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 })
if (!file || file.size === 0) return NextResponse.json({ error: 'No file provided' }, { status: 400 })
if (file.size > MAX_BYTES) return NextResponse.json({ error: `File exceeds ${process.env.DATAROOM_MAX_UPLOAD_MB || 50}MB limit` }, { status: 413 })
const oversized = validFiles.find(f => f.size > MAX_BYTES)
if (oversized) return NextResponse.json({ error: `File "${oversized.name}" exceeds ${process.env.DATAROOM_MAX_UPLOAD_MB || 50}MB limit` }, { status: 413 })
const uploadId = randomUUID()
const filename = safeName(file.name)
const buffer = Buffer.from(await file.arrayBuffer())
const filePath = await saveFile(investorUploadDir(investorId, uploadId), filename, buffer)
const description = (formData.get('description') as string | null) || null
const descLang = (formData.get('description_lang') as 'de' | 'en' | null) || 'en'
const { rows } = await pool.query(
`INSERT INTO dataroom_investor_uploads (id, investor_id, filename, file_path, display_name, mime_type, file_size)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, filename, display_name, mime_type, file_size, created_at`,
[uploadId, investorId, filename, filePath, displayName || file.name, file.type || 'application/octet-stream', file.size],
)
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)
}
await logAudit(investorId, 'dataroom_investor_uploaded', { upload_id: uploadId, filename, file_size: file.size }, request, undefined, sessionId ?? undefined)
return NextResponse.json({ upload: rows[0] }, { status: 201 })
const inserted = []
for (const file of validFiles) {
const uploadId = randomUUID()
const filename = safeName(file.name)
const buffer = Buffer.from(await file.arrayBuffer())
const filePath = await saveFile(investorUploadDir(investorId, uploadId), filename, buffer)
const { rows } = await pool.query(
`INSERT INTO dataroom_investor_uploads
(id, investor_id, filename, file_path, display_name, description_de, description_en, mime_type, file_size)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
RETURNING id, filename, display_name, description_de, description_en, mime_type, file_size, created_at`,
[uploadId, investorId, filename, filePath, file.name, desc_de, desc_en,
file.type || 'application/octet-stream', file.size],
)
await logAudit(investorId, 'dataroom_investor_uploaded', { upload_id: uploadId, filename, file_size: file.size }, request, undefined, sessionId)
inserted.push(rows[0])
}
return NextResponse.json({ uploads: inserted }, { status: 201 })
}