refactor(admin): split dsfa, audit-llm, quality pages
Extract components and hooks from oversized page files (563/561/520 LOC) into colocated _components/ and _hooks/ subdirectories. All three page.tsx files are now thin orchestrators under 300 LOC each (dsfa: 216, audit-llm: 121, quality: 163). Zero behavior changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
191
admin-compliance/app/sdk/dsfa/_components/GeneratorWizard.tsx
Normal file
191
admin-compliance/app/sdk/dsfa/_components/GeneratorWizard.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import type { DSFA } from './DSFACard'
|
||||
|
||||
export function GeneratorWizard({ onClose, onSubmit }: { onClose: () => void; onSubmit: (data: Partial<DSFA>) => Promise<void> }) {
|
||||
const [step, setStep] = useState(1)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [title, setTitle] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [processingActivity, setProcessingActivity] = useState('')
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
|
||||
const [riskLevel, setRiskLevel] = useState<'low' | 'medium' | 'high' | 'critical'>('low')
|
||||
const [selectedMeasures, setSelectedMeasures] = useState<string[]>([])
|
||||
|
||||
const riskMap: Record<string, 'low' | 'medium' | 'high' | 'critical'> = {
|
||||
Niedrig: 'low', Mittel: 'medium', Hoch: 'high', Kritisch: 'critical',
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
await onSubmit({
|
||||
title,
|
||||
description,
|
||||
processingActivity,
|
||||
dataCategories: selectedCategories,
|
||||
riskLevel,
|
||||
measures: selectedMeasures,
|
||||
status: 'draft',
|
||||
})
|
||||
onClose()
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
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}
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
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>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Verarbeitungstaetigkeit</label>
|
||||
<input
|
||||
type="text"
|
||||
value={processingActivity}
|
||||
onChange={e => setProcessingActivity(e.target.value)}
|
||||
placeholder="z.B. Automatisierte Auswertung von Kundendaten"
|
||||
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"
|
||||
checked={selectedCategories.includes(cat)}
|
||||
onChange={e => setSelectedCategories(prev =>
|
||||
e.target.checked ? [...prev, cat] : prev.filter(c => c !== cat)
|
||||
)}
|
||||
/>
|
||||
<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'] as const).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"
|
||||
checked={riskLevel === riskMap[level]}
|
||||
onChange={() => setRiskLevel(riskMap[level])}
|
||||
/>
|
||||
<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"
|
||||
checked={selectedMeasures.includes(m)}
|
||||
onChange={e => setSelectedMeasures(prev =>
|
||||
e.target.checked ? [...prev, m] : prev.filter(x => x !== m)
|
||||
)}
|
||||
/>
|
||||
<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"
|
||||
disabled={saving}
|
||||
>
|
||||
{step === 1 ? 'Abbrechen' : 'Zurueck'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => step < 4 ? setStep(step + 1) : handleSubmit()}
|
||||
disabled={saving || (step === 1 && !title.trim())}
|
||||
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{step === 4 ? (saving ? 'Wird erstellt...' : 'DSFA erstellen') : 'Weiter'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user