diff --git a/pitch-deck/app/api/admin/dataroom/documents/[id]/route.ts b/pitch-deck/app/api/admin/dataroom/documents/[id]/route.ts
index 46a50a7..971e255 100644
--- a/pitch-deck/app/api/admin/dataroom/documents/[id]/route.ts
+++ b/pitch-deck/app/api/admin/dataroom/documents/[id]/route.ts
@@ -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 *`,
diff --git a/pitch-deck/app/api/admin/dataroom/documents/route.ts b/pitch-deck/app/api/admin/dataroom/documents/route.ts
index 3c548ed..d01b78f 100644
--- a/pitch-deck/app/api/admin/dataroom/documents/route.ts
+++ b/pitch-deck/app/api/admin/dataroom/documents/route.ts
@@ -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 })
}
diff --git a/pitch-deck/app/api/admin/dataroom/investors/[id]/uploads/route.ts b/pitch-deck/app/api/admin/dataroom/investors/[id]/uploads/route.ts
index e2e8df9..6584d20 100644
--- a/pitch-deck/app/api/admin/dataroom/investors/[id]/uploads/route.ts
+++ b/pitch-deck/app/api/admin/dataroom/investors/[id]/uploads/route.ts
@@ -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] })
+}
diff --git a/pitch-deck/app/api/admin/migrate/route.ts b/pitch-deck/app/api/admin/migrate/route.ts
index 73e6c94..8299282 100644
--- a/pitch-deck/app/api/admin/migrate/route.ts
+++ b/pitch-deck/app/api/admin/migrate/route.ts
@@ -155,6 +155,11 @@ export async function POST(request: NextRequest) {
`CREATE INDEX IF NOT EXISTS idx_dataroom_releases_investor ON dataroom_releases(investor_id)`,
`CREATE INDEX IF NOT EXISTS idx_dataroom_releases_document ON dataroom_releases(document_id)`,
`CREATE INDEX IF NOT EXISTS idx_dataroom_uploads_investor ON dataroom_investor_uploads(investor_id)`,
+ // 006 — dataroom bilingual descriptions
+ `ALTER TABLE dataroom_documents ADD COLUMN IF NOT EXISTS description_de TEXT`,
+ `ALTER TABLE dataroom_documents ADD COLUMN IF NOT EXISTS description_en TEXT`,
+ `ALTER TABLE dataroom_investor_uploads ADD COLUMN IF NOT EXISTS description_de TEXT`,
+ `ALTER TABLE dataroom_investor_uploads ADD COLUMN IF NOT EXISTS description_en TEXT`,
]
for (const sql of statements) {
diff --git a/pitch-deck/app/api/dataroom/documents/route.ts b/pitch-deck/app/api/dataroom/documents/route.ts
index 70d7aa4..63c0f76 100644
--- a/pitch-deck/app/api/dataroom/documents/route.ts
+++ b/pitch-deck/app/api/dataroom/documents/route.ts
@@ -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
diff --git a/pitch-deck/app/api/dataroom/uploads/[id]/route.ts b/pitch-deck/app/api/dataroom/uploads/[id]/route.ts
new file mode 100644
index 0000000..f045314
--- /dev/null
+++ b/pitch-deck/app/api/dataroom/uploads/[id]/route.ts
@@ -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] })
+}
diff --git a/pitch-deck/app/api/dataroom/uploads/route.ts b/pitch-deck/app/api/dataroom/uploads/route.ts
index 12fc132..1f09eac 100644
--- a/pitch-deck/app/api/dataroom/uploads/route.ts
+++ b/pitch-deck/app/api/dataroom/uploads/route.ts
@@ -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 })
}
diff --git a/pitch-deck/app/dataroom/page.tsx b/pitch-deck/app/dataroom/page.tsx
index 991c15e..6776a11 100644
--- a/pitch-deck/app/dataroom/page.tsx
+++ b/pitch-deck/app/dataroom/page.tsx
@@ -1,12 +1,14 @@
'use client'
-import { useEffect, useState, useRef } from 'react'
-import { FileText, Download, Upload, Eye, LogOut } from 'lucide-react'
+import { useEffect, useState, useRef, useCallback } from 'react'
+import { FileText, Download, Upload, Eye, LogOut, Pencil, Globe } from 'lucide-react'
interface Doc {
id: string
filename: string
display_name: string | null
+ description_de: string | null
+ description_en: string | null
mime_type: string
file_size: number
released_at: string
@@ -16,6 +18,8 @@ interface MyUpload {
id: string
filename: string
display_name: string | null
+ description_de: string | null
+ description_en: string | null
mime_type: string
file_size: number
created_at: string
@@ -27,57 +31,83 @@ function fmt(bytes: number) {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
-function isPDF(mime: string) {
- return mime === 'application/pdf'
+function isPDF(mime: string) { return mime === 'application/pdf' }
+
+function LangToggle({ lang, onChange }: { lang: 'de' | 'en'; onChange: (l: 'de' | 'en') => void }) {
+ return (
+
+ {(['de', 'en'] as const).map(l => (
+
+ ))}
+
+ )
}
export default function DataroomPage() {
const [docs, setDocs] = useState([])
const [uploads, setUploads] = useState([])
+ const [dragging, setDragging] = useState(false)
const [uploading, setUploading] = useState(false)
const [toast, setToast] = useState(null)
+ const [description, setDescription] = useState('')
+ const [descLang, setDescLang] = useState<'de' | 'en'>('en')
+ const [editingUpload, setEditingUpload] = useState<{ id: string; text: string; lang: 'de' | 'en' } | null>(null)
const fileRef = useRef(null)
- function flash(msg: string) {
- setToast(msg)
- setTimeout(() => setToast(null), 3500)
- }
+ function flash(msg: string) { setToast(msg); setTimeout(() => setToast(null), 3500) }
async function loadAll() {
- const [dr, ur] = await Promise.all([
- fetch('/api/dataroom/documents'),
- fetch('/api/dataroom/uploads'),
- ])
+ const [dr, ur] = await Promise.all([fetch('/api/dataroom/documents'), fetch('/api/dataroom/uploads')])
if (dr.ok) setDocs((await dr.json()).documents)
if (ur.ok) setUploads((await ur.json()).uploads)
}
useEffect(() => { loadAll() }, [])
- async function handleUpload(e: React.ChangeEvent) {
- const file = e.target.files?.[0]
- if (!file) return
+ async function uploadFiles(files: FileList | File[]) {
+ const list = Array.from(files).filter(f => f.size > 0)
+ if (!list.length) return
setUploading(true)
const fd = new FormData()
- fd.append('file', file)
+ list.forEach(f => fd.append('file', f))
+ if (description.trim()) { fd.append('description', description.trim()); fd.append('description_lang', descLang) }
const r = await fetch('/api/dataroom/uploads', { method: 'POST', body: fd })
setUploading(false)
- if (r.ok) { flash('File uploaded successfully'); loadAll() }
- else {
+ if (r.ok) {
+ flash(`${list.length} file${list.length > 1 ? 's' : ''} uploaded`)
+ setDescription(''); loadAll()
+ } else {
const d = await r.json().catch(() => ({}))
flash(d.error || 'Upload failed')
}
- e.target.value = ''
+ }
+
+ const handleDrop = useCallback((e: React.DragEvent) => {
+ e.preventDefault(); setDragging(false)
+ if (e.dataTransfer.files.length) uploadFiles(e.dataTransfer.files)
+ }, [description, descLang])
+
+ async function saveEditDescription() {
+ if (!editingUpload) return
+ const r = await fetch(`/api/dataroom/uploads/${editingUpload.id}`, {
+ method: 'PATCH', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ description: editingUpload.text || null, description_lang: editingUpload.lang }),
+ })
+ if (r.ok) {
+ const updated = (await r.json()).upload as MyUpload
+ setUploads(prev => prev.map(u => u.id === updated.id ? updated : u))
+ setEditingUpload(null); flash('Description saved & translated')
+ } else flash('Save failed')
}
return (
- {/* Header */}
-
- Data Room
-
+
Data Room
BreakPilot ComplAI · Investor Portal
@@ -101,32 +131,30 @@ export default function DataroomPage() {
) : (
{docs.map(doc => (
-
-
+
+
-
{doc.display_name || doc.filename}
-
- {fmt(doc.file_size)} · Released {new Date(doc.released_at).toLocaleDateString()}
-
+
{doc.display_name || doc.filename}
+
{fmt(doc.file_size)} · Released {new Date(doc.released_at).toLocaleDateString()}
+ {(doc.description_en || doc.description_de) && (
+
+ {doc.description_en &&
{doc.description_en}
}
+ {doc.description_de && !doc.description_en &&
{doc.description_de}
}
+ {doc.description_de && doc.description_en &&
{doc.description_de}
}
+
+ )}
@@ -138,40 +166,88 @@ export default function DataroomPage() {
{/* Upload section */}
-
-
Your Documents
-
-
-
-
+ Your Documents
Upload documents you want to share with us — NDAs, term sheets, financial statements, or any other relevant files.
- {uploads.length === 0 ? (
-
-
-
No files uploaded yet.
-
- ) : (
-
- {uploads.map(u => (
-
-
-
-
{u.display_name || u.filename}
-
{fmt(u.file_size)} · {new Date(u.created_at).toLocaleString()}
+ {/* Description field */}
+
+
+
+ {/* Drop zone */}
+
{ e.preventDefault(); setDragging(true) }}
+ onDragLeave={() => setDragging(false)}
+ onDrop={handleDrop}
+ onClick={() => fileRef.current?.click()}
+ className={`border-2 border-dashed rounded-2xl p-10 text-center cursor-pointer transition-all select-none ${dragging ? 'border-indigo-400/60 bg-indigo-500/10' : 'border-white/[0.08] bg-white/[0.02] hover:border-white/20 hover:bg-white/[0.03]'}`}
+ >
+ {uploading
+ ?
Uploading…
+ : <>
+
+
Drop files here
+
or click to browse · multiple files supported
+ >}
+
{ if (e.target.files) uploadFiles(e.target.files); e.target.value = '' }} />
+
+
+ {/* Uploaded files list */}
+ {uploads.length > 0 && (
+
+ {uploads.map(u => {
+ const isEditing = editingUpload?.id === u.id
+ return (
+
+
+
+
+
{u.display_name || u.filename}
+
{fmt(u.file_size)} · {new Date(u.created_at).toLocaleString()}
+
+
+
+
Received
+
+
+ {(u.description_en || u.description_de) && !isEditing && (
+
+ {u.description_en &&
{u.description_en}
}
+ {u.description_de && !u.description_en &&
{u.description_de}
}
+
+ )}
+ {isEditing && editingUpload && (
+
+
+
+
+
+
+
+
+ )}
-
Received
-
- ))}
+ )
+ })}
)}
diff --git a/pitch-deck/app/pitch-admin/(authed)/dataroom/page.tsx b/pitch-deck/app/pitch-admin/(authed)/dataroom/page.tsx
index c7ec07a..f52d3b0 100644
--- a/pitch-deck/app/pitch-admin/(authed)/dataroom/page.tsx
+++ b/pitch-deck/app/pitch-admin/(authed)/dataroom/page.tsx
@@ -1,12 +1,14 @@
'use client'
-import { useEffect, useState, useRef } from 'react'
-import { Upload, FileText, Trash2, X, Share2, Users, ChevronDown, Check, Download } from 'lucide-react'
+import { useEffect, useState, useRef, useCallback } from 'react'
+import { FileText, Trash2, X, Share2, Users, Check, Download, Pencil, Globe } from 'lucide-react'
interface Doc {
id: string
filename: string
display_name: string
+ description_de: string | null
+ description_en: string | null
mime_type: string
file_size: number
uploaded_by: string
@@ -37,6 +39,8 @@ interface InvestorUpload {
id: string
filename: string
display_name: string
+ description_de: string | null
+ description_en: string | null
mime_type: string
file_size: number
created_at: string
@@ -48,22 +52,35 @@ function fmt(bytes: number) {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
+function LangToggle({ lang, onChange }: { lang: 'de' | 'en'; onChange: (l: 'de' | 'en') => void }) {
+ return (
+
+ {(['de', 'en'] as const).map(l => (
+
+ ))}
+
+ )
+}
+
export default function DataroomPage() {
const [docs, setDocs] = useState
([])
const [investors, setInvestors] = useState([])
const [selected, setSelected] = useState(null)
const [releases, setReleases] = useState([])
+ const [dragging, setDragging] = useState(false)
const [uploading, setUploading] = useState(false)
const [busy, setBusy] = useState(false)
const [toast, setToast] = useState(null)
const [tab, setTab] = useState<'documents' | 'uploads'>('documents')
const [investorUploads, setInvestorUploads] = useState>({})
+ const [descEdit, setDescEdit] = useState<{ text: string; lang: 'de' | 'en' } | null>(null)
+ const [editingUpload, setEditingUpload] = useState<{ invId: string; uploadId: string; text: string; lang: 'de' | 'en' } | null>(null)
const fileRef = useRef(null)
- function flash(msg: string) {
- setToast(msg)
- setTimeout(() => setToast(null), 3000)
- }
+ function flash(msg: string) { setToast(msg); setTimeout(() => setToast(null), 3000) }
async function loadDocs() {
const r = await fetch('/api/admin/dataroom/documents')
@@ -96,23 +113,27 @@ export default function DataroomPage() {
useEffect(() => { if (tab === 'uploads' && investors.length > 0) loadInvestorUploads() }, [tab, investors])
async function selectDoc(doc: Doc) {
- setSelected(doc)
+ setSelected(doc); setDescEdit(null)
await loadReleases(doc.id)
}
- async function uploadFile(e: React.ChangeEvent) {
- const file = e.target.files?.[0]
- if (!file) return
+ async function uploadFiles(files: FileList | File[]) {
+ const list = Array.from(files).filter(f => f.size > 0)
+ if (!list.length) return
setUploading(true)
const fd = new FormData()
- fd.append('file', file)
+ list.forEach(f => fd.append('file', f))
const r = await fetch('/api/admin/dataroom/documents', { method: 'POST', body: fd })
setUploading(false)
- if (r.ok) { flash('Uploaded'); loadDocs() }
+ if (r.ok) { flash(`Uploaded ${list.length} file${list.length > 1 ? 's' : ''}`); loadDocs() }
else { const d = await r.json().catch(() => ({})); flash(d.error || 'Upload failed') }
- e.target.value = ''
}
+ const handleDrop = useCallback((e: React.DragEvent) => {
+ e.preventDefault(); setDragging(false)
+ if (e.dataTransfer.files.length) uploadFiles(e.dataTransfer.files)
+ }, [])
+
async function deleteDoc(id: string) {
if (!confirm('Delete this document? All releases will be removed.')) return
setBusy(true)
@@ -129,27 +150,55 @@ export default function DataroomPage() {
await fetch(`/api/admin/dataroom/documents/${selected.id}/release/${investorId}`, { method: 'DELETE' })
} else {
await fetch(`/api/admin/dataroom/documents/${selected.id}/release`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ investor_ids: [investorId] }),
})
}
- setBusy(false)
- await loadReleases(selected.id)
- loadDocs()
+ setBusy(false); await loadReleases(selected.id); loadDocs()
}
async function releaseAll() {
if (!selected || investors.length === 0) return
setBusy(true)
await fetch(`/api/admin/dataroom/documents/${selected.id}/release`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ investor_ids: investors.map(i => i.id) }),
})
+ setBusy(false); await loadReleases(selected.id); loadDocs()
+ }
+
+ async function saveDescription() {
+ if (!selected || !descEdit) return
+ setBusy(true)
+ const r = await fetch(`/api/admin/dataroom/documents/${selected.id}`, {
+ method: 'PATCH', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ description: descEdit.text || null, description_lang: descEdit.lang }),
+ })
setBusy(false)
- await loadReleases(selected.id)
- loadDocs()
+ if (r.ok) {
+ const updated = (await r.json()).document as Doc
+ setDocs(prev => prev.map(d => d.id === updated.id ? { ...d, ...updated } : d))
+ setSelected(prev => prev ? { ...prev, ...updated } : prev)
+ setDescEdit(null); flash('Description saved & translated')
+ } else flash('Save failed')
+ }
+
+ async function saveUploadDescription() {
+ if (!editingUpload) return
+ setBusy(true)
+ const r = await fetch(`/api/admin/dataroom/investors/${editingUpload.invId}/uploads`, {
+ method: 'PATCH', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ upload_id: editingUpload.uploadId, description: editingUpload.text || null, description_lang: editingUpload.lang }),
+ })
+ setBusy(false)
+ if (r.ok) {
+ const updated = (await r.json()).upload as InvestorUpload
+ setInvestorUploads(prev => ({
+ ...prev,
+ [editingUpload.invId]: (prev[editingUpload.invId] || []).map(u => u.id === updated.id ? updated : u),
+ }))
+ setEditingUpload(null); flash('Description saved & translated')
+ } else flash('Save failed')
}
const releasedIds = new Set(releases.map(r => r.investor_id))
@@ -161,55 +210,50 @@ export default function DataroomPage() {
Data Room
-
-
+ {(['documents', 'uploads'] as const).map(t => (
+
+ ))}
{tab === 'documents' && (
- {/* Document list */}
+ {/* Drop zone + document list */}
-
-
{docs.length} document{docs.length !== 1 ? 's' : ''}
-
-
+
{ e.preventDefault(); setDragging(true) }}
+ onDragLeave={() => setDragging(false)}
+ onDrop={handleDrop}
+ onClick={() => fileRef.current?.click()}
+ className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all select-none ${dragging ? 'border-indigo-400/60 bg-indigo-500/10' : 'border-white/[0.08] bg-white/[0.02] hover:border-white/20 hover:bg-white/[0.03]'}`}
+ >
+ {uploading
+ ?
Uploading…
+ : <>
+
Drop files here
+
or click to browse · multiple files supported
+ >}
+
{ if (e.target.files) uploadFiles(e.target.files); e.target.value = '' }} />
- {docs.length === 0 && (
-
- No documents yet. Upload the first one.
-
+ {docs.length > 0 && (
+
{docs.length} document{docs.length !== 1 ? 's' : ''}
)}
{docs.map(doc => (
-