'use client' import React, { useState, useCallback, useEffect } from 'react' import { useRouter } from 'next/navigation' // ============================================================================= // TYPES // ============================================================================= export interface UploadedDocument { id: string name: string type: string size: number uploadedAt: Date extractedVersion?: string extractedContent?: ExtractedContent status: 'uploading' | 'processing' | 'ready' | 'error' error?: string } export interface ExtractedContent { title?: string version?: string lastModified?: string sections?: ExtractedSection[] metadata?: Record } export interface ExtractedSection { title: string content: string type?: string } export interface DocumentUploadSectionProps { /** Type of document being uploaded (tom, dsfa, vvt, loeschfristen, etc.) */ documentType: 'tom' | 'dsfa' | 'vvt' | 'loeschfristen' | 'consent' | 'policy' | 'custom' /** Title displayed in the upload section */ title?: string /** Description text */ description?: string /** Accepted file types */ acceptedTypes?: string /** Callback when document is uploaded and processed */ onDocumentProcessed?: (doc: UploadedDocument) => void /** Callback to open document in workflow editor */ onOpenInEditor?: (doc: UploadedDocument) => void /** Session ID for QR upload */ sessionId?: string /** Custom CSS classes */ className?: string } // ============================================================================= // ICONS // ============================================================================= const UploadIcon = () => ( ) const QRIcon = () => ( ) const DocumentIcon = () => ( ) const CheckIcon = () => ( ) const EditIcon = () => ( ) const CloseIcon = () => ( ) // ============================================================================= // HELPER FUNCTIONS // ============================================================================= function formatFileSize(bytes: number): string { if (bytes === 0) return '0 B' const k = 1024 const sizes = ['B', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] } function detectVersionFromFilename(filename: string): string | undefined { // Common version patterns: v1.0, V2.1, _v3, -v1.2.3, version-2 const patterns = [ /[vV](\d+(?:\.\d+)*)/, /version[_-]?(\d+(?:\.\d+)*)/i, /[_-]v?(\d+\.\d+(?:\.\d+)?)[_-]/, ] for (const pattern of patterns) { const match = filename.match(pattern) if (match) { return match[1] } } return undefined } function suggestNextVersion(currentVersion?: string): string { if (!currentVersion) return '1.0' const parts = currentVersion.split('.').map(Number) if (parts.length >= 2) { parts[parts.length - 1] += 1 } else { parts.push(1) } return parts.join('.') } // ============================================================================= // QR CODE MODAL // ============================================================================= interface QRModalProps { isOpen: boolean onClose: () => void sessionId: string onFileUploaded?: (file: File) => void } function QRCodeModal({ isOpen, onClose, sessionId }: QRModalProps) { const [uploadUrl, setUploadUrl] = useState('') const [qrCodeUrl, setQrCodeUrl] = useState(null) useEffect(() => { if (!isOpen) return let baseUrl = typeof window !== 'undefined' ? window.location.origin : '' // Hostname to IP mapping for local network const hostnameToIP: Record = { 'macmini': '192.168.178.100', 'macmini.local': '192.168.178.100', } Object.entries(hostnameToIP).forEach(([hostname, ip]) => { if (baseUrl.includes(hostname)) { baseUrl = baseUrl.replace(hostname, ip) } }) // Force HTTP for mobile access (SSL cert is for hostname, not IP) // This is safe because it's only used on the local network if (baseUrl.startsWith('https://')) { baseUrl = baseUrl.replace('https://', 'http://') } const uploadPath = `/upload/sdk/${sessionId}` const fullUrl = `${baseUrl}${uploadPath}` setUploadUrl(fullUrl) const qrApiUrl = `https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${encodeURIComponent(fullUrl)}` setQrCodeUrl(qrApiUrl) }, [isOpen, sessionId]) const copyToClipboard = async () => { try { await navigator.clipboard.writeText(uploadUrl) } catch (err) { console.error('Copy failed:', err) } } if (!isOpen) return null return (

Mit Handy hochladen

QR-Code scannen

{qrCodeUrl ? ( QR Code ) : (
)}

Scannen Sie den Code mit Ihrem Handy,
um Dokumente hochzuladen.

Oder Link teilen:

Hinweis: Ihr Handy muss im gleichen Netzwerk sein.

) } // ============================================================================= // MAIN COMPONENT // ============================================================================= export function DocumentUploadSection({ documentType, title, description, acceptedTypes = '.pdf,.docx,.doc', onDocumentProcessed, onOpenInEditor, sessionId, className = '', }: DocumentUploadSectionProps) { const router = useRouter() const [isDragging, setIsDragging] = useState(false) const [uploadedDocs, setUploadedDocs] = useState([]) const [showQRModal, setShowQRModal] = useState(false) const [isExpanded, setIsExpanded] = useState(false) const effectiveSessionId = sessionId || `sdk-${documentType}-${Date.now()}` const defaultTitles: Record = { tom: 'Bestehende TOMs hochladen', dsfa: 'Bestehende DSFA hochladen', vvt: 'Bestehendes VVT hochladen', loeschfristen: 'Bestehende Löschfristen hochladen', consent: 'Bestehende Einwilligungsdokumente hochladen', policy: 'Bestehende Richtlinie hochladen', custom: 'Dokument hochladen', } const defaultDescriptions: Record = { tom: 'Laden Sie Ihre bestehenden technischen und organisatorischen Maßnahmen hoch. Wir erkennen die Version und zeigen Ihnen, was aktualisiert werden sollte.', dsfa: 'Laden Sie Ihre bestehende Datenschutz-Folgenabschätzung hoch. Wir analysieren den Inhalt und schlagen Ergänzungen vor.', vvt: 'Laden Sie Ihr bestehendes Verarbeitungsverzeichnis hoch. Wir erkennen die Struktur und helfen bei der Aktualisierung.', loeschfristen: 'Laden Sie Ihre bestehenden Löschfristen-Dokumente hoch. Wir analysieren die Fristen und prüfen auf Vollständigkeit.', consent: 'Laden Sie Ihre bestehenden Einwilligungsdokumente hoch.', policy: 'Laden Sie Ihre bestehende Richtlinie hoch.', custom: 'Laden Sie ein bestehendes Dokument hoch.', } const displayTitle = title || defaultTitles[documentType] const displayDescription = description || defaultDescriptions[documentType] // Drag & Drop handlers const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragging(true) }, []) const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragging(false) }, []) const processFile = useCallback(async (file: File) => { const docId = `doc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` const newDoc: UploadedDocument = { id: docId, name: file.name, type: file.type, size: file.size, uploadedAt: new Date(), extractedVersion: detectVersionFromFilename(file.name), status: 'uploading', } setUploadedDocs(prev => [...prev, newDoc]) setIsExpanded(true) try { // Upload to API const formData = new FormData() formData.append('file', file) formData.append('documentType', documentType) formData.append('sessionId', effectiveSessionId) const response = await fetch('/api/sdk/v1/documents/upload', { method: 'POST', body: formData, }) if (!response.ok) { throw new Error('Upload fehlgeschlagen') } const result = await response.json() const updatedDoc: UploadedDocument = { ...newDoc, status: 'ready', extractedVersion: result.extractedVersion || newDoc.extractedVersion, extractedContent: result.extractedContent, } setUploadedDocs(prev => prev.map(d => d.id === docId ? updatedDoc : d)) if (onDocumentProcessed) { onDocumentProcessed(updatedDoc) } } catch (error) { setUploadedDocs(prev => prev.map(d => d.id === docId ? { ...d, status: 'error', error: error instanceof Error ? error.message : 'Fehler' } : d )) } }, [documentType, effectiveSessionId, onDocumentProcessed]) const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragging(false) const files = Array.from(e.dataTransfer.files).filter(f => f.type === 'application/pdf' || f.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || f.type === 'application/msword' ) files.forEach(processFile) }, [processFile]) const handleFileSelect = useCallback((e: React.ChangeEvent) => { if (!e.target.files) return Array.from(e.target.files).forEach(processFile) e.target.value = '' // Reset input }, [processFile]) const removeDocument = useCallback((docId: string) => { setUploadedDocs(prev => prev.filter(d => d.id !== docId)) }, []) const handleOpenInEditor = useCallback((doc: UploadedDocument) => { if (onOpenInEditor) { onOpenInEditor(doc) } else { // Default: navigate to workflow editor router.push(`/sdk/workflow?documentType=${documentType}&documentId=${doc.id}`) } }, [documentType, onOpenInEditor, router]) return (
{/* Header */} {/* Expanded Content */} {isExpanded && (

{displayDescription}

{/* Upload Area */}

{isDragging ? 'Dateien hier ablegen' : 'Dateien hierher ziehen'}

oder klicken zum Auswählen (PDF, DOCX)

{/* QR Upload Button */} {/* Uploaded Documents */} {uploadedDocs.length > 0 && (

Hochgeladene Dokumente:

{uploadedDocs.map(doc => (
{doc.status === 'uploading' || doc.status === 'processing' ? (
) : doc.status === 'ready' ? (
) : (
!
)}

{doc.name}

{formatFileSize(doc.size)} {doc.extractedVersion && ( <> Version {doc.extractedVersion} Vorschlag: v{suggestNextVersion(doc.extractedVersion)} )}
{doc.error && (

{doc.error}

)}
{doc.status === 'ready' && ( )}
))}
)} {/* Extracted Content Preview */} {uploadedDocs.some(d => d.status === 'ready' && d.extractedContent) && (

Erkannte Inhalte

{uploadedDocs .filter(d => d.status === 'ready' && d.extractedContent) .map(doc => (
{doc.extractedContent?.title && (

Titel: {doc.extractedContent.title}

)} {doc.extractedContent?.sections && (

Abschnitte: {doc.extractedContent.sections.length} gefunden

)}
)) }
)}
)} {/* QR Modal */} setShowQRModal(false)} sessionId={effectiveSessionId} />
) } export default DocumentUploadSection