This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/app/(sdk)/sdk/dsfa/page.tsx
BreakPilot Dev f09e24d52c refactor(admin-v2): Consolidate compliance/DSGVO pages into SDK pipeline
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>
2026-02-10 23:26:05 +01:00

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>
)
}