Remove duplicate compliance and DSGVO admin pages that have been superseded by the unified SDK pipeline. Update navigation, sidebar, roles, and module registry to reflect the new structure. Add DSFA corpus API proxy and source-policy components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
426 lines
17 KiB
TypeScript
426 lines
17 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useCallback } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { useSDK } from '@/lib/sdk'
|
|
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
|
|
import { DocumentUploadSection, type UploadedDocument } from '@/components/sdk'
|
|
|
|
// =============================================================================
|
|
// TYPES
|
|
// =============================================================================
|
|
|
|
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[]
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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 (
|
|
<div className={`bg-white rounded-xl border-2 p-6 ${
|
|
dsfa.status === 'needs-update' ? 'border-orange-200' :
|
|
dsfa.status === 'approved' ? 'border-green-200' : 'border-gray-200'
|
|
}`}>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className={`px-2 py-1 text-xs rounded-full ${statusColors[dsfa.status]}`}>
|
|
{statusLabels[dsfa.status]}
|
|
</span>
|
|
<span className={`px-2 py-1 text-xs rounded-full ${riskColors[dsfa.riskLevel]}`}>
|
|
Risiko: {dsfa.riskLevel === 'low' ? 'Niedrig' :
|
|
dsfa.riskLevel === 'medium' ? 'Mittel' :
|
|
dsfa.riskLevel === 'high' ? 'Hoch' : 'Kritisch'}
|
|
</span>
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-gray-900">{dsfa.title}</h3>
|
|
<p className="text-sm text-gray-500 mt-1">{dsfa.description}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 text-sm text-gray-600">
|
|
<p><span className="text-gray-500">Verarbeitungstaetigkeit:</span> {dsfa.processingActivity}</p>
|
|
</div>
|
|
|
|
<div className="mt-3 flex flex-wrap gap-1">
|
|
{dsfa.dataCategories.map(cat => (
|
|
<span key={cat} className="px-2 py-0.5 text-xs bg-blue-50 text-blue-600 rounded">
|
|
{cat}
|
|
</span>
|
|
))}
|
|
</div>
|
|
|
|
{dsfa.measures.length > 0 && (
|
|
<div className="mt-3">
|
|
<span className="text-sm text-gray-500">Massnahmen:</span>
|
|
<div className="flex flex-wrap gap-1 mt-1">
|
|
{dsfa.measures.map(m => (
|
|
<span key={m} className="px-2 py-0.5 text-xs bg-green-50 text-green-600 rounded">
|
|
{m}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-4 pt-4 border-t border-gray-100 flex items-center justify-between text-sm">
|
|
<div className="text-gray-500">
|
|
<span>Erstellt: {dsfa.createdAt.toLocaleDateString('de-DE')}</span>
|
|
{dsfa.approvedBy && (
|
|
<span className="ml-4">Genehmigt von: {dsfa.approvedBy}</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button className="px-3 py-1 text-purple-600 hover:bg-purple-50 rounded-lg transition-colors">
|
|
Bearbeiten
|
|
</button>
|
|
<button className="px-3 py-1 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
|
Exportieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function GeneratorWizard({ onClose }: { onClose: () => void }) {
|
|
const [step, setStep] = useState(1)
|
|
|
|
return (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="text-lg font-semibold text-gray-900">Neue DSFA erstellen</h3>
|
|
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Progress Steps */}
|
|
<div className="flex items-center gap-2 mb-6">
|
|
{[1, 2, 3, 4].map(s => (
|
|
<React.Fragment key={s}>
|
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
|
|
s < step ? 'bg-green-500 text-white' :
|
|
s === step ? 'bg-purple-600 text-white' : 'bg-gray-200 text-gray-500'
|
|
}`}>
|
|
{s < step ? (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
) : s}
|
|
</div>
|
|
{s < 4 && <div className={`flex-1 h-1 ${s < step ? 'bg-green-500' : 'bg-gray-200'}`} />}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
|
|
{/* Step Content */}
|
|
<div className="min-h-48">
|
|
{step === 1 && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Titel der DSFA</label>
|
|
<input
|
|
type="text"
|
|
placeholder="z.B. DSFA - Mitarbeiter-Monitoring"
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung der Verarbeitung</label>
|
|
<textarea
|
|
rows={3}
|
|
placeholder="Beschreiben Sie die geplante Datenverarbeitung..."
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{step === 2 && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Datenkategorien</label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{['Kontaktdaten', 'Identifikationsdaten', 'Finanzdaten', 'Gesundheitsdaten', 'Standortdaten', 'Nutzungsdaten'].map(cat => (
|
|
<label key={cat} className="flex items-center gap-2 p-2 border rounded-lg hover:bg-gray-50">
|
|
<input type="checkbox" className="w-4 h-4 text-purple-600" />
|
|
<span className="text-sm">{cat}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{step === 3 && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Risikobewertung</label>
|
|
<p className="text-sm text-gray-500 mb-4">Bewerten Sie die Risiken fuer die Rechte und Freiheiten der Betroffenen.</p>
|
|
<div className="space-y-2">
|
|
{['Niedrig', 'Mittel', 'Hoch', 'Kritisch'].map(level => (
|
|
<label key={level} className="flex items-center gap-3 p-3 border rounded-lg hover:bg-gray-50 cursor-pointer">
|
|
<input type="radio" name="risk" className="w-4 h-4 text-purple-600" />
|
|
<span className="text-sm font-medium">{level}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{step === 4 && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Schutzmassnahmen</label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{['Verschluesselung', 'Pseudonymisierung', 'Zugriffskontrolle', 'Loeschkonzept', 'Schulungen', 'Menschliche Pruefung'].map(m => (
|
|
<label key={m} className="flex items-center gap-2 p-2 border rounded-lg hover:bg-gray-50">
|
|
<input type="checkbox" className="w-4 h-4 text-purple-600" />
|
|
<span className="text-sm">{m}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<div className="flex items-center justify-between mt-6 pt-4 border-t border-gray-200">
|
|
<button
|
|
onClick={() => step > 1 ? setStep(step - 1) : onClose()}
|
|
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
{step === 1 ? 'Abbrechen' : 'Zurueck'}
|
|
</button>
|
|
<button
|
|
onClick={() => step < 4 ? setStep(step + 1) : onClose()}
|
|
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
|
>
|
|
{step === 4 ? 'DSFA erstellen' : 'Weiter'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN PAGE
|
|
// =============================================================================
|
|
|
|
export default function DSFAPage() {
|
|
const router = useRouter()
|
|
const { state } = useSDK()
|
|
const [dsfas] = useState<DSFA[]>(mockDSFAs)
|
|
const [showGenerator, setShowGenerator] = useState(false)
|
|
const [filter, setFilter] = useState<string>('all')
|
|
|
|
// Handle uploaded document
|
|
const handleDocumentProcessed = useCallback((doc: UploadedDocument) => {
|
|
console.log('[DSFA Page] Document processed:', doc)
|
|
}, [])
|
|
|
|
// Open document in workflow editor
|
|
const handleOpenInEditor = useCallback((doc: UploadedDocument) => {
|
|
router.push(`/sdk/workflow?documentType=dsfa&documentId=${doc.id}&mode=change`)
|
|
}, [router])
|
|
|
|
const filteredDSFAs = filter === 'all'
|
|
? dsfas
|
|
: dsfas.filter(d => d.status === filter)
|
|
|
|
const draftCount = dsfas.filter(d => d.status === 'draft').length
|
|
const inReviewCount = dsfas.filter(d => d.status === 'in-review').length
|
|
const approvedCount = dsfas.filter(d => d.status === 'approved').length
|
|
|
|
const stepInfo = STEP_EXPLANATIONS['dsfa']
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Step Header */}
|
|
<StepHeader
|
|
stepId="dsfa"
|
|
title={stepInfo.title}
|
|
description={stepInfo.description}
|
|
explanation={stepInfo.explanation}
|
|
tips={stepInfo.tips}
|
|
>
|
|
{!showGenerator && (
|
|
<button
|
|
onClick={() => setShowGenerator(true)}
|
|
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
Neue DSFA
|
|
</button>
|
|
)}
|
|
</StepHeader>
|
|
|
|
{/* Generator */}
|
|
{showGenerator && (
|
|
<GeneratorWizard onClose={() => setShowGenerator(false)} />
|
|
)}
|
|
|
|
{/* Document Upload Section */}
|
|
<DocumentUploadSection
|
|
documentType="dsfa"
|
|
onDocumentProcessed={handleDocumentProcessed}
|
|
onOpenInEditor={handleOpenInEditor}
|
|
/>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div className="text-sm text-gray-500">Gesamt</div>
|
|
<div className="text-3xl font-bold text-gray-900">{dsfas.length}</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div className="text-sm text-gray-500">Entwuerfe</div>
|
|
<div className="text-3xl font-bold text-gray-500">{draftCount}</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-yellow-200 p-6">
|
|
<div className="text-sm text-yellow-600">In Pruefung</div>
|
|
<div className="text-3xl font-bold text-yellow-600">{inReviewCount}</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-green-200 p-6">
|
|
<div className="text-sm text-green-600">Genehmigt</div>
|
|
<div className="text-3xl font-bold text-green-600">{approvedCount}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filter */}
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-gray-500">Filter:</span>
|
|
{['all', 'draft', 'in-review', 'approved', 'needs-update'].map(f => (
|
|
<button
|
|
key={f}
|
|
onClick={() => setFilter(f)}
|
|
className={`px-3 py-1 text-sm rounded-full transition-colors ${
|
|
filter === f
|
|
? 'bg-purple-600 text-white'
|
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
|
}`}
|
|
>
|
|
{f === 'all' ? 'Alle' :
|
|
f === 'draft' ? 'Entwuerfe' :
|
|
f === 'in-review' ? 'In Pruefung' :
|
|
f === 'approved' ? 'Genehmigt' : 'Update erforderlich'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* DSFA List */}
|
|
<div className="space-y-4">
|
|
{filteredDSFAs.map(dsfa => (
|
|
<DSFACard key={dsfa.id} dsfa={dsfa} />
|
|
))}
|
|
</div>
|
|
|
|
{filteredDSFAs.length === 0 && !showGenerator && (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
|
<div className="w-16 h-16 mx-auto bg-purple-100 rounded-full flex items-center justify-center mb-4">
|
|
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-gray-900">Keine DSFAs gefunden</h3>
|
|
<p className="mt-2 text-gray-500">Erstellen Sie eine neue Datenschutz-Folgenabschaetzung.</p>
|
|
<button
|
|
onClick={() => setShowGenerator(true)}
|
|
className="mt-4 px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
|
>
|
|
Erste DSFA erstellen
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|