From c89e46a82895f2203ada6ae4f805caa5e606232f Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sat, 9 May 2026 08:07:58 +0200 Subject: [PATCH] feat: Dokumenten Upload im Normenrecherche-Tab Drag & Drop Upload-Zone fuer kundeneigene PDFs (Normen, Spezifikationen). Tenant-isoliert, Status-Tracking, Backend-Placeholder fuer RAG-Pipeline. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../norms/_components/DocumentUpload.tsx | 197 ++++++++++++++++++ .../app/sdk/iace/[projectId]/norms/page.tsx | 1 + 2 files changed, 198 insertions(+) create mode 100644 admin-compliance/app/sdk/iace/[projectId]/norms/_components/DocumentUpload.tsx diff --git a/admin-compliance/app/sdk/iace/[projectId]/norms/_components/DocumentUpload.tsx b/admin-compliance/app/sdk/iace/[projectId]/norms/_components/DocumentUpload.tsx new file mode 100644 index 0000000..ccac591 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/norms/_components/DocumentUpload.tsx @@ -0,0 +1,197 @@ +'use client' + +import { useState, useEffect, useCallback, useRef } from 'react' + +interface UploadedDoc { + id: string + filename: string + size_bytes: number + status: 'uploaded' | 'processing' | 'indexed' + created_at: string +} + +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` +} + +const STATUS_BADGE: Record = { + uploaded: { label: 'Hochgeladen', cls: 'bg-gray-100 text-gray-600' }, + processing: { label: 'Wird verarbeitet', cls: 'bg-yellow-100 text-yellow-700 animate-pulse' }, + indexed: { label: 'Durchsuchbar', cls: 'bg-green-100 text-green-700' }, +} + +const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50 MB + +export function DocumentUpload({ projectId }: { projectId: string }) { + const [docs, setDocs] = useState([]) + const [loading, setLoading] = useState(true) + const [backendReady, setBackendReady] = useState(true) + const [collapsed, setCollapsed] = useState(true) + const [dragging, setDragging] = useState(false) + const [uploading, setUploading] = useState(false) + const [error, setError] = useState(null) + const fileInputRef = useRef(null) + const base = `/api/sdk/v1/iace/projects/${projectId}/documents` + + const fetchDocs = useCallback(async () => { + try { + const r = await fetch(base) + if (r.status === 404) { setBackendReady(false); return } + if (!r.ok) return + const json = await r.json() + setDocs(Array.isArray(json) ? json : json.documents ?? []) + } catch { + setBackendReady(false) + } finally { + setLoading(false) + } + }, [base]) + + useEffect(() => { fetchDocs() }, [fetchDocs]) + + const uploadFiles = async (files: FileList | File[]) => { + setError(null) + const valid = Array.from(files).filter((f) => { + if (!f.name.toLowerCase().endsWith('.pdf')) { setError('Nur PDF-Dateien erlaubt.'); return false } + if (f.size > MAX_FILE_SIZE) { setError(`${f.name} ist groesser als 50 MB.`); return false } + return true + }) + if (valid.length === 0) return + + setUploading(true) + for (const file of valid) { + const form = new FormData() + form.append('file', file) + try { + const r = await fetch(base, { method: 'POST', body: form }) + if (r.status === 404) { setBackendReady(false); break } + if (!r.ok) { setError(`Fehler beim Hochladen von ${file.name}`); continue } + } catch { + setBackendReady(false) + break + } + } + setUploading(false) + await fetchDocs() + } + + const deleteDoc = async (docId: string) => { + try { + const r = await fetch(`${base}/${docId}`, { method: 'DELETE' }) + if (r.ok || r.status === 204) setDocs((prev) => prev.filter((d) => d.id !== docId)) + } catch { /* ignore */ } + } + + const onDrop = (e: React.DragEvent) => { + e.preventDefault() + setDragging(false) + if (e.dataTransfer.files.length) uploadFiles(e.dataTransfer.files) + } + + return ( +
+ {/* Header — collapsible toggle */} + + + {!collapsed && ( +
+ {/* Info text */} +

+ Laden Sie hier Ihre eigenen Dokumente hoch (z.B. Normen, technische Spezifikationen, Pruefberichte). + Die Dokumente werden fuer Ihr Unternehmen indexiert und stehen in der Normenrecherche als zusaetzliche + Quelle zur Verfuegung. Hochgeladene Dokumente sind nur fuer Ihren Mandanten sichtbar. +

+ + {!backendReady ? ( +
+ Backend wird vorbereitet — Die Dokumenten-Upload-Funktion wird derzeit eingerichtet. + Bitte versuchen Sie es spaeter erneut. +
+ ) : ( + <> + {/* Drop zone */} +
{ e.preventDefault(); setDragging(true) }} + onDragLeave={() => setDragging(false)} + onDrop={onDrop} + onClick={() => fileInputRef.current?.click()} + className={`relative flex flex-col items-center justify-center gap-2 p-8 rounded-lg border-2 border-dashed cursor-pointer transition-colors ${ + dragging + ? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20' + : 'border-gray-300 dark:border-gray-600 hover:border-purple-400 hover:bg-gray-50 dark:hover:bg-gray-700/50' + }`} + > + + + +

+ {uploading ? 'Wird hochgeladen...' : 'PDF-Dateien hierher ziehen oder klicken'} +

+

Max. 50 MB pro Datei

+ { if (e.target.files?.length) uploadFiles(e.target.files); e.target.value = '' }} + /> +
+ + {error && ( +

{error}

+ )} + + {/* File list */} + {loading ? null : docs.length > 0 && ( +
+ {docs.map((doc) => { + const badge = STATUS_BADGE[doc.status] ?? STATUS_BADGE.uploaded + return ( +
+ + + +
+

{doc.filename}

+

{formatSize(doc.size_bytes)}

+
+ {badge.label} + +
+ ) + })} +
+ )} + + )} +
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/norms/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/norms/page.tsx index 4f210e1..8f77838 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/norms/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/norms/page.tsx @@ -2,6 +2,7 @@ import { useParams } from 'next/navigation' import { SuggestedNorms } from '../_components/SuggestedNorms' +import { DocumentUpload } from './_components/DocumentUpload' export default function NormsPage() { const params = useParams()