Files
breakpilot-compliance/admin-compliance/components/sdk/DocumentUpload/DocumentUploadSection.tsx

357 lines
13 KiB
TypeScript

'use client'
import React, { useState, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import {
type UploadedDocument,
type DocumentUploadSectionProps,
formatFileSize,
detectVersionFromFilename,
suggestNextVersion,
} from './DocumentUploadTypes'
import {
UploadIcon,
QRIcon,
DocumentIcon,
CheckIcon,
EditIcon,
CloseIcon,
} from './DocumentUploadIcons'
import { QRCodeModal } from './DocumentUploadQRModal'
export type {
UploadedDocument,
ExtractedContent,
ExtractedSection,
DocumentUploadSectionProps,
} from './DocumentUploadTypes'
// =============================================================================
// 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<UploadedDocument[]>([])
const [showQRModal, setShowQRModal] = useState(false)
const [isExpanded, setIsExpanded] = useState(false)
const effectiveSessionId = sessionId || `sdk-${documentType}-${Date.now()}`
const defaultTitles: Record<string, string> = {
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<string, string> = {
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<HTMLInputElement>) => {
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 (
<div className={`bg-white border border-gray-200 rounded-xl overflow-hidden ${className}`}>
{/* Header */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between p-4 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-50 flex items-center justify-center text-blue-600">
<DocumentIcon />
</div>
<div className="text-left">
<h3 className="font-medium text-gray-900">{displayTitle}</h3>
<p className="text-sm text-gray-500">
{uploadedDocs.length > 0
? `${uploadedDocs.length} Dokument(e) hochgeladen`
: 'Optional - bestehende Dokumente importieren'
}
</p>
</div>
</div>
<svg
className={`w-5 h-5 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* Expanded Content */}
{isExpanded && (
<div className="p-4 pt-0 space-y-4">
<p className="text-sm text-gray-600">{displayDescription}</p>
{/* Upload Area */}
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`relative p-6 rounded-lg border-2 border-dashed transition-colors ${
isDragging
? 'border-purple-400 bg-purple-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<input
type="file"
accept={acceptedTypes}
multiple
onChange={handleFileSelect}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
<div className="text-center">
<div className="text-gray-400 mb-2">
<UploadIcon />
</div>
<p className="text-sm font-medium text-gray-700">
{isDragging ? 'Dateien hier ablegen' : 'Dateien hierher ziehen'}
</p>
<p className="text-xs text-gray-500 mt-1">
oder klicken zum Auswählen (PDF, DOCX)
</p>
</div>
</div>
{/* QR Upload Button */}
<button
onClick={() => setShowQRModal(true)}
className="w-full flex items-center justify-center gap-2 px-4 py-2 text-sm text-purple-600 bg-purple-50 hover:bg-purple-100 rounded-lg transition-colors"
>
<QRIcon />
<span>Mit Handy hochladen (QR-Code)</span>
</button>
{/* Uploaded Documents */}
{uploadedDocs.length > 0 && (
<div className="space-y-2">
<p className="text-sm font-medium text-gray-700">Hochgeladene Dokumente:</p>
{uploadedDocs.map(doc => (
<div
key={doc.id}
className={`flex items-center gap-3 p-3 rounded-lg border ${
doc.status === 'error'
? 'bg-red-50 border-red-200'
: doc.status === 'ready'
? 'bg-green-50 border-green-200'
: 'bg-gray-50 border-gray-200'
}`}
>
<div className="flex-shrink-0">
{doc.status === 'uploading' || doc.status === 'processing' ? (
<div className="w-5 h-5 border-2 border-purple-500 border-t-transparent rounded-full animate-spin" />
) : doc.status === 'ready' ? (
<div className="w-5 h-5 rounded-full bg-green-500 text-white flex items-center justify-center">
<CheckIcon />
</div>
) : (
<div className="w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center text-xs">!</div>
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">{doc.name}</p>
<div className="flex items-center gap-2 text-xs text-gray-500">
<span>{formatFileSize(doc.size)}</span>
{doc.extractedVersion && (
<>
<span></span>
<span className="text-purple-600">Version {doc.extractedVersion}</span>
<span></span>
<span className="text-green-600 font-medium">
Vorschlag: v{suggestNextVersion(doc.extractedVersion)}
</span>
</>
)}
</div>
{doc.error && (
<p className="text-xs text-red-600 mt-1">{doc.error}</p>
)}
</div>
<div className="flex items-center gap-1">
{doc.status === 'ready' && (
<button
onClick={() => handleOpenInEditor(doc)}
className="p-2 text-purple-600 hover:bg-purple-100 rounded-lg transition-colors"
title="Im Editor öffnen"
>
<EditIcon />
</button>
)}
<button
onClick={() => removeDocument(doc.id)}
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
title="Entfernen"
>
<CloseIcon />
</button>
</div>
</div>
))}
</div>
)}
{/* Extracted Content Preview */}
{uploadedDocs.some(d => d.status === 'ready' && d.extractedContent) && (
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="text-sm font-medium text-blue-900 mb-2">Erkannte Inhalte</h4>
{uploadedDocs
.filter(d => d.status === 'ready' && d.extractedContent)
.map(doc => (
<div key={doc.id} className="text-sm text-blue-800">
{doc.extractedContent?.title && (
<p><strong>Titel:</strong> {doc.extractedContent.title}</p>
)}
{doc.extractedContent?.sections && (
<p><strong>Abschnitte:</strong> {doc.extractedContent.sections.length} gefunden</p>
)}
</div>
))
}
<button
onClick={() => uploadedDocs.find(d => d.status === 'ready') && handleOpenInEditor(uploadedDocs.find(d => d.status === 'ready')!)}
className="mt-3 w-full px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors"
>
Im Änderungsmodus öffnen
</button>
</div>
)}
</div>
)}
{/* QR Modal */}
<QRCodeModal
isOpen={showQRModal}
onClose={() => setShowQRModal(false)}
sessionId={effectiveSessionId}
/>
</div>
)
}
export default DocumentUploadSection