From ed275f49092f490b81b586f5d366e036aa0c7236 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Mon, 9 Feb 2026 10:18:39 +0100 Subject: [PATCH] fix(admin-v2): Restore HEAD SDK files for compatibility with new pages Restore the SDK context, types, and component files to the HEAD version since newer pages (company-profile, import) depend on these API changes. Co-Authored-By: Claude Opus 4.6 --- admin-v2/app/(admin)/ai/ocr-compare/page.tsx | 1412 ----------------- .../document-generator/components/index.ts | 9 - .../app/(sdk)/sdk/document-generator/page.tsx | 793 --------- admin-v2/app/(sdk)/sdk/dsfa/page.tsx | 591 +++---- admin-v2/app/(sdk)/sdk/page.tsx | 241 ++- admin-v2/app/api/sdk/v1/checkpoints/route.ts | 12 +- .../app/api/sdk/v1/dsgvo/[...path]/route.ts | 130 ++ admin-v2/app/api/sdk/v1/flow/route.ts | 19 +- .../components/sdk/CommandBar/CommandBar.tsx | 6 +- .../DocumentUpload/DocumentUploadSection.tsx | 6 + .../SDKPipelineSidebar/SDKPipelineSidebar.tsx | 245 ++- .../components/sdk/Sidebar/SDKSidebar.tsx | 341 ++-- admin-v2/lib/sdk/context.tsx | 121 +- admin-v2/lib/sdk/demo-data/index.ts | 36 +- admin-v2/lib/sdk/types.ts | 13 + 15 files changed, 1054 insertions(+), 2921 deletions(-) delete mode 100644 admin-v2/app/(admin)/ai/ocr-compare/page.tsx delete mode 100644 admin-v2/app/(sdk)/sdk/document-generator/components/index.ts delete mode 100644 admin-v2/app/(sdk)/sdk/document-generator/page.tsx create mode 100644 admin-v2/app/api/sdk/v1/dsgvo/[...path]/route.ts diff --git a/admin-v2/app/(admin)/ai/ocr-compare/page.tsx b/admin-v2/app/(admin)/ai/ocr-compare/page.tsx deleted file mode 100644 index 73a0bb4..0000000 --- a/admin-v2/app/(admin)/ai/ocr-compare/page.tsx +++ /dev/null @@ -1,1412 +0,0 @@ -'use client' - -/** - * OCR Comparison Tool - * - * Zeigt Original-PDF neben den Extraktionsergebnissen von verschiedenen OCR-Methoden. - * Ermoeglicht direkten visuellen Vergleich mit voller Breite. - * Bietet Session-Historie fuer Verbesserungsvergleiche. - */ - -import { useState, useEffect, useCallback, useMemo } from 'react' -import { PagePurpose } from '@/components/common/PagePurpose' -import { AIToolsSidebarResponsive } from '@/components/ai/AIToolsSidebar' -import { QRCodeUpload, UploadedFile } from '@/components/QRCodeUpload' -import { GridOverlay, GridStats, GridLegend, CellCorrectionDialog, BlockReviewPanel, BlockReviewSummary, getCellBlockNumber } from '@/components/ocr' -import type { GridData, GridCell, BlockReviewData, BlockStatus } from '@/components/ocr' - -interface VocabEntry { - english: string - german: string - example?: string -} - -interface MethodResult { - name: string - model: string - duration_seconds: number - vocabulary_count: number - vocabulary: VocabEntry[] - confidence: number - error?: string - success: boolean -} - -interface ComparisonResult { - session_id: string - page_number: number - methods: Record - comparison: { - found_by_all_methods: Array<{ english: string; german: string; methods: string[] }> - found_by_some_methods: Array<{ english: string; german: string; methods: string[] }> - total_unique_vocabulary: number - agreement_rate: number - } - recommendation: { - best_method: string - reason: string - } -} - -interface SessionInfo { - id: string - name: string - created_at: string - page_count?: number -} - -// OCR-Methoden Konfiguration -const OCR_METHODS = { - local_llm: { - id: 'local_llm', - name: 'Loesung A: Lokales 32B LLM', - shortName: 'A: Local LLM', - model: 'qwen2.5:32b extern', - color: 'slate', - description: 'Externes 32B LLM', - enabled: true, - }, - vision_llm: { - id: 'vision_llm', - name: 'Loesung B: Vision LLM', - shortName: 'B: Vision LLM', - model: 'qwen2.5vl:32b', - color: 'blue', - description: 'Direkte Bild-zu-Text Extraktion', - enabled: true, - }, - paddleocr: { - id: 'paddleocr', - name: 'Loesung C: PaddleOCR', - shortName: 'C: PaddleOCR', - model: 'paddleocr (x86)', - color: 'red', - description: 'Aktuell deaktiviert (Rosetta)', - enabled: false, - }, - tesseract: { - id: 'tesseract', - name: 'Loesung D: Tesseract', - shortName: 'D: Tesseract', - model: 'tesseract + qwen2.5:14b', - color: 'purple', - description: 'ARM64-nativ, Standard', - enabled: true, - }, -} - -export default function OCRComparePage() { - // Session State - const [sessionId, setSessionId] = useState(null) - const [pageCount, setPageCount] = useState(0) - const [selectedPage, setSelectedPage] = useState(0) - const [thumbnails, setThumbnails] = useState([]) - const [loadingThumbnails, setLoadingThumbnails] = useState(false) - - // Session History - const [sessions, setSessions] = useState([]) - const [loadingSessions, setLoadingSessions] = useState(false) - const [showHistory, setShowHistory] = useState(false) - - // Comparison State - const [comparing, setComparing] = useState(false) - const [result, setResult] = useState(null) - const [error, setError] = useState(null) - const [uploading, setUploading] = useState(false) - - // Method Selection - const [selectedMethods, setSelectedMethods] = useState(['vision_llm', 'tesseract']) - - // QR Upload State - const [showQRModal, setShowQRModal] = useState(false) - const [qrUploadSessionId, setQrUploadSessionId] = useState('') - const [mobileUploadedFiles, setMobileUploadedFiles] = useState([]) - - // View Mode State - const [isFullscreen, setIsFullscreen] = useState(false) - const [expandedMethod, setExpandedMethod] = useState(null) // For single document view - const [visibleMethods, setVisibleMethods] = useState([]) // For custom multi-column view - - // Grid Detection State - const [gridData, setGridData] = useState(null) - const [analyzingGrid, setAnalyzingGrid] = useState(false) - const [showGridOverlay, setShowGridOverlay] = useState(true) - const [selectedCell, setSelectedCell] = useState(null) - const [showCellDialog, setShowCellDialog] = useState(false) - - // Block Review State - const [blockReviewMode, setBlockReviewMode] = useState(false) - const [currentBlockNumber, setCurrentBlockNumber] = useState(1) - const [blockReviewData, setBlockReviewData] = useState>({}) - - const KLAUSUR_API = '/klausur-api' - - // Load session history - const loadSessions = useCallback(async () => { - setLoadingSessions(true) - try { - const res = await fetch(`${KLAUSUR_API}/api/v1/vocab/sessions`) - if (res.ok) { - const data = await res.json() - // Filter to only show OCR Vergleich sessions and sort by date - const ocrSessions = (data.sessions || data || []) - .filter((s: SessionInfo) => s.name?.includes('OCR Vergleich')) - .sort((a: SessionInfo, b: SessionInfo) => - new Date(b.created_at).getTime() - new Date(a.created_at).getTime() - ) - .slice(0, 20) // Limit to 20 most recent - setSessions(ocrSessions) - } - } catch (e) { - console.error('Failed to load sessions:', e) - } finally { - setLoadingSessions(false) - } - }, []) - - // Initialize and restore session - useEffect(() => { - loadSessions() - - let sid = localStorage.getItem('ocr-compare-upload-session') - if (!sid) { - sid = `ocr-compare-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` - localStorage.setItem('ocr-compare-upload-session', sid) - } - setQrUploadSessionId(sid) - - // Restore last active session if available - const lastSessionId = localStorage.getItem('ocr-compare-active-session') - if (lastSessionId) { - // Load the session data - fetch(`${KLAUSUR_API}/api/v1/vocab/sessions/${lastSessionId}`) - .then(res => { - if (res.ok) return res.json() - throw new Error('Session not found') - }) - .then(data => { - setSessionId(lastSessionId) - setPageCount(data.page_count || 1) - setSelectedPage(0) - loadAllThumbnails(lastSessionId, data.page_count || 1) - }) - .catch(() => { - // Session no longer exists, clear localStorage - localStorage.removeItem('ocr-compare-active-session') - }) - } - }, [loadSessions]) - - // ESC key to exit fullscreen - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - if (expandedMethod) { - setExpandedMethod(null) - } else if (isFullscreen) { - setIsFullscreen(false) - } - } - } - window.addEventListener('keydown', handleKeyDown) - return () => window.removeEventListener('keydown', handleKeyDown) - }, [isFullscreen, expandedMethod]) - - // Load a session from history - const loadSession = async (session: SessionInfo) => { - setSessionId(session.id) - localStorage.setItem('ocr-compare-active-session', session.id) - setResult(null) - setThumbnails([]) - - try { - // Get session details - const res = await fetch(`${KLAUSUR_API}/api/v1/vocab/sessions/${session.id}`) - if (res.ok) { - const data = await res.json() - setPageCount(data.page_count || 1) - setSelectedPage(0) - - // Load thumbnails - await loadAllThumbnails(session.id, data.page_count || 1) - } - } catch (e) { - setError('Session konnte nicht geladen werden') - } - } - - // Handle mobile file upload - const handleMobileFile = useCallback(async (file: UploadedFile) => { - if (!file.dataUrl) return - - setUploading(true) - setError(null) - setResult(null) - setThumbnails([]) - - try { - // Create session - const sessionRes = await fetch(`${KLAUSUR_API}/api/v1/vocab/sessions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: `OCR Vergleich - ${file.name}` }) - }) - - if (!sessionRes.ok) throw new Error('Session konnte nicht erstellt werden') - const sessionData = await sessionRes.json() - setSessionId(sessionData.id) - localStorage.setItem('ocr-compare-active-session', sessionData.id) - - // Convert dataUrl to blob and upload - const response = await fetch(file.dataUrl) - const blob = await response.blob() - - const formData = new FormData() - formData.append('file', blob, file.name) - - const uploadRes = await fetch( - `${KLAUSUR_API}/api/v1/vocab/sessions/${sessionData.id}/upload-pdf-info`, - { method: 'POST', body: formData } - ) - - if (!uploadRes.ok) throw new Error('PDF Upload fehlgeschlagen') - const uploadData = await uploadRes.json() - setPageCount(uploadData.page_count || 1) - setSelectedPage(0) - - // Load thumbnails - await loadAllThumbnails(sessionData.id, uploadData.page_count || 1) - - // Refresh session list - loadSessions() - - } catch (err) { - setError(err instanceof Error ? err.message : 'Upload fehlgeschlagen') - } finally { - setUploading(false) - } - }, [loadSessions]) - - // Watch for new mobile files - useEffect(() => { - if (mobileUploadedFiles.length > 0) { - const latestFile = mobileUploadedFiles[mobileUploadedFiles.length - 1] - handleMobileFile(latestFile) - setShowQRModal(false) - } - }, [mobileUploadedFiles, handleMobileFile]) - - const handleFileUpload = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0] - if (!file) return - - setUploading(true) - setError(null) - setResult(null) - setThumbnails([]) - - try { - // Create session - const sessionRes = await fetch(`${KLAUSUR_API}/api/v1/vocab/sessions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: `OCR Vergleich - ${file.name}` }) - }) - - if (!sessionRes.ok) throw new Error('Session konnte nicht erstellt werden') - const sessionData = await sessionRes.json() - setSessionId(sessionData.id) - localStorage.setItem('ocr-compare-active-session', sessionData.id) - - // Upload PDF - const formData = new FormData() - formData.append('file', file) - - const uploadRes = await fetch( - `${KLAUSUR_API}/api/v1/vocab/sessions/${sessionData.id}/upload-pdf-info`, - { method: 'POST', body: formData } - ) - - if (!uploadRes.ok) throw new Error('PDF Upload fehlgeschlagen') - const uploadData = await uploadRes.json() - setPageCount(uploadData.page_count || 1) - setSelectedPage(0) - - // Load all thumbnails - await loadAllThumbnails(sessionData.id, uploadData.page_count || 1) - - // Refresh session list - loadSessions() - - } catch (err) { - setError(err instanceof Error ? err.message : 'Upload fehlgeschlagen') - } finally { - setUploading(false) - } - } - - const loadAllThumbnails = async (sid: string, count: number) => { - setLoadingThumbnails(true) - const thumbs: string[] = [] - - for (let i = 0; i < count; i++) { - try { - const res = await fetch(`${KLAUSUR_API}/api/v1/vocab/sessions/${sid}/pdf-thumbnail/${i}?hires=true`) - if (res.ok) { - const blob = await res.blob() - thumbs.push(URL.createObjectURL(blob)) - } else { - thumbs.push('') - } - } catch { - thumbs.push('') - } - } - - setThumbnails(thumbs) - setLoadingThumbnails(false) - } - - const toggleMethod = (methodId: string) => { - setSelectedMethods(prev => - prev.includes(methodId) - ? prev.filter(m => m !== methodId) - : [...prev, methodId] - ) - } - - const runComparison = async () => { - if (!sessionId || selectedMethods.length === 0) return - - setComparing(true) - setError(null) - - try { - const res = await fetch( - `${KLAUSUR_API}/api/v1/vocab/sessions/${sessionId}/compare-ocr/${selectedPage}`, - { method: 'POST' } - ) - - if (!res.ok) throw new Error(`Vergleich fehlgeschlagen: ${res.status}`) - const data = await res.json() - setResult(data) - - } catch (err) { - setError(err instanceof Error ? err.message : 'Vergleich fehlgeschlagen') - } finally { - setComparing(false) - } - } - - // Grid Analysis - const analyzeGrid = async () => { - if (!sessionId) return - - setAnalyzingGrid(true) - setError(null) - - try { - const res = await fetch( - `${KLAUSUR_API}/api/v1/vocab/sessions/${sessionId}/analyze-grid/${selectedPage}`, - { method: 'POST' } - ) - - if (!res.ok) throw new Error(`Grid-Analyse fehlgeschlagen: ${res.status}`) - const data = await res.json() - - if (data.success && data.grid) { - setGridData(data.grid) - } else { - setError(data.error || 'Grid-Erkennung fehlgeschlagen') - } - } catch (err) { - setError(err instanceof Error ? err.message : 'Grid-Analyse fehlgeschlagen') - } finally { - setAnalyzingGrid(false) - } - } - - // Handle cell click for correction - const handleCellClick = useCallback((cell: GridCell) => { - setSelectedCell(cell) - setShowCellDialog(true) - }, []) - - // Handle cell save - const handleCellSave = useCallback((text: string) => { - if (!gridData || !selectedCell) return - - // Update local grid data - const updatedCells = gridData.cells.map(row => - row.map(cell => - cell.row === selectedCell.row && cell.col === selectedCell.col - ? { ...cell, text, status: 'manual' as const, confidence: 1.0 } - : cell - ) - ) - - // Recalculate stats - const recognized = updatedCells.flat().filter(c => c.status === 'recognized').length - const manual = updatedCells.flat().filter(c => c.status === 'manual').length - const problematic = updatedCells.flat().filter(c => c.status === 'problematic').length - const total = updatedCells.flat().length - - setGridData({ - ...gridData, - cells: updatedCells, - stats: { - ...gridData.stats, - recognized, - manual, - problematic, - empty: total - recognized - manual - problematic, - coverage: (recognized + manual) / total - } - }) - - setShowCellDialog(false) - setSelectedCell(null) - }, [gridData, selectedCell]) - - // Block Review Handlers - const handleBlockApprove = useCallback((blockNumber: number, methodId: string, text: string) => { - if (!gridData) return - - const cell = gridData.cells.flat().find(c => getCellBlockNumber(c, gridData) === blockNumber) - if (!cell) return - - setBlockReviewData(prev => ({ - ...prev, - [blockNumber]: { - blockNumber, - cell, - methodResults: [], - status: 'approved' as BlockStatus, - correctedText: text, - approvedMethodId: methodId, - } - })) - }, [gridData]) - - const handleBlockCorrect = useCallback((blockNumber: number, correctedText: string) => { - if (!gridData) return - - const cell = gridData.cells.flat().find(c => getCellBlockNumber(c, gridData) === blockNumber) - if (!cell) return - - setBlockReviewData(prev => ({ - ...prev, - [blockNumber]: { - blockNumber, - cell, - methodResults: [], - status: 'corrected' as BlockStatus, - correctedText, - } - })) - }, [gridData]) - - const handleBlockSkip = useCallback((blockNumber: number) => { - if (!gridData) return - - const cell = gridData.cells.flat().find(c => getCellBlockNumber(c, gridData) === blockNumber) - if (!cell) return - - setBlockReviewData(prev => ({ - ...prev, - [blockNumber]: { - blockNumber, - cell, - methodResults: [], - status: 'skipped' as BlockStatus, - } - })) - }, [gridData]) - - // Start block review mode - const startBlockReview = useCallback(() => { - if (!gridData) return - - // Find first non-empty block - const firstBlock = gridData.cells.flat().find(c => c.status !== 'empty') - if (firstBlock) { - setCurrentBlockNumber(getCellBlockNumber(firstBlock, gridData)) - setBlockReviewMode(true) - } - }, [gridData]) - - // Count non-empty blocks - const nonEmptyBlockCount = useMemo(() => { - if (!gridData) return 0 - return gridData.cells.flat().filter(c => c.status !== 'empty').length - }, [gridData]) - - const VocabList = ({ vocab, highlight }: { vocab: VocabEntry[]; highlight?: Set }) => ( -
- {vocab.map((v, idx) => { - const key = `${v.english}|${v.german}` - const isUnique = highlight?.has(key) - return ( -
-
{v.english}
-
{v.german}
- {v.example && ( -
{v.example}
- )} -
- ) - })} -
- ) - - const getUniqueVocab = (methodKey: string): Set => { - if (!result?.comparison?.found_by_some_methods) return new Set() - const unique = new Set() - result.comparison.found_by_some_methods.forEach(v => { - if (v.methods.includes(methodKey) && v.methods.length === 1) { - unique.add(`${v.english}|${v.german}`) - } - }) - return unique - } - - const getMethodColor = (color: string, type: 'bg' | 'border' | 'text') => { - const colors: Record> = { - slate: { bg: 'bg-slate-50', border: 'border-slate-300', text: 'text-slate-700' }, - blue: { bg: 'bg-blue-50', border: 'border-blue-300', text: 'text-blue-700' }, - red: { bg: 'bg-red-50', border: 'border-red-300', text: 'text-red-700' }, - purple: { bg: 'bg-purple-50', border: 'border-purple-300', text: 'text-purple-700' }, - } - return colors[color]?.[type] || colors.slate[type] - } - - // Anzahl der ausgewaehlten Methoden + 1 fuer das Original - const columnCount = selectedMethods.length + 1 - - return ( -
- - - {/* KI-Werkzeuge Sidebar */} - - -
- {/* Left Sidebar: Upload & History */} -
- {/* Upload Section */} -
-

PDF hochladen

- - - - - - {uploading && ( -
- - - - - Wird hochgeladen... -
- )} - - {error && ( -
- {error} -
- )} -
- - {/* Session History Panel */} -
- - - {showHistory && ( -
- {loadingSessions ? ( -
- - - - - Lade Sessions... -
- ) : sessions.length === 0 ? ( -
- Keine Sessions vorhanden -
- ) : ( - sessions.map(session => ( - - )) - )} -
- )} -
- - {/* Method Selection */} - {sessionId && pageCount > 0 && ( -
-

OCR-Methoden

- -
- {Object.values(OCR_METHODS).map(method => ( - - ))} -
- -
- - - {/* Grid Analysis Button */} - -
- - {/* Grid Overlay Toggle */} - {gridData && ( -
- - - {/* Block Review Button */} - {result && nonEmptyBlockCount > 0 && ( - - )} -
- )} -
- )} - - {/* Grid Stats */} - {gridData && ( -
-

Grid-Erkennung

- -
- -
-
- )} - - {/* Block Review Summary */} - {blockReviewMode && gridData && Object.keys(blockReviewData).length > 0 && ( -
- setCurrentBlockNumber(blockNumber)} - /> -
- )} -
- - {/* Main Content Area */} -
- {/* Page Thumbnails Grid */} - {sessionId && pageCount > 0 && ( -
-

- Seite auswaehlen ({pageCount} Seiten) -

- - {loadingThumbnails ? ( -
- - - - - Lade Seitenvorschau... -
- ) : ( -
- {thumbnails.map((thumb, idx) => ( - - ))} -
- )} -
- )} - - {/* Full-Width Comparison View */} - {(thumbnails[selectedPage] || result) && sessionId && ( -
- {/* Header with Controls */} -
-

- Vergleich - Seite {selectedPage + 1} -

- -
- {/* Layout Selector - only show after comparison */} - {result && ( -
- Ansicht: - {[1, 2, 3, 4].map(cols => ( - - ))} - -
- )} - - {/* Fullscreen Toggle */} - -
-
- - {/* Single Method Expanded View */} - {expandedMethod && ( -
- - - {expandedMethod === 'original' ? ( -
-
-

Original - Seite {selectedPage + 1}

-
-
- {thumbnails[selectedPage] ? ( - {`Seite - ) : ( -
- Kein Bild verfuegbar -
- )} -
-
- ) : ( - (() => { - const method = OCR_METHODS[expandedMethod as keyof typeof OCR_METHODS] - const methodResult = result?.methods?.[expandedMethod] - const isBest = result?.recommendation?.best_method === expandedMethod - return ( -
-
-
-

{method.name}

-

{method.model}

-
- {isBest && ( - - Beste Methode - - )} -
-
- {methodResult && ( -
-
-
-
-
{methodResult.duration_seconds}s
-
Dauer
-
-
-
{methodResult.vocabulary_count}
-
Vokabeln
-
-
-
{(methodResult.confidence * 100).toFixed(0)}%
-
Konfidenz
-
-
-
- {methodResult.vocabulary?.length > 0 && ( -
- -
- )} -
- )} -
-
- ) - })() - )} -
- )} - - {/* Grid View (Normal or Custom Selection) */} - {!expandedMethod && ( -
0 ? visibleMethods.length : columnCount - }, minmax(0, 1fr))` - }} - > - {/* Original PDF Column */} - {(visibleMethods.length === 0 || visibleMethods.includes('original')) && ( -
setExpandedMethod('original')} - > -
-
-

Original

-

Seite {selectedPage + 1}

-
- - - -
-
- {thumbnails[selectedPage] ? ( -
- {/* Show Grid Overlay if available */} - {gridData && showGridOverlay ? ( - - ) : ( - {`Seite - )} -
- ) : ( -
- Kein Bild verfuegbar -
- )} -
-
- )} - - {/* Method Result Columns */} - {selectedMethods - .filter(methodId => visibleMethods.length === 0 || visibleMethods.includes(methodId)) - .map(methodId => { - const method = OCR_METHODS[methodId as keyof typeof OCR_METHODS] - const methodResult = result?.methods?.[methodId] - const isBest = result?.recommendation?.best_method === methodId - - return ( -
setExpandedMethod(methodId)} - > -
-
-

{method.shortName}

-

{method.model}

-
-
- {isBest && ( - - Beste - - )} - - - -
-
-
- {comparing && !methodResult && ( -
- - - - - Extrahiere... -
- )} - {methodResult && ( -
-
-
- Dauer: - {methodResult.duration_seconds}s -
-
- Vokabeln: - {methodResult.vocabulary_count} -
- {methodResult.error && ( -
{methodResult.error}
- )} -
- {methodResult.vocabulary?.length > 0 && ( - - )} -
- )} - {!comparing && !methodResult && ( -
- Noch keine Ergebnisse -
- )} -
-
- ) - })} -
- )} - - {/* Method Selector Chips (for custom view) */} - {result && visibleMethods.length > 0 && visibleMethods.length < selectedMethods.length + 1 && ( -
-
- Methoden ein-/ausblenden: - - {selectedMethods.map(methodId => { - const method = OCR_METHODS[methodId as keyof typeof OCR_METHODS] - return ( - - ) - })} -
-
- )} - - {/* Block Review Panel */} - {blockReviewMode && gridData && result && ( -
-
-

- - - - Block-Review -

-

- Prüfen Sie jeden Block und wählen Sie die korrekte Erkennung oder korrigieren Sie manuell. -

-
- -
- )} -
- )} - - {/* Comparison Summary */} - {result?.comparison && ( -
-

Vergleichszusammenfassung

- -
-
-
- {result.comparison.total_unique_vocabulary} -
-
Gesamt eindeutig
-
-
-
- {result.comparison.found_by_all_methods?.length || 0} -
-
Von allen erkannt
-
-
-
- {result.comparison.found_by_some_methods?.length || 0} -
-
Unterschiede
-
-
-
- {(result.comparison.agreement_rate * 100).toFixed(0)}% -
-
Uebereinstimmung
-
-
- - {result.recommendation && ( -
-
- Empfehlung: - - {OCR_METHODS[result.recommendation.best_method as keyof typeof OCR_METHODS]?.name || result.recommendation.best_method} - -
-

{result.recommendation.reason}

-
- )} - - {result.comparison.found_by_some_methods?.length > 0 && ( -
-

- Unterschiede (gelb markiert): -

-
- {result.comparison.found_by_some_methods.map((v, idx) => ( -
- {v.english} = {v.german} - - (nur: {v.methods.join(', ')}) - -
- ))} -
-
- )} -
- )} - - {/* Empty State */} - {!sessionId && ( -
- - - -

PDF hochladen

-

- Laden Sie ein PDF hoch oder waehlen Sie eine Session aus der Historie, um OCR-Methoden zu vergleichen. -

-
- )} -
-
- - {/* QR Code Upload Modal */} - {showQRModal && ( -
-
setShowQRModal(false)} /> -
- setShowQRModal(false)} - onFilesChanged={(files) => { - setMobileUploadedFiles(files) - }} - /> -
-
- )} - - {/* Cell Correction Dialog */} - {showCellDialog && selectedCell && sessionId && gridData && ( - { - setShowCellDialog(false) - setSelectedCell(null) - }} - /> - )} -
- ) -} diff --git a/admin-v2/app/(sdk)/sdk/document-generator/components/index.ts b/admin-v2/app/(sdk)/sdk/document-generator/components/index.ts deleted file mode 100644 index 20b97f3..0000000 --- a/admin-v2/app/(sdk)/sdk/document-generator/components/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Document Generator Components - * - * Diese Komponenten integrieren die Einwilligungen-Datenpunkte - * in den Dokumentengenerator. - */ - -export { DataPointsPreview } from './DataPointsPreview' -export { DocumentValidation } from './DocumentValidation' diff --git a/admin-v2/app/(sdk)/sdk/document-generator/page.tsx b/admin-v2/app/(sdk)/sdk/document-generator/page.tsx deleted file mode 100644 index e240fbe..0000000 --- a/admin-v2/app/(sdk)/sdk/document-generator/page.tsx +++ /dev/null @@ -1,793 +0,0 @@ -'use client' - -import React, { useState, useEffect, useCallback, useMemo } from 'react' -import { useSDK } from '@/lib/sdk' -import { useEinwilligungen } from '@/lib/sdk/einwilligungen/context' -import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' -import { - LegalTemplateResult, - TemplateType, - Jurisdiction, - LicenseType, - GeneratedDocument, - TEMPLATE_TYPE_LABELS, - LICENSE_TYPE_LABELS, - JURISDICTION_LABELS, - DEFAULT_PLACEHOLDERS, -} from '@/lib/sdk/types' -import { DataPointsPreview } from './components/DataPointsPreview' -import { DocumentValidation } from './components/DocumentValidation' -import { generateAllPlaceholders } from '@/lib/sdk/document-generator/datapoint-helpers' - -// ============================================================================= -// API CLIENT -// ============================================================================= - -const KLAUSUR_SERVICE_URL = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086' - -async function searchTemplates(params: { - query: string - templateType?: TemplateType - licenseTypes?: LicenseType[] - language?: 'de' | 'en' - jurisdiction?: Jurisdiction - limit?: number -}): Promise { - const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/search`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - query: params.query, - template_type: params.templateType, - license_types: params.licenseTypes, - language: params.language, - jurisdiction: params.jurisdiction, - limit: params.limit || 10, - }), - }) - - if (!response.ok) { - throw new Error('Search failed') - } - - const data = await response.json() - return data.map((r: any) => ({ - id: r.id, - score: r.score, - text: r.text, - documentTitle: r.document_title, - templateType: r.template_type, - clauseCategory: r.clause_category, - language: r.language, - jurisdiction: r.jurisdiction, - licenseId: r.license_id, - licenseName: r.license_name, - licenseUrl: r.license_url, - attributionRequired: r.attribution_required, - attributionText: r.attribution_text, - sourceName: r.source_name, - sourceUrl: r.source_url, - sourceRepo: r.source_repo, - placeholders: r.placeholders || [], - isCompleteDocument: r.is_complete_document, - isModular: r.is_modular, - requiresCustomization: r.requires_customization, - outputAllowed: r.output_allowed ?? true, - modificationAllowed: r.modification_allowed ?? true, - distortionProhibited: r.distortion_prohibited ?? false, - })) -} - -async function getTemplatesStatus(): Promise { - const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/status`) - if (!response.ok) return null - return response.json() -} - -async function getSources(): Promise { - const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/sources`) - if (!response.ok) return [] - const data = await response.json() - return data.sources || [] -} - -// ============================================================================= -// COMPONENTS -// ============================================================================= - -function StatusBadge({ status }: { status: string }) { - const colors: Record = { - ready: 'bg-green-100 text-green-700', - empty: 'bg-yellow-100 text-yellow-700', - error: 'bg-red-100 text-red-700', - running: 'bg-blue-100 text-blue-700', - } - return ( - - {status} - - ) -} - -function LicenseBadge({ licenseId, small = false }: { licenseId: LicenseType | null; small?: boolean }) { - if (!licenseId) return null - - const colors: Record = { - public_domain: 'bg-green-100 text-green-700 border-green-200', - cc0: 'bg-green-100 text-green-700 border-green-200', - unlicense: 'bg-green-100 text-green-700 border-green-200', - mit: 'bg-blue-100 text-blue-700 border-blue-200', - cc_by_4: 'bg-purple-100 text-purple-700 border-purple-200', - reuse_notice: 'bg-orange-100 text-orange-700 border-orange-200', - } - - return ( - - {LICENSE_TYPE_LABELS[licenseId] || licenseId} - - ) -} - -function TemplateCard({ - template, - selected, - onSelect, -}: { - template: LegalTemplateResult - selected: boolean - onSelect: () => void -}) { - return ( -
-
-
-
- - {template.documentTitle || 'Untitled'} - - - {template.score.toFixed(2)} - -
-
- {template.templateType && ( - - {TEMPLATE_TYPE_LABELS[template.templateType as TemplateType] || template.templateType} - - )} - - - {template.language} - -
-
- -
- -

- {template.text} -

- - {template.attributionRequired && template.attributionText && ( -
- Attribution: {template.attributionText} -
- )} - - {template.placeholders && template.placeholders.length > 0 && ( -
- {template.placeholders.slice(0, 5).map((p, i) => ( - - {p} - - ))} - {template.placeholders.length > 5 && ( - - +{template.placeholders.length - 5} more - - )} -
- )} - -
- Source: {template.sourceName} -
-
- ) -} - -function PlaceholderEditor({ - placeholders, - values, - onChange, -}: { - placeholders: string[] - values: Record - onChange: (key: string, value: string) => void -}) { - if (placeholders.length === 0) return null - - return ( -
-

Platzhalter ausfuellen

-
- {placeholders.map((placeholder) => ( -
- - onChange(placeholder, e.target.value)} - placeholder={`Wert fuer ${placeholder}`} - className="w-full px-3 py-2 border border-blue-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500" - /> -
- ))} -
-
- ) -} - -function AttributionFooter({ templates }: { templates: LegalTemplateResult[] }) { - const attributionTemplates = templates.filter((t) => t.attributionRequired) - if (attributionTemplates.length === 0) return null - - return ( -
-

Quellenangaben (werden automatisch hinzugefuegt)

-
-

Dieses Dokument wurde unter Verwendung folgender Quellen erstellt:

-
    - {attributionTemplates.map((t, i) => ( -
  • - {t.attributionText || `${t.sourceName} (${t.licenseName})`} -
  • - ))} -
-
-
- ) -} - -function DocumentPreview({ - content, - placeholders, -}: { - content: string - placeholders: Record -}) { - // Replace placeholders in content - let processedContent = content - for (const [key, value] of Object.entries(placeholders)) { - if (value) { - processedContent = processedContent.replace(new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value) - } - } - - return ( -
-
{processedContent}
-
- ) -} - -// ============================================================================= -// MAIN PAGE -// ============================================================================= - -export default function DocumentGeneratorPage() { - const { state } = useSDK() - const { selectedDataPointsData } = useEinwilligungen() - - // Status state - const [status, setStatus] = useState(null) - const [sources, setSources] = useState([]) - const [isLoading, setIsLoading] = useState(true) - - // Search state - const [searchQuery, setSearchQuery] = useState('') - const [selectedType, setSelectedType] = useState('') - const [selectedLanguage, setSelectedLanguage] = useState<'de' | 'en' | ''>('') - const [selectedJurisdiction, setSelectedJurisdiction] = useState('') - const [searchResults, setSearchResults] = useState([]) - const [isSearching, setIsSearching] = useState(false) - - // Selection state - const [selectedTemplates, setSelectedTemplates] = useState([]) - - // Editor state - const [placeholderValues, setPlaceholderValues] = useState>({}) - const [activeTab, setActiveTab] = useState<'search' | 'compose' | 'preview'>('search') - - // Load initial status - useEffect(() => { - async function loadStatus() { - try { - const [statusData, sourcesData] = await Promise.all([ - getTemplatesStatus(), - getSources(), - ]) - setStatus(statusData) - setSources(sourcesData) - } catch (error) { - console.error('Failed to load status:', error) - } finally { - setIsLoading(false) - } - } - loadStatus() - }, []) - - // Pre-fill placeholders from company profile - useEffect(() => { - if (state?.companyProfile) { - const profile = state.companyProfile - setPlaceholderValues((prev) => ({ - ...prev, - '[COMPANY_NAME]': profile.companyName || '', - '[FIRMENNAME]': profile.companyName || '', - '[EMAIL]': profile.dpoEmail || '', - '[DSB_EMAIL]': profile.dpoEmail || '', - '[DPO_NAME]': profile.dpoName || '', - '[DSB_NAME]': profile.dpoName || '', - })) - } - }, [state?.companyProfile]) - - // Pre-fill placeholders from Einwilligungen data points - useEffect(() => { - if (selectedDataPointsData && selectedDataPointsData.length > 0) { - const einwilligungenPlaceholders = generateAllPlaceholders(selectedDataPointsData, 'de') - setPlaceholderValues((prev) => ({ - ...prev, - ...einwilligungenPlaceholders, - })) - } - }, [selectedDataPointsData]) - - // Handler for inserting placeholders from DataPointsPreview - const handleInsertPlaceholder = useCallback((placeholder: string) => { - // This is a simplified version - in a real editor you would insert at cursor position - // For now, we just ensure the placeholder is in the values so it can be replaced - if (!placeholderValues[placeholder]) { - // The placeholder value will be generated from einwilligungen data - const einwilligungenPlaceholders = generateAllPlaceholders(selectedDataPointsData || [], 'de') - if (einwilligungenPlaceholders[placeholder as keyof typeof einwilligungenPlaceholders]) { - setPlaceholderValues((prev) => ({ - ...prev, - [placeholder]: einwilligungenPlaceholders[placeholder as keyof typeof einwilligungenPlaceholders], - })) - } - } - }, [placeholderValues, selectedDataPointsData]) - - // Search handler - const handleSearch = useCallback(async () => { - if (!searchQuery.trim()) return - - setIsSearching(true) - try { - const results = await searchTemplates({ - query: searchQuery, - templateType: selectedType || undefined, - language: selectedLanguage || undefined, - jurisdiction: selectedJurisdiction || undefined, - limit: 20, - }) - setSearchResults(results) - } catch (error) { - console.error('Search failed:', error) - } finally { - setIsSearching(false) - } - }, [searchQuery, selectedType, selectedLanguage, selectedJurisdiction]) - - // Toggle template selection - const toggleTemplate = (id: string) => { - setSelectedTemplates((prev) => - prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id] - ) - } - - // Get selected template objects - const selectedTemplateObjects = searchResults.filter((r) => - selectedTemplates.includes(r.id) - ) - - // Get all unique placeholders from selected templates - const allPlaceholders = Array.from( - new Set(selectedTemplateObjects.flatMap((t) => t.placeholders || [])) - ) - - // Combined content from selected templates - const combinedContent = selectedTemplateObjects - .map((t) => `## ${t.documentTitle || 'Abschnitt'}\n\n${t.text}`) - .join('\n\n---\n\n') - - // Step info - using 'consent' as base since document-generator doesn't exist yet - const stepInfo = STEP_EXPLANATIONS['consent'] || { - title: 'Dokumentengenerator', - description: 'Generieren Sie rechtliche Dokumente aus lizenzkonformen Vorlagen', - explanation: 'Der Dokumentengenerator nutzt frei lizenzierte Textbausteine um Datenschutzerklaerungen, AGB und andere rechtliche Dokumente zu erstellen.', - tips: ['Waehlen Sie passende Vorlagen aus der Suche', 'Fuellen Sie die Platzhalter mit Ihren Unternehmensdaten'], - } - - if (isLoading) { - return ( -
-
-
- ) - } - - return ( -
- {/* Step Header */} - - - - - {/* Status Overview */} -
-
-
Collection Status
-
- -
-
-
-
Indexierte Chunks
-
- {status?.stats?.points_count || 0} -
-
-
-
Aktive Quellen
-
- {sources.filter((s) => s.enabled).length} -
-
-
-
Ausgewaehlt
-
- {selectedTemplates.length} -
-
-
- - {/* Tab Navigation */} -
- {(['search', 'compose', 'preview'] as const).map((tab) => ( - - ))} -
- - {/* Search Tab */} - {activeTab === 'search' && ( -
- {/* Search Form */} -
-
-
- - setSearchQuery(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSearch()} - placeholder="z.B. Datenschutzerklaerung, Cookie-Banner, Widerruf..." - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500" - /> -
-
- - -
-
- - -
- -
-
- - {/* Search Results */} - {searchResults.length > 0 && ( -
-
-

- {searchResults.length} Ergebnisse -

- {selectedTemplates.length > 0 && ( - - )} -
-
- {searchResults.map((result) => ( - toggleTemplate(result.id)} - /> - ))} -
-
- )} - - {searchResults.length === 0 && searchQuery && !isSearching && ( -
-
- - - -
-

Keine Vorlagen gefunden

-

- Versuchen Sie einen anderen Suchbegriff oder aendern Sie die Filter. -

-
- )} - - {/* Quick Start Templates */} - {searchResults.length === 0 && !searchQuery && ( -
-

Schnellstart - Haeufig benoetigte Dokumente

-
- {[ - { query: 'Datenschutzerklaerung DSGVO', type: 'privacy_policy', icon: '🔒' }, - { query: 'Cookie Banner', type: 'cookie_banner', icon: '🍪' }, - { query: 'Impressum', type: 'impressum', icon: '📋' }, - { query: 'AGB Nutzungsbedingungen', type: 'terms_of_service', icon: '📜' }, - ].map((item) => ( - - ))} -
-
- )} -
- )} - - {/* Compose Tab */} - {activeTab === 'compose' && selectedTemplates.length > 0 && ( -
- {/* Main Content - 2/3 */} -
- {/* Selected Templates */} -
-

- Ausgewaehlte Bausteine ({selectedTemplates.length}) -

-
- {selectedTemplateObjects.map((t, index) => ( -
-
- {index + 1}. - {t.documentTitle} - -
- -
- ))} -
-
- - {/* Placeholder Editor */} - - setPlaceholderValues((prev) => ({ ...prev, [key]: value })) - } - /> - - {/* Attribution Footer */} - - - {/* Actions */} -
- - -
-
- - {/* Sidebar - 1/3: Einwilligungen DataPoints */} -
- -
-
- )} - - {/* Preview Tab */} - {activeTab === 'preview' && selectedTemplates.length > 0 && ( -
-
-

Dokument-Vorschau

-
- - - -
-
- - {/* Document Validation based on selected Einwilligungen */} - {selectedDataPointsData && selectedDataPointsData.length > 0 && ( - - )} - - - - {/* Attribution */} - -
- )} - - {/* Sources Info */} - {activeTab === 'search' && sources.length > 0 && ( -
-

Verfuegbare Quellen

-
- {sources.filter((s) => s.enabled).slice(0, 6).map((source) => ( -
-
- {source.name} - -
-

{source.description}

-
- {source.template_types.slice(0, 3).map((t: string) => ( - - {TEMPLATE_TYPE_LABELS[t as TemplateType] || t} - - ))} -
-
- ))} -
-
- )} -
- ) -} diff --git a/admin-v2/app/(sdk)/sdk/dsfa/page.tsx b/admin-v2/app/(sdk)/sdk/dsfa/page.tsx index 20f6bdb..a3b2994 100644 --- a/admin-v2/app/(sdk)/sdk/dsfa/page.tsx +++ b/admin-v2/app/(sdk)/sdk/dsfa/page.tsx @@ -1,177 +1,179 @@ 'use client' -import React, { useState, useEffect, useCallback } from 'react' +import React, { useState, useCallback } from 'react' import { useRouter } from 'next/navigation' -import Link from 'next/link' import { useSDK } from '@/lib/sdk' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' -import { DSFACard } from '@/components/sdk/dsfa' -import { - DSFA, - DSFAStatus, - DSFA_STATUS_LABELS, - DSFA_RISK_LEVEL_LABELS, -} from '@/lib/sdk/dsfa/types' -import { - listDSFAs, - deleteDSFA, - exportDSFAAsJSON, - getDSFAStats, - createDSFAFromAssessment, - getDSFAByAssessment, -} from '@/lib/sdk/dsfa/api' +import { DocumentUploadSection, type UploadedDocument } from '@/components/sdk' // ============================================================================= -// UCCA TRIGGER WARNING COMPONENT +// TYPES // ============================================================================= -interface UCCATriggerWarningProps { - assessmentId: string - triggeredRules: string[] - existingDsfaId?: string - onCreateDSFA: () => void +interface DSFA { + id: string + title: string + description: string + status: 'draft' | 'in-review' | 'approved' | 'needs-update' + createdAt: Date + updatedAt: Date + approvedBy: string | null + riskLevel: 'low' | 'medium' | 'high' | 'critical' + processingActivity: string + dataCategories: string[] + recipients: string[] + measures: string[] } -function UCCATriggerWarning({ - assessmentId, - triggeredRules, - existingDsfaId, - onCreateDSFA, -}: UCCATriggerWarningProps) { - if (existingDsfaId) { - return ( -
-
- - - -
-
-

DSFA bereits erstellt

-

- Fuer dieses Assessment wurde bereits eine DSFA angelegt. -

- - DSFA oeffnen - - - - -
-
- ) +// ============================================================================= +// MOCK DATA +// ============================================================================= + +const mockDSFAs: DSFA[] = [ + { + id: 'dsfa-1', + title: 'DSFA - Bewerber-Management-System', + description: 'Datenschutz-Folgenabschaetzung fuer das KI-gestuetzte Bewerber-Screening', + status: 'in-review', + createdAt: new Date('2024-01-10'), + updatedAt: new Date('2024-01-20'), + approvedBy: null, + riskLevel: 'high', + processingActivity: 'Automatisierte Bewertung von Bewerbungsunterlagen', + dataCategories: ['Kontaktdaten', 'Beruflicher Werdegang', 'Qualifikationen'], + recipients: ['HR-Abteilung', 'Fachabteilungen'], + measures: ['Verschluesselung', 'Zugriffskontrolle', 'Menschliche Pruefung'], + }, + { + id: 'dsfa-2', + title: 'DSFA - Video-Ueberwachung Buero', + description: 'Datenschutz-Folgenabschaetzung fuer die Videoueberwachung im Buerogebaeude', + status: 'approved', + createdAt: new Date('2023-11-01'), + updatedAt: new Date('2023-12-15'), + approvedBy: 'DSB Mueller', + riskLevel: 'medium', + processingActivity: 'Videoueberwachung zu Sicherheitszwecken', + dataCategories: ['Bilddaten', 'Bewegungsdaten'], + recipients: ['Sicherheitsdienst'], + measures: ['Loeschfristen', 'Zugriffsbeschraenkung', 'Hinweisschilder'], + }, + { + id: 'dsfa-3', + title: 'DSFA - Kundenanalyse', + description: 'Datenschutz-Folgenabschaetzung fuer Big-Data-Kundenanalysen', + status: 'draft', + createdAt: new Date('2024-01-22'), + updatedAt: new Date('2024-01-22'), + approvedBy: null, + riskLevel: 'high', + processingActivity: 'Analyse von Kundenverhalten fuer Marketing', + dataCategories: ['Kaufhistorie', 'Nutzungsverhalten', 'Praeferenzen'], + recipients: ['Marketing', 'Vertrieb'], + measures: [], + }, +] + +// ============================================================================= +// COMPONENTS +// ============================================================================= + +function DSFACard({ dsfa }: { dsfa: DSFA }) { + const statusColors = { + draft: 'bg-gray-100 text-gray-600 border-gray-200', + 'in-review': 'bg-yellow-100 text-yellow-700 border-yellow-200', + approved: 'bg-green-100 text-green-700 border-green-200', + 'needs-update': 'bg-orange-100 text-orange-700 border-orange-200', + } + + const statusLabels = { + draft: 'Entwurf', + 'in-review': 'In Pruefung', + approved: 'Genehmigt', + 'needs-update': 'Aktualisierung erforderlich', + } + + const riskColors = { + low: 'bg-green-100 text-green-700', + medium: 'bg-yellow-100 text-yellow-700', + high: 'bg-orange-100 text-orange-700', + critical: 'bg-red-100 text-red-700', } return ( -
-
- - - -
-
-

DSFA erforderlich

-

- Das UCCA-Assessment hat folgende Trigger ausgeloest: -

-
- {triggeredRules.map(rule => ( - - {rule} +
+
+
+
+ + {statusLabels[dsfa.status]} - ))} + + Risiko: {dsfa.riskLevel === 'low' ? 'Niedrig' : + dsfa.riskLevel === 'medium' ? 'Mittel' : + dsfa.riskLevel === 'high' ? 'Hoch' : 'Kritisch'} + +
+

{dsfa.title}

+

{dsfa.description}

+
+
+ +
+

Verarbeitungstaetigkeit: {dsfa.processingActivity}

+
+ +
+ {dsfa.dataCategories.map(cat => ( + + {cat} + + ))} +
+ + {dsfa.measures.length > 0 && ( +
+ Massnahmen: +
+ {dsfa.measures.map(m => ( + + {m} + + ))} +
+
+ )} + +
+
+ Erstellt: {dsfa.createdAt.toLocaleDateString('de-DE')} + {dsfa.approvedBy && ( + Genehmigt von: {dsfa.approvedBy} + )} +
+
+ +
-
) } -// ============================================================================= -// GENERATOR WIZARD COMPONENT -// ============================================================================= - -function GeneratorWizard({ onClose, onCreated }: { onClose: () => void; onCreated: (dsfa: DSFA) => void }) { +function GeneratorWizard({ onClose }: { onClose: () => void }) { const [step, setStep] = useState(1) - const [formData, setFormData] = useState({ - name: '', - description: '', - processingPurpose: '', - dataCategories: [] as string[], - legalBasis: '', - }) - const [isSubmitting, setIsSubmitting] = useState(false) - - const DATA_CATEGORIES = [ - 'Kontaktdaten', - 'Identifikationsdaten', - 'Finanzdaten', - 'Gesundheitsdaten', - 'Standortdaten', - 'Nutzungsdaten', - 'Biometrische Daten', - 'Daten Minderjaehriger', - ] - - const LEGAL_BASES = [ - { value: 'consent', label: 'Einwilligung (Art. 6 Abs. 1 lit. a)' }, - { value: 'contract', label: 'Vertrag (Art. 6 Abs. 1 lit. b)' }, - { value: 'legal_obligation', label: 'Rechtliche Verpflichtung (Art. 6 Abs. 1 lit. c)' }, - { value: 'legitimate_interest', label: 'Berechtigtes Interesse (Art. 6 Abs. 1 lit. f)' }, - ] - - const handleCategoryToggle = (cat: string) => { - setFormData(prev => ({ - ...prev, - dataCategories: prev.dataCategories.includes(cat) - ? prev.dataCategories.filter(c => c !== cat) - : [...prev.dataCategories, cat], - })) - } - - const handleSubmit = async () => { - setIsSubmitting(true) - try { - // For standalone DSFA, we use the regular create endpoint - const response = await fetch('/api/sdk/v1/dsgvo/dsfas', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: formData.name, - description: formData.description, - processing_purpose: formData.processingPurpose, - data_categories: formData.dataCategories, - legal_basis: formData.legalBasis, - status: 'draft', - }), - }) - if (response.ok) { - const dsfa = await response.json() - onCreated(dsfa) - onClose() - } - } catch (error) { - console.error('Failed to create DSFA:', error) - } finally { - setIsSubmitting(false) - } - } return (
-

Neue Standalone-DSFA erstellen

+

Neue DSFA erstellen