fix(admin-v2): Restore complete admin-v2 application

The admin-v2 application was incomplete in the repository. This commit
restores all missing components:

- Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education,
  infrastructure, communication, development, onboarding, rbac
- SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen,
  vendor-compliance, tom-generator, dsr, and more
- Developer portal (25 pages): API docs, SDK guides, frameworks
- All components, lib files, hooks, and types
- Updated package.json with all dependencies

The issue was caused by incomplete initial repository state - the full
admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2
but was never fully synced to the main admin-v2 directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
BreakPilot Dev
2026-02-08 23:40:15 -08:00
parent f28244753f
commit 660295e218
385 changed files with 138126 additions and 3079 deletions

View File

@@ -0,0 +1,286 @@
'use client'
// =============================================================================
// TOM Generator Wizard Component
// Main wizard container with step navigation
// =============================================================================
import React from 'react'
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
import { TOM_GENERATOR_STEPS, TOMGeneratorStepId } from '@/lib/sdk/tom-generator/types'
// =============================================================================
// STEP INDICATOR
// =============================================================================
interface StepIndicatorProps {
stepId: TOMGeneratorStepId
stepNumber: number
title: string
isActive: boolean
isCompleted: boolean
onClick: () => void
}
function StepIndicator({
stepNumber,
title,
isActive,
isCompleted,
onClick,
}: StepIndicatorProps) {
return (
<button
onClick={onClick}
className={`flex items-center gap-3 p-3 rounded-lg transition-all w-full text-left ${
isActive
? 'bg-blue-50 border-2 border-blue-500'
: isCompleted
? 'bg-green-50 border border-green-300 hover:bg-green-100'
: 'bg-gray-50 border border-gray-200 hover:bg-gray-100'
}`}
>
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
isActive
? 'bg-blue-500 text-white'
: isCompleted
? 'bg-green-500 text-white'
: 'bg-gray-300 text-gray-600'
}`}
>
{isCompleted ? (
<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>
) : (
stepNumber
)}
</div>
<span
className={`text-sm font-medium ${
isActive ? 'text-blue-700' : isCompleted ? 'text-green-700' : 'text-gray-600'
}`}
>
{title}
</span>
</button>
)
}
// =============================================================================
// WIZARD NAVIGATION
// =============================================================================
interface WizardNavigationProps {
onPrevious: () => void
onNext: () => void
onSave: () => void
canGoPrevious: boolean
canGoNext: boolean
isLastStep: boolean
isSaving: boolean
}
function WizardNavigation({
onPrevious,
onNext,
onSave,
canGoPrevious,
canGoNext,
isLastStep,
isSaving,
}: WizardNavigationProps) {
return (
<div className="flex justify-between items-center mt-8 pt-6 border-t">
<button
onClick={onPrevious}
disabled={!canGoPrevious}
className={`px-4 py-2 rounded-lg font-medium flex items-center gap-2 ${
canGoPrevious
? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
: 'bg-gray-50 text-gray-400 cursor-not-allowed'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Zurück
</button>
<div className="flex gap-3">
<button
onClick={onSave}
disabled={isSaving}
className="px-4 py-2 rounded-lg font-medium bg-gray-100 text-gray-700 hover:bg-gray-200 flex items-center gap-2"
>
{isSaving ? (
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
</svg>
)}
Speichern
</button>
<button
onClick={onNext}
disabled={!canGoNext && !isLastStep}
className={`px-4 py-2 rounded-lg font-medium flex items-center gap-2 ${
canGoNext || isLastStep
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-blue-300 text-white cursor-not-allowed'
}`}
>
{isLastStep ? 'Abschließen' : 'Weiter'}
{!isLastStep && (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
)}
</button>
</div>
</div>
)
}
// =============================================================================
// PROGRESS BAR
// =============================================================================
interface ProgressBarProps {
percentage: number
}
function ProgressBar({ percentage }: ProgressBarProps) {
return (
<div className="mb-6">
<div className="flex justify-between text-sm text-gray-600 mb-2">
<span>Fortschritt</span>
<span>{percentage}%</span>
</div>
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 transition-all duration-300"
style={{ width: `${percentage}%` }}
/>
</div>
</div>
)
}
// =============================================================================
// MAIN WIZARD COMPONENT
// =============================================================================
interface TOMGeneratorWizardProps {
children: React.ReactNode
showSidebar?: boolean
showProgress?: boolean
}
export function TOMGeneratorWizard({
children,
showSidebar = true,
showProgress = true,
}: TOMGeneratorWizardProps) {
const {
state,
currentStepIndex,
canGoNext,
canGoPrevious,
goToStep,
goToNextStep,
goToPreviousStep,
isStepCompleted,
getCompletionPercentage,
saveState,
isLoading,
} = useTOMGenerator()
const [isSaving, setIsSaving] = React.useState(false)
const handleSave = async () => {
setIsSaving(true)
try {
await saveState()
} catch (error) {
console.error('Failed to save:', error)
} finally {
setIsSaving(false)
}
}
const isLastStep = currentStepIndex === TOM_GENERATOR_STEPS.length - 1
return (
<div className="flex gap-8">
{/* Sidebar */}
{showSidebar && (
<div className="w-72 flex-shrink-0">
<div className="bg-white rounded-lg shadow-sm border p-4 sticky top-4">
<h3 className="font-semibold text-gray-900 mb-4">Wizard-Schritte</h3>
{showProgress && <ProgressBar percentage={getCompletionPercentage()} />}
<div className="space-y-2">
{TOM_GENERATOR_STEPS.map((step, index) => (
<StepIndicator
key={step.id}
stepId={step.id}
stepNumber={index + 1}
title={step.title.de}
isActive={state.currentStep === step.id}
isCompleted={isStepCompleted(step.id)}
onClick={() => goToStep(step.id)}
/>
))}
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="flex-1 min-w-0">
<div className="bg-white rounded-lg shadow-sm border p-6">
{/* Step Header */}
<div className="mb-6">
<div className="text-sm text-blue-600 font-medium mb-1">
Schritt {currentStepIndex + 1} von {TOM_GENERATOR_STEPS.length}
</div>
<h2 className="text-2xl font-bold text-gray-900">
{TOM_GENERATOR_STEPS[currentStepIndex].title.de}
</h2>
<p className="text-gray-600 mt-1">
{TOM_GENERATOR_STEPS[currentStepIndex].description.de}
</p>
</div>
{/* Step Content */}
<div className="min-h-[400px]">{children}</div>
{/* Navigation */}
<WizardNavigation
onPrevious={goToPreviousStep}
onNext={goToNextStep}
onSave={handleSave}
canGoPrevious={canGoPrevious}
canGoNext={canGoNext}
isLastStep={isLastStep}
isSaving={isSaving}
/>
</div>
</div>
</div>
)
}
// =============================================================================
// EXPORTS
// =============================================================================
export { StepIndicator, WizardNavigation, ProgressBar }

View File

@@ -0,0 +1,19 @@
// =============================================================================
// TOM Generator Components - Public API
// =============================================================================
// Main Wizard
export {
TOMGeneratorWizard,
StepIndicator,
WizardNavigation,
ProgressBar,
} from './TOMGeneratorWizard'
// Step Components
export { ScopeRolesStep } from './steps/ScopeRolesStep'
export { DataCategoriesStep } from './steps/DataCategoriesStep'
export { ArchitectureStep } from './steps/ArchitectureStep'
export { SecurityProfileStep } from './steps/SecurityProfileStep'
export { RiskProtectionStep } from './steps/RiskProtectionStep'
export { ReviewExportStep } from './steps/ReviewExportStep'

View File

@@ -0,0 +1,460 @@
'use client'
// =============================================================================
// Step 3: Architecture & Hosting
// Hosting model, location, and provider configuration
// =============================================================================
import React, { useState, useEffect } from 'react'
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
import {
ArchitectureProfile,
HostingModel,
HostingLocation,
MultiTenancy,
CloudProvider,
} from '@/lib/sdk/tom-generator/types'
// =============================================================================
// CONSTANTS
// =============================================================================
const HOSTING_MODELS: { value: HostingModel; label: string; description: string; icon: string }[] = [
{
value: 'ON_PREMISE',
label: 'On-Premise',
description: 'Eigenes Rechenzentrum oder Co-Location',
icon: '🏢',
},
{
value: 'PRIVATE_CLOUD',
label: 'Private Cloud',
description: 'Dedizierte Cloud-Infrastruktur',
icon: '☁️',
},
{
value: 'PUBLIC_CLOUD',
label: 'Public Cloud',
description: 'AWS, Azure, GCP oder andere',
icon: '🌐',
},
{
value: 'HYBRID',
label: 'Hybrid',
description: 'Kombination aus On-Premise und Cloud',
icon: '🔄',
},
]
const HOSTING_LOCATIONS: { value: HostingLocation; label: string; description: string }[] = [
{ value: 'DE', label: 'Deutschland', description: 'Rechenzentrum in Deutschland' },
{ value: 'EU', label: 'EU (nicht DE)', description: 'Innerhalb der EU, aber nicht in Deutschland' },
{ value: 'EEA', label: 'EWR', description: 'Europäischer Wirtschaftsraum' },
{ value: 'THIRD_COUNTRY_ADEQUATE', label: 'Drittland (Angemessenheit)', description: 'Mit Angemessenheitsbeschluss' },
{ value: 'THIRD_COUNTRY', label: 'Drittland (andere)', description: 'Ohne Angemessenheitsbeschluss' },
]
const MULTI_TENANCY_OPTIONS: { value: MultiTenancy; label: string; description: string }[] = [
{ value: 'SINGLE_TENANT', label: 'Single-Tenant', description: 'Dedizierte Instanz pro Kunde' },
{ value: 'MULTI_TENANT', label: 'Multi-Tenant', description: 'Geteilte Infrastruktur mit logischer Trennung' },
{ value: 'DEDICATED', label: 'Dedicated', description: 'Dedizierte Hardware, aber gemeinsame Software' },
]
const COMMON_CERTIFICATIONS = [
'ISO 27001',
'SOC 2 Type II',
'C5',
'TISAX',
'PCI DSS',
'HIPAA',
'FedRAMP',
]
// =============================================================================
// COMPONENT
// =============================================================================
export function ArchitectureStep() {
const { state, setArchitectureProfile, completeCurrentStep } = useTOMGenerator()
const [formData, setFormData] = useState<Partial<ArchitectureProfile>>({
hostingModel: 'PUBLIC_CLOUD',
hostingLocation: 'EU',
providers: [],
multiTenancy: 'MULTI_TENANT',
hasSubprocessors: false,
subprocessorCount: 0,
encryptionAtRest: false,
encryptionInTransit: false,
})
const [newProvider, setNewProvider] = useState<Partial<CloudProvider>>({
name: '',
location: 'EU',
certifications: [],
})
const [certificationInput, setCertificationInput] = useState('')
// Load existing data
useEffect(() => {
if (state.architectureProfile) {
setFormData(state.architectureProfile)
}
}, [state.architectureProfile])
// Handle provider addition
const addProvider = () => {
if (newProvider.name?.trim()) {
const provider: CloudProvider = {
name: newProvider.name.trim(),
location: newProvider.location || 'EU',
certifications: newProvider.certifications || [],
}
setFormData((prev) => ({
...prev,
providers: [...(prev.providers || []), provider],
}))
setNewProvider({ name: '', location: 'EU', certifications: [] })
}
}
const removeProvider = (index: number) => {
setFormData((prev) => ({
...prev,
providers: (prev.providers || []).filter((_, i) => i !== index),
}))
}
// Handle certification toggle
const toggleCertification = (cert: string) => {
setNewProvider((prev) => {
const current = prev.certifications || []
const updated = current.includes(cert)
? current.filter((c) => c !== cert)
: [...current, cert]
return { ...prev, certifications: updated }
})
}
// Handle submit
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const profile: ArchitectureProfile = {
hostingModel: formData.hostingModel || 'PUBLIC_CLOUD',
hostingLocation: formData.hostingLocation || 'EU',
providers: formData.providers || [],
multiTenancy: formData.multiTenancy || 'MULTI_TENANT',
hasSubprocessors: formData.hasSubprocessors || false,
subprocessorCount: formData.subprocessorCount || 0,
encryptionAtRest: formData.encryptionAtRest || false,
encryptionInTransit: formData.encryptionInTransit || false,
}
setArchitectureProfile(profile)
completeCurrentStep(profile)
}
const showProviderSection = formData.hostingModel !== 'ON_PREMISE'
return (
<form onSubmit={handleSubmit} className="space-y-8">
{/* Hosting Model */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Hosting-Modell</h3>
<p className="text-sm text-gray-600 mb-4">
Wie wird Ihre Infrastruktur betrieben?
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{HOSTING_MODELS.map((model) => (
<label
key={model.value}
className={`relative flex items-start p-4 border rounded-lg cursor-pointer transition-all ${
formData.hostingModel === model.value
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<input
type="radio"
name="hostingModel"
value={model.value}
checked={formData.hostingModel === model.value}
onChange={(e) => setFormData((prev) => ({ ...prev, hostingModel: e.target.value as HostingModel }))}
className="sr-only"
/>
<span className="text-2xl mr-3">{model.icon}</span>
<div className="flex-1">
<span className="font-medium text-gray-900">{model.label}</span>
<p className="text-sm text-gray-500 mt-1">{model.description}</p>
</div>
{formData.hostingModel === model.value && (
<svg className="w-5 h-5 text-blue-500 absolute top-3 right-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
)}
</label>
))}
</div>
</div>
{/* Hosting Location */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Primärer Hosting-Standort</h3>
<p className="text-sm text-gray-600 mb-4">
Wo werden die Daten primär gespeichert?
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{HOSTING_LOCATIONS.map((location) => (
<label
key={location.value}
className={`relative flex items-start p-3 border rounded-lg cursor-pointer transition-all ${
formData.hostingLocation === location.value
? location.value.startsWith('THIRD_COUNTRY')
? 'border-amber-500 bg-amber-50'
: 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<input
type="radio"
name="hostingLocation"
value={location.value}
checked={formData.hostingLocation === location.value}
onChange={(e) => setFormData((prev) => ({ ...prev, hostingLocation: e.target.value as HostingLocation }))}
className="sr-only"
/>
<div className="flex-1">
<span className="font-medium text-gray-900">{location.label}</span>
<p className="text-xs text-gray-500 mt-0.5">{location.description}</p>
</div>
</label>
))}
</div>
{formData.hostingLocation?.startsWith('THIRD_COUNTRY') && (
<div className="mt-4 bg-amber-50 border border-amber-200 rounded-lg p-4">
<p className="text-sm text-amber-800">
<strong>Hinweis:</strong> Bei Hosting in Drittländern sind zusätzliche Garantien nach Art. 46 DSGVO
erforderlich (z.B. Standardvertragsklauseln).
</p>
</div>
)}
</div>
{/* Cloud Providers */}
{showProviderSection && (
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Cloud-Provider / Rechenzentren</h3>
<p className="text-sm text-gray-600 mb-4">
Fügen Sie Ihre genutzten Provider hinzu.
</p>
{/* Existing providers */}
{formData.providers && formData.providers.length > 0 && (
<div className="space-y-2 mb-4">
{formData.providers.map((provider, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-gray-50 border rounded-lg"
>
<div>
<span className="font-medium text-gray-900">{provider.name}</span>
<span className="text-sm text-gray-500 ml-2">({provider.location})</span>
{provider.certifications.length > 0 && (
<div className="flex gap-1 mt-1">
{provider.certifications.map((cert) => (
<span
key={cert}
className="px-2 py-0.5 text-xs bg-green-100 text-green-800 rounded"
>
{cert}
</span>
))}
</div>
)}
</div>
<button
type="button"
onClick={() => removeProvider(index)}
className="text-gray-400 hover:text-red-500"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
))}
</div>
)}
{/* Add new provider */}
<div className="border rounded-lg p-4 bg-gray-50">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Provider-Name
</label>
<input
type="text"
value={newProvider.name || ''}
onChange={(e) => setNewProvider((prev) => ({ ...prev, name: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="z.B. AWS, Azure, Hetzner"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Standort
</label>
<select
value={newProvider.location || 'EU'}
onChange={(e) => setNewProvider((prev) => ({ ...prev, location: e.target.value as HostingLocation }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
{HOSTING_LOCATIONS.map((loc) => (
<option key={loc.value} value={loc.value}>{loc.label}</option>
))}
</select>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Zertifizierungen
</label>
<div className="flex flex-wrap gap-2">
{COMMON_CERTIFICATIONS.map((cert) => (
<button
key={cert}
type="button"
onClick={() => toggleCertification(cert)}
className={`px-3 py-1 text-sm rounded-full border transition-all ${
newProvider.certifications?.includes(cert)
? 'bg-green-100 border-green-300 text-green-800'
: 'bg-white border-gray-300 text-gray-600 hover:border-gray-400'
}`}
>
{cert}
</button>
))}
</div>
</div>
<button
type="button"
onClick={addProvider}
disabled={!newProvider.name?.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
Provider hinzufügen
</button>
</div>
</div>
)}
{/* Multi-Tenancy */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Mandantentrennung</h3>
<p className="text-sm text-gray-600 mb-4">
Wie ist die Trennung zwischen verschiedenen Mandanten/Kunden umgesetzt?
</p>
<div className="space-y-3">
{MULTI_TENANCY_OPTIONS.map((option) => (
<label
key={option.value}
className={`relative flex items-start p-4 border rounded-lg cursor-pointer transition-all ${
formData.multiTenancy === option.value
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<input
type="radio"
name="multiTenancy"
value={option.value}
checked={formData.multiTenancy === option.value}
onChange={(e) => setFormData((prev) => ({ ...prev, multiTenancy: e.target.value as MultiTenancy }))}
className="sr-only"
/>
<div className="flex-1">
<span className="font-medium text-gray-900">{option.label}</span>
<p className="text-sm text-gray-500 mt-1">{option.description}</p>
</div>
</label>
))}
</div>
</div>
{/* Subprocessors */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Unterauftragsverarbeiter</h3>
<label className="flex items-center gap-3 cursor-pointer mb-4">
<input
type="checkbox"
checked={formData.hasSubprocessors || false}
onChange={(e) => setFormData((prev) => ({ ...prev, hasSubprocessors: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-gray-700">
Wir setzen Unterauftragsverarbeiter ein
</span>
</label>
{formData.hasSubprocessors && (
<div className="pl-8">
<label className="block text-sm font-medium text-gray-700 mb-1">
Anzahl der Unterauftragsverarbeiter
</label>
<input
type="number"
min="0"
value={formData.subprocessorCount || 0}
onChange={(e) => setFormData((prev) => ({ ...prev, subprocessorCount: parseInt(e.target.value) || 0 }))}
className="w-32 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
)}
</div>
{/* Encryption */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Verschlüsselung</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={formData.encryptionAtRest || false}
onChange={(e) => setFormData((prev) => ({ ...prev, encryptionAtRest: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="text-gray-700 font-medium">Verschlüsselung ruhender Daten (at rest)</span>
<p className="text-sm text-gray-500">Daten werden verschlüsselt gespeichert</p>
</div>
</label>
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={formData.encryptionInTransit || false}
onChange={(e) => setFormData((prev) => ({ ...prev, encryptionInTransit: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="text-gray-700 font-medium">Transportverschlüsselung (in transit)</span>
<p className="text-sm text-gray-500">TLS/SSL für alle Datenübertragungen</p>
</div>
</label>
</div>
</div>
</form>
)
}
export default ArchitectureStep

View File

@@ -0,0 +1,374 @@
'use client'
// =============================================================================
// Step 2: Data Categories
// Data categories and data subjects selection
// =============================================================================
import React, { useState, useEffect } from 'react'
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
import {
DataProfile,
DataCategory,
DataSubject,
DataVolume,
DATA_CATEGORIES_METADATA,
DATA_SUBJECTS_METADATA,
} from '@/lib/sdk/tom-generator/types'
// =============================================================================
// CONSTANTS
// =============================================================================
const DATA_VOLUMES: { value: DataVolume; label: string; description: string }[] = [
{ value: 'LOW', label: 'Niedrig', description: '< 1.000 Datensätze' },
{ value: 'MEDIUM', label: 'Mittel', description: '1.000 - 100.000 Datensätze' },
{ value: 'HIGH', label: 'Hoch', description: '100.000 - 1 Mio. Datensätze' },
{ value: 'VERY_HIGH', label: 'Sehr hoch', description: '> 1 Mio. Datensätze' },
]
// =============================================================================
// COMPONENT
// =============================================================================
export function DataCategoriesStep() {
const { state, setDataProfile, completeCurrentStep } = useTOMGenerator()
const [formData, setFormData] = useState<Partial<DataProfile>>({
categories: [],
subjects: [],
hasSpecialCategories: false,
processesMinors: false,
dataVolume: 'MEDIUM',
thirdCountryTransfers: false,
thirdCountryList: [],
})
const [thirdCountryInput, setThirdCountryInput] = useState('')
// Load existing data
useEffect(() => {
if (state.dataProfile) {
setFormData(state.dataProfile)
}
}, [state.dataProfile])
// Check for special categories
useEffect(() => {
const hasSpecial = formData.categories?.some((cat) => {
const meta = DATA_CATEGORIES_METADATA.find((m) => m.id === cat)
return meta?.isSpecialCategory
})
setFormData((prev) => ({ ...prev, hasSpecialCategories: hasSpecial || false }))
}, [formData.categories])
// Check for minors
useEffect(() => {
const hasMinors = formData.subjects?.includes('MINORS')
setFormData((prev) => ({ ...prev, processesMinors: hasMinors || false }))
}, [formData.subjects])
// Handle category toggle
const toggleCategory = (category: DataCategory) => {
setFormData((prev) => {
const current = prev.categories || []
const updated = current.includes(category)
? current.filter((c) => c !== category)
: [...current, category]
return { ...prev, categories: updated }
})
}
// Handle subject toggle
const toggleSubject = (subject: DataSubject) => {
setFormData((prev) => {
const current = prev.subjects || []
const updated = current.includes(subject)
? current.filter((s) => s !== subject)
: [...current, subject]
return { ...prev, subjects: updated }
})
}
// Handle third country addition
const addThirdCountry = () => {
if (thirdCountryInput.trim()) {
setFormData((prev) => ({
...prev,
thirdCountryList: [...(prev.thirdCountryList || []), thirdCountryInput.trim()],
}))
setThirdCountryInput('')
}
}
const removeThirdCountry = (index: number) => {
setFormData((prev) => ({
...prev,
thirdCountryList: (prev.thirdCountryList || []).filter((_, i) => i !== index),
}))
}
// Handle submit
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const profile: DataProfile = {
categories: formData.categories || [],
subjects: formData.subjects || [],
hasSpecialCategories: formData.hasSpecialCategories || false,
processesMinors: formData.processesMinors || false,
dataVolume: formData.dataVolume || 'MEDIUM',
thirdCountryTransfers: formData.thirdCountryTransfers || false,
thirdCountryList: formData.thirdCountryList || [],
}
setDataProfile(profile)
completeCurrentStep(profile)
}
const selectedSpecialCategories = (formData.categories || []).filter((cat) => {
const meta = DATA_CATEGORIES_METADATA.find((m) => m.id === cat)
return meta?.isSpecialCategory
})
return (
<form onSubmit={handleSubmit} className="space-y-8">
{/* Data Categories */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Datenkategorien</h3>
<p className="text-sm text-gray-600 mb-4">
Wählen Sie alle Kategorien personenbezogener Daten, die Sie verarbeiten.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{DATA_CATEGORIES_METADATA.map((category) => (
<label
key={category.id}
className={`relative flex items-start p-3 border rounded-lg cursor-pointer transition-all ${
formData.categories?.includes(category.id)
? category.isSpecialCategory
? 'border-amber-500 bg-amber-50'
: 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<input
type="checkbox"
checked={formData.categories?.includes(category.id) || false}
onChange={() => toggleCategory(category.id)}
className="sr-only"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900">{category.name.de}</span>
{category.isSpecialCategory && (
<span className="px-2 py-0.5 text-xs font-medium bg-amber-100 text-amber-800 rounded">
Art. 9
</span>
)}
</div>
</div>
{formData.categories?.includes(category.id) && (
<svg className="w-5 h-5 text-blue-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
)}
</label>
))}
</div>
{/* Special Categories Warning */}
{selectedSpecialCategories.length > 0 && (
<div className="mt-4 bg-amber-50 border border-amber-200 rounded-lg p-4">
<div className="flex gap-3">
<svg className="w-5 h-5 text-amber-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<div>
<h4 className="font-medium text-amber-900">Besondere Kategorien nach Art. 9 DSGVO</h4>
<p className="text-sm text-amber-700 mt-1">
Sie verarbeiten besonders schützenswerte Daten. Dies erfordert zusätzliche Schutzmaßnahmen
und möglicherweise eine Datenschutz-Folgenabschätzung (DSFA).
</p>
</div>
</div>
</div>
)}
</div>
{/* Data Subjects */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Betroffene Personen</h3>
<p className="text-sm text-gray-600 mb-4">
Wählen Sie alle Personengruppen, deren Daten Sie verarbeiten.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{DATA_SUBJECTS_METADATA.map((subject) => (
<label
key={subject.id}
className={`relative flex items-start p-3 border rounded-lg cursor-pointer transition-all ${
formData.subjects?.includes(subject.id)
? subject.isVulnerable
? 'border-red-500 bg-red-50'
: 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<input
type="checkbox"
checked={formData.subjects?.includes(subject.id) || false}
onChange={() => toggleSubject(subject.id)}
className="sr-only"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900">{subject.name.de}</span>
{subject.isVulnerable && (
<span className="px-2 py-0.5 text-xs font-medium bg-red-100 text-red-800 rounded">
Schutzbedürftig
</span>
)}
</div>
</div>
{formData.subjects?.includes(subject.id) && (
<svg className="w-5 h-5 text-blue-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
)}
</label>
))}
</div>
{/* Minors Warning */}
{formData.subjects?.includes('MINORS') && (
<div className="mt-4 bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex gap-3">
<svg className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<div>
<h4 className="font-medium text-red-900">Verarbeitung von Minderjährigen-Daten</h4>
<p className="text-sm text-red-700 mt-1">
Die Verarbeitung von Daten Minderjähriger erfordert besondere Schutzmaßnahmen nach Art. 8 DSGVO
und erhöhte Sorgfaltspflichten.
</p>
</div>
</div>
</div>
)}
</div>
{/* Data Volume */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Datenvolumen</h3>
<p className="text-sm text-gray-600 mb-4">
Geschätzte Anzahl der Datensätze, die Sie verarbeiten.
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{DATA_VOLUMES.map((volume) => (
<label
key={volume.value}
className={`relative flex flex-col items-center p-4 border rounded-lg cursor-pointer transition-all text-center ${
formData.dataVolume === volume.value
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<input
type="radio"
name="dataVolume"
value={volume.value}
checked={formData.dataVolume === volume.value}
onChange={(e) => setFormData((prev) => ({ ...prev, dataVolume: e.target.value as DataVolume }))}
className="sr-only"
/>
<span className="font-medium text-gray-900">{volume.label}</span>
<span className="text-xs text-gray-500 mt-1">{volume.description}</span>
</label>
))}
</div>
</div>
{/* Third Country Transfers */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Drittlandübermittlungen</h3>
<p className="text-sm text-gray-600 mb-4">
Werden Daten in Länder außerhalb der EU/EWR übermittelt?
</p>
<div className="space-y-4">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={formData.thirdCountryTransfers || false}
onChange={(e) => setFormData((prev) => ({ ...prev, thirdCountryTransfers: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-gray-700">
Ja, wir übermitteln Daten in Drittländer
</span>
</label>
{formData.thirdCountryTransfers && (
<div className="pl-8">
<label className="block text-sm font-medium text-gray-700 mb-1">
Zielländer
</label>
<div className="flex gap-2">
<input
type="text"
value={thirdCountryInput}
onChange={(e) => setThirdCountryInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addThirdCountry())}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="z.B. USA, Schweiz, UK"
/>
<button
type="button"
onClick={addThirdCountry}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
>
Hinzufügen
</button>
</div>
{formData.thirdCountryList && formData.thirdCountryList.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{formData.thirdCountryList.map((country, index) => (
<span
key={index}
className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-800 rounded-full text-sm"
>
{country}
<button
type="button"
onClick={() => removeThirdCountry(index)}
className="hover:text-gray-600"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</span>
))}
</div>
)}
{formData.thirdCountryList?.includes('USA') && (
<div className="mt-3 bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<p className="text-sm text-yellow-800">
<strong>Hinweis:</strong> Für Übermittlungen in die USA ist seit dem EU-US Data Privacy Framework
ein Angemessenheitsbeschluss vorhanden. Prüfen Sie, ob Ihr US-Partner zertifiziert ist.
</p>
</div>
)}
</div>
)}
</div>
</div>
</form>
)
}
export default DataCategoriesStep

View File

@@ -0,0 +1,593 @@
'use client'
// =============================================================================
// Step 6: Review & Export
// Summary, derived TOMs table, gap analysis, and export
// =============================================================================
import React, { useEffect, useState } from 'react'
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
import { CONTROL_CATEGORIES } from '@/lib/sdk/tom-generator/types'
import { getControlById } from '@/lib/sdk/tom-generator/controls/loader'
import { generateDOCXBlob } from '@/lib/sdk/tom-generator/export/docx'
import { generatePDFBlob } from '@/lib/sdk/tom-generator/export/pdf'
import { generateZIPBlob } from '@/lib/sdk/tom-generator/export/zip'
// =============================================================================
// SUMMARY CARD COMPONENT
// =============================================================================
interface SummaryCardProps {
title: string
value: string | number
description?: string
variant?: 'default' | 'success' | 'warning' | 'danger'
}
function SummaryCard({ title, value, description, variant = 'default' }: SummaryCardProps) {
const colors = {
default: 'bg-gray-100 text-gray-800',
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
danger: 'bg-red-100 text-red-800',
}
return (
<div className={`rounded-lg p-4 ${colors[variant]}`}>
<div className="text-2xl font-bold">{value}</div>
<div className="font-medium">{title}</div>
{description && <div className="text-sm opacity-75 mt-1">{description}</div>}
</div>
)
}
// =============================================================================
// TOMS TABLE COMPONENT
// =============================================================================
function TOMsTable() {
const { state } = useTOMGenerator()
const [selectedCategory, setSelectedCategory] = useState<string>('all')
const [selectedApplicability, setSelectedApplicability] = useState<string>('all')
const filteredTOMs = state.derivedTOMs.filter((tom) => {
const control = getControlById(tom.controlId)
const categoryMatch = selectedCategory === 'all' || control?.category === selectedCategory
const applicabilityMatch = selectedApplicability === 'all' || tom.applicability === selectedApplicability
return categoryMatch && applicabilityMatch
})
const getStatusBadge = (status: string) => {
const badges: Record<string, { bg: string; text: string }> = {
IMPLEMENTED: { bg: 'bg-green-100', text: 'text-green-800' },
PARTIAL: { bg: 'bg-yellow-100', text: 'text-yellow-800' },
NOT_IMPLEMENTED: { bg: 'bg-red-100', text: 'text-red-800' },
}
const labels: Record<string, string> = {
IMPLEMENTED: 'Umgesetzt',
PARTIAL: 'Teilweise',
NOT_IMPLEMENTED: 'Offen',
}
const config = badges[status] || badges.NOT_IMPLEMENTED
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${config.bg} ${config.text}`}>
{labels[status] || status}
</span>
)
}
const getApplicabilityBadge = (applicability: string) => {
const badges: Record<string, { bg: string; text: string }> = {
REQUIRED: { bg: 'bg-red-100', text: 'text-red-800' },
RECOMMENDED: { bg: 'bg-blue-100', text: 'text-blue-800' },
OPTIONAL: { bg: 'bg-gray-100', text: 'text-gray-800' },
NOT_APPLICABLE: { bg: 'bg-gray-50', text: 'text-gray-500' },
}
const labels: Record<string, string> = {
REQUIRED: 'Erforderlich',
RECOMMENDED: 'Empfohlen',
OPTIONAL: 'Optional',
NOT_APPLICABLE: 'N/A',
}
const config = badges[applicability] || badges.OPTIONAL
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${config.bg} ${config.text}`}>
{labels[applicability] || applicability}
</span>
)
}
return (
<div>
{/* Filters */}
<div className="flex flex-wrap gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Kategorie</label>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="all">Alle Kategorien</option>
{CONTROL_CATEGORIES.map((cat) => (
<option key={cat.id} value={cat.id}>{cat.name.de}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Anwendbarkeit</label>
<select
value={selectedApplicability}
onChange={(e) => setSelectedApplicability(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="all">Alle</option>
<option value="REQUIRED">Erforderlich</option>
<option value="RECOMMENDED">Empfohlen</option>
<option value="OPTIONAL">Optional</option>
<option value="NOT_APPLICABLE">Nicht anwendbar</option>
</select>
</div>
</div>
{/* Table */}
<div className="overflow-x-auto border rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
ID
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Maßnahme
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Anwendbarkeit
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nachweise
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredTOMs.map((tom) => (
<tr key={tom.id} className="hover:bg-gray-50">
<td className="px-4 py-3 text-sm font-mono text-gray-900">
{tom.controlId}
</td>
<td className="px-4 py-3">
<div className="text-sm font-medium text-gray-900">{tom.name}</div>
<div className="text-xs text-gray-500 max-w-md truncate">{tom.applicabilityReason}</div>
</td>
<td className="px-4 py-3">
{getApplicabilityBadge(tom.applicability)}
</td>
<td className="px-4 py-3">
{getStatusBadge(tom.implementationStatus)}
</td>
<td className="px-4 py-3 text-sm text-gray-500">
{tom.linkedEvidence.length > 0 ? (
<span className="text-green-600">{tom.linkedEvidence.length} Dok.</span>
) : tom.evidenceGaps.length > 0 ? (
<span className="text-red-600">{tom.evidenceGaps.length} fehlen</span>
) : (
'-'
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-2 text-sm text-gray-500">
{filteredTOMs.length} von {state.derivedTOMs.length} Maßnahmen angezeigt
</div>
</div>
)
}
// =============================================================================
// GAP ANALYSIS PANEL
// =============================================================================
function GapAnalysisPanel() {
const { state, runGapAnalysis } = useTOMGenerator()
useEffect(() => {
if (!state.gapAnalysis && state.derivedTOMs.length > 0) {
runGapAnalysis()
}
}, [state.derivedTOMs, state.gapAnalysis, runGapAnalysis])
if (!state.gapAnalysis) {
return (
<div className="text-center py-8 text-gray-500">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2" />
Lückenanalyse wird durchgeführt...
</div>
)
}
const { overallScore, missingControls, partialControls, recommendations } = state.gapAnalysis
const getScoreColor = (score: number) => {
if (score >= 80) return 'text-green-600'
if (score >= 50) return 'text-yellow-600'
return 'text-red-600'
}
return (
<div className="space-y-6">
{/* Score */}
<div className="text-center">
<div className={`text-5xl font-bold ${getScoreColor(overallScore)}`}>
{overallScore}%
</div>
<div className="text-gray-600 mt-1">Compliance Score</div>
</div>
{/* Progress bar */}
<div className="h-4 bg-gray-200 rounded-full overflow-hidden">
<div
className={`h-full transition-all duration-500 ${
overallScore >= 80 ? 'bg-green-500' : overallScore >= 50 ? 'bg-yellow-500' : 'bg-red-500'
}`}
style={{ width: `${overallScore}%` }}
/>
</div>
{/* Missing Controls */}
{missingControls.length > 0 && (
<div>
<h4 className="font-medium text-gray-900 mb-2">
Fehlende Maßnahmen ({missingControls.length})
</h4>
<div className="space-y-2 max-h-48 overflow-y-auto">
{missingControls.map((mc) => {
const control = getControlById(mc.controlId)
return (
<div key={mc.controlId} className="flex items-center justify-between p-2 bg-red-50 rounded-lg">
<div>
<span className="font-mono text-sm text-gray-600">{mc.controlId}</span>
<span className="ml-2 text-sm text-gray-900">{control?.name.de}</span>
</div>
<span className={`px-2 py-0.5 text-xs rounded ${
mc.priority === 'CRITICAL' ? 'bg-red-200 text-red-800' :
mc.priority === 'HIGH' ? 'bg-orange-200 text-orange-800' :
'bg-gray-200 text-gray-800'
}`}>
{mc.priority}
</span>
</div>
)
})}
</div>
</div>
)}
{/* Partial Controls */}
{partialControls.length > 0 && (
<div>
<h4 className="font-medium text-gray-900 mb-2">
Teilweise umgesetzt ({partialControls.length})
</h4>
<div className="space-y-2 max-h-48 overflow-y-auto">
{partialControls.map((pc) => {
const control = getControlById(pc.controlId)
return (
<div key={pc.controlId} className="p-2 bg-yellow-50 rounded-lg">
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-gray-600">{pc.controlId}</span>
<span className="text-sm text-gray-900">{control?.name.de}</span>
</div>
<div className="text-xs text-yellow-700 mt-1">
Fehlend: {pc.missingAspects.join(', ')}
</div>
</div>
)
})}
</div>
</div>
)}
{/* Recommendations */}
{recommendations.length > 0 && (
<div>
<h4 className="font-medium text-gray-900 mb-2">Empfehlungen</h4>
<ul className="space-y-2">
{recommendations.map((rec, index) => (
<li key={index} className="flex items-start gap-2 text-sm text-gray-600">
<svg className="w-4 h-4 text-blue-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
{rec}
</li>
))}
</ul>
</div>
)}
</div>
)
}
// =============================================================================
// EXPORT PANEL
// =============================================================================
function ExportPanel() {
const { state, addExport } = useTOMGenerator()
const [isExporting, setIsExporting] = useState<string | null>(null)
const handleExport = async (format: 'docx' | 'pdf' | 'json' | 'zip') => {
setIsExporting(format)
try {
let blob: Blob
let filename: string
switch (format) {
case 'docx':
blob = await generateDOCXBlob(state, { language: 'de' })
filename = `TOM-Dokumentation-${new Date().toISOString().split('T')[0]}.docx`
break
case 'pdf':
blob = await generatePDFBlob(state, { language: 'de' })
filename = `TOM-Dokumentation-${new Date().toISOString().split('T')[0]}.pdf`
break
case 'json':
blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
filename = `TOM-Export-${new Date().toISOString().split('T')[0]}.json`
break
case 'zip':
blob = await generateZIPBlob(state, { language: 'de' })
filename = `TOM-Package-${new Date().toISOString().split('T')[0]}.zip`
break
default:
return
}
// Download
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
// Record export
addExport({
id: `export-${Date.now()}`,
format: format.toUpperCase() as 'DOCX' | 'PDF' | 'JSON' | 'ZIP',
generatedAt: new Date(),
filename,
})
} catch (error) {
console.error('Export failed:', error)
} finally {
setIsExporting(null)
}
}
const exportFormats = [
{ id: 'docx', label: 'Word (.docx)', icon: '📄', description: 'Bearbeitbares Dokument' },
{ id: 'pdf', label: 'PDF', icon: '📕', description: 'Druckversion' },
{ id: 'json', label: 'JSON', icon: '💾', description: 'Maschinelles Format' },
{ id: 'zip', label: 'ZIP-Paket', icon: '📦', description: 'Vollständiges Paket' },
]
return (
<div className="space-y-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{exportFormats.map((format) => (
<button
key={format.id}
onClick={() => handleExport(format.id as 'docx' | 'pdf' | 'json' | 'zip')}
disabled={isExporting !== null}
className={`p-4 border rounded-lg text-center transition-all ${
isExporting === format.id
? 'bg-blue-50 border-blue-300'
: 'hover:bg-gray-50 hover:border-gray-300'
} disabled:opacity-50 disabled:cursor-not-allowed`}
>
<div className="text-3xl mb-2">{format.icon}</div>
<div className="font-medium text-gray-900">{format.label}</div>
<div className="text-xs text-gray-500">{format.description}</div>
{isExporting === format.id && (
<div className="mt-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mx-auto" />
</div>
)}
</button>
))}
</div>
{/* Export History */}
{state.exports.length > 0 && (
<div className="mt-6">
<h4 className="font-medium text-gray-900 mb-2">Letzte Exporte</h4>
<div className="space-y-2">
{state.exports.slice(-5).reverse().map((exp) => (
<div key={exp.id} className="flex items-center justify-between p-2 bg-gray-50 rounded-lg text-sm">
<span className="font-medium">{exp.filename}</span>
<span className="text-gray-500">
{new Date(exp.generatedAt).toLocaleString('de-DE')}
</span>
</div>
))}
</div>
</div>
)}
</div>
)
}
// =============================================================================
// MAIN COMPONENT
// =============================================================================
export function ReviewExportStep() {
const { state, deriveTOMs, completeCurrentStep } = useTOMGenerator()
const [activeTab, setActiveTab] = useState<'summary' | 'toms' | 'gaps' | 'export'>('summary')
// Derive TOMs if not already done
useEffect(() => {
if (state.derivedTOMs.length === 0 && state.companyProfile && state.dataProfile) {
deriveTOMs()
}
}, [state, deriveTOMs])
// Mark step as complete when viewing
useEffect(() => {
completeCurrentStep({ reviewed: true })
}, [completeCurrentStep])
// Statistics
const stats = {
totalTOMs: state.derivedTOMs.length,
required: state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length,
implemented: state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length,
partial: state.derivedTOMs.filter((t) => t.implementationStatus === 'PARTIAL').length,
documents: state.documents.length,
score: state.gapAnalysis?.overallScore ?? 0,
}
const tabs = [
{ id: 'summary', label: 'Zusammenfassung' },
{ id: 'toms', label: 'TOMs-Tabelle' },
{ id: 'gaps', label: 'Lückenanalyse' },
{ id: 'export', label: 'Export' },
]
return (
<div className="space-y-6">
{/* Tabs */}
<div className="border-b">
<nav className="flex space-x-8">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as typeof activeTab)}
className={`py-3 px-1 border-b-2 font-medium text-sm transition-all ${
activeTab === tab.id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab.label}
</button>
))}
</nav>
</div>
{/* Tab Content */}
<div className="min-h-[400px]">
{activeTab === 'summary' && (
<div className="space-y-6">
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<SummaryCard
title="Gesamt TOMs"
value={stats.totalTOMs}
variant="default"
/>
<SummaryCard
title="Erforderlich"
value={stats.required}
variant="danger"
/>
<SummaryCard
title="Umgesetzt"
value={stats.implemented}
variant="success"
/>
<SummaryCard
title="Teilweise"
value={stats.partial}
variant="warning"
/>
<SummaryCard
title="Dokumente"
value={stats.documents}
variant="default"
/>
<SummaryCard
title="Score"
value={`${stats.score}%`}
variant={stats.score >= 80 ? 'success' : stats.score >= 50 ? 'warning' : 'danger'}
/>
</div>
{/* Profile Summaries */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Company */}
{state.companyProfile && (
<div className="bg-white border rounded-lg p-4">
<h4 className="font-medium text-gray-900 mb-3">Unternehmen</h4>
<dl className="space-y-2 text-sm">
<div className="flex justify-between">
<dt className="text-gray-500">Name:</dt>
<dd className="text-gray-900">{state.companyProfile.name}</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-500">Branche:</dt>
<dd className="text-gray-900">{state.companyProfile.industry}</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-500">Rolle:</dt>
<dd className="text-gray-900">{state.companyProfile.role}</dd>
</div>
</dl>
</div>
)}
{/* Risk */}
{state.riskProfile && (
<div className="bg-white border rounded-lg p-4">
<h4 className="font-medium text-gray-900 mb-3">Schutzbedarf</h4>
<dl className="space-y-2 text-sm">
<div className="flex justify-between">
<dt className="text-gray-500">Level:</dt>
<dd className={`font-medium ${
state.riskProfile.protectionLevel === 'VERY_HIGH' ? 'text-red-600' :
state.riskProfile.protectionLevel === 'HIGH' ? 'text-yellow-600' : 'text-green-600'
}`}>
{state.riskProfile.protectionLevel}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-500">DSFA erforderlich:</dt>
<dd className={state.riskProfile.dsfaRequired ? 'text-red-600 font-medium' : 'text-gray-900'}>
{state.riskProfile.dsfaRequired ? 'Ja' : 'Nein'}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-500">CIA (V/I/V):</dt>
<dd className="text-gray-900">
{state.riskProfile.ciaAssessment.confidentiality}/
{state.riskProfile.ciaAssessment.integrity}/
{state.riskProfile.ciaAssessment.availability}
</dd>
</div>
</dl>
</div>
)}
</div>
</div>
)}
{activeTab === 'toms' && <TOMsTable />}
{activeTab === 'gaps' && <GapAnalysisPanel />}
{activeTab === 'export' && <ExportPanel />}
</div>
</div>
)
}
export default ReviewExportStep

View File

@@ -0,0 +1,422 @@
'use client'
// =============================================================================
// Step 5: Risk & Protection Level
// CIA assessment and protection level determination
// =============================================================================
import React, { useState, useEffect } from 'react'
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
import {
RiskProfile,
CIARating,
ProtectionLevel,
calculateProtectionLevel,
isDSFARequired,
} from '@/lib/sdk/tom-generator/types'
// =============================================================================
// CONSTANTS
// =============================================================================
const CIA_LEVELS: { value: CIARating; label: string; description: string }[] = [
{ value: 1, label: 'Sehr gering', description: 'Kein nennenswerter Schaden bei Verletzung' },
{ value: 2, label: 'Gering', description: 'Begrenzter, beherrschbarer Schaden' },
{ value: 3, label: 'Mittel', description: 'Erheblicher Schaden, aber kompensierbar' },
{ value: 4, label: 'Hoch', description: 'Schwerwiegender Schaden, schwer kompensierbar' },
{ value: 5, label: 'Sehr hoch', description: 'Existenzbedrohender oder irreversibler Schaden' },
]
const REGULATORY_REQUIREMENTS = [
'DSGVO',
'BDSG',
'MaRisk (Finanz)',
'BAIT (Finanz)',
'PSD2 (Zahlungsdienste)',
'SGB (Gesundheit)',
'MDR (Medizinprodukte)',
'TISAX (Automotive)',
'KRITIS (Kritische Infrastruktur)',
'NIS2',
'ISO 27001',
'SOC 2',
]
// =============================================================================
// CIA SLIDER COMPONENT
// =============================================================================
interface CIASliderProps {
label: string
description: string
value: CIARating
onChange: (value: CIARating) => void
}
function CIASlider({ label, description, value, onChange }: CIASliderProps) {
const level = CIA_LEVELS.find((l) => l.value === value)
const getColor = (v: CIARating) => {
if (v <= 2) return 'bg-green-500'
if (v === 3) return 'bg-yellow-500'
return 'bg-red-500'
}
return (
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex justify-between items-start mb-3">
<div>
<h4 className="font-medium text-gray-900">{label}</h4>
<p className="text-sm text-gray-500">{description}</p>
</div>
<span className={`px-3 py-1 rounded-full text-sm font-medium text-white ${getColor(value)}`}>
{level?.label}
</span>
</div>
<input
type="range"
min="1"
max="5"
value={value}
onChange={(e) => onChange(parseInt(e.target.value) as CIARating)}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
<div className="flex justify-between mt-1 text-xs text-gray-400">
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
</div>
<p className="text-sm text-gray-600 mt-2 italic">{level?.description}</p>
</div>
)
}
// =============================================================================
// PROTECTION LEVEL DISPLAY
// =============================================================================
interface ProtectionLevelDisplayProps {
level: ProtectionLevel
}
function ProtectionLevelDisplay({ level }: ProtectionLevelDisplayProps) {
const config: Record<ProtectionLevel, { label: string; color: string; bg: string; description: string }> = {
NORMAL: {
label: 'Normal',
color: 'text-green-800',
bg: 'bg-green-100',
description: 'Standard-Schutzmaßnahmen ausreichend',
},
HIGH: {
label: 'Hoch',
color: 'text-yellow-800',
bg: 'bg-yellow-100',
description: 'Erweiterte Schutzmaßnahmen erforderlich',
},
VERY_HIGH: {
label: 'Sehr hoch',
color: 'text-red-800',
bg: 'bg-red-100',
description: 'Höchste Schutzmaßnahmen erforderlich',
},
}
const { label, color, bg, description } = config[level]
return (
<div className={`${bg} rounded-lg p-4 border`}>
<div className="flex items-center gap-3">
<div className={`text-2xl font-bold ${color}`}>{label}</div>
</div>
<p className={`text-sm ${color} mt-1`}>{description}</p>
</div>
)
}
// =============================================================================
// COMPONENT
// =============================================================================
export function RiskProtectionStep() {
const { state, setRiskProfile, completeCurrentStep } = useTOMGenerator()
const [formData, setFormData] = useState<Partial<RiskProfile>>({
ciaAssessment: {
confidentiality: 3,
integrity: 3,
availability: 3,
justification: '',
},
protectionLevel: 'HIGH',
specialRisks: [],
regulatoryRequirements: ['DSGVO'],
hasHighRiskProcessing: false,
dsfaRequired: false,
})
const [specialRiskInput, setSpecialRiskInput] = useState('')
// Load existing data
useEffect(() => {
if (state.riskProfile) {
setFormData(state.riskProfile)
}
}, [state.riskProfile])
// Calculate protection level when CIA changes
useEffect(() => {
if (formData.ciaAssessment) {
const level = calculateProtectionLevel(formData.ciaAssessment)
const dsfaReq = isDSFARequired(state.dataProfile, {
...formData,
protectionLevel: level,
} as RiskProfile)
setFormData((prev) => ({
...prev,
protectionLevel: level,
dsfaRequired: dsfaReq,
}))
}
}, [formData.ciaAssessment, state.dataProfile])
// Handle CIA changes
const handleCIAChange = (field: 'confidentiality' | 'integrity' | 'availability', value: CIARating) => {
setFormData((prev) => ({
...prev,
ciaAssessment: {
...prev.ciaAssessment!,
[field]: value,
},
}))
}
// Handle regulatory requirements toggle
const toggleRequirement = (req: string) => {
setFormData((prev) => {
const current = prev.regulatoryRequirements || []
const updated = current.includes(req)
? current.filter((r) => r !== req)
: [...current, req]
return { ...prev, regulatoryRequirements: updated }
})
}
// Handle special risk addition
const addSpecialRisk = () => {
if (specialRiskInput.trim()) {
setFormData((prev) => ({
...prev,
specialRisks: [...(prev.specialRisks || []), specialRiskInput.trim()],
}))
setSpecialRiskInput('')
}
}
const removeSpecialRisk = (index: number) => {
setFormData((prev) => ({
...prev,
specialRisks: (prev.specialRisks || []).filter((_, i) => i !== index),
}))
}
// Handle submit
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const profile: RiskProfile = {
ciaAssessment: formData.ciaAssessment!,
protectionLevel: formData.protectionLevel || 'HIGH',
specialRisks: formData.specialRisks || [],
regulatoryRequirements: formData.regulatoryRequirements || [],
hasHighRiskProcessing: formData.hasHighRiskProcessing || false,
dsfaRequired: formData.dsfaRequired || false,
}
setRiskProfile(profile)
completeCurrentStep(profile)
}
return (
<form onSubmit={handleSubmit} className="space-y-8">
{/* CIA Assessment */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">CIA-Bewertung</h3>
<p className="text-sm text-gray-600 mb-4">
Bewerten Sie die Schutzziele für Ihre Datenverarbeitung. Was passiert, wenn die Vertraulichkeit,
Integrität oder Verfügbarkeit der Daten beeinträchtigt wird?
</p>
<div className="space-y-4">
<CIASlider
label="Vertraulichkeit (Confidentiality)"
description="Schutz vor unbefugtem Zugriff auf Daten"
value={formData.ciaAssessment?.confidentiality || 3}
onChange={(v) => handleCIAChange('confidentiality', v)}
/>
<CIASlider
label="Integrität (Integrity)"
description="Schutz vor unbefugter Änderung von Daten"
value={formData.ciaAssessment?.integrity || 3}
onChange={(v) => handleCIAChange('integrity', v)}
/>
<CIASlider
label="Verfügbarkeit (Availability)"
description="Sicherstellung des Zugriffs auf Daten"
value={formData.ciaAssessment?.availability || 3}
onChange={(v) => handleCIAChange('availability', v)}
/>
</div>
{/* Justification */}
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Begründung der Bewertung
</label>
<textarea
value={formData.ciaAssessment?.justification || ''}
onChange={(e) =>
setFormData((prev) => ({
...prev,
ciaAssessment: {
...prev.ciaAssessment!,
justification: e.target.value,
},
}))
}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="Beschreiben Sie kurz, warum Sie diese Bewertung gewählt haben..."
/>
</div>
</div>
{/* Calculated Protection Level */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Ermittelter Schutzbedarf</h3>
<p className="text-sm text-gray-600 mb-4">
Basierend auf Ihrer CIA-Bewertung ergibt sich folgender Schutzbedarf:
</p>
<ProtectionLevelDisplay level={formData.protectionLevel || 'HIGH'} />
</div>
{/* DSFA Indicator */}
{formData.dsfaRequired && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex gap-3">
<svg className="w-6 h-6 text-red-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<div>
<h4 className="font-medium text-red-900">DSFA erforderlich</h4>
<p className="text-sm text-red-700 mt-1">
Aufgrund Ihrer Datenverarbeitung (besondere Kategorien, Minderjährige oder sehr hoher Schutzbedarf)
ist eine Datenschutz-Folgenabschätzung nach Art. 35 DSGVO erforderlich.
</p>
</div>
</div>
</div>
)}
{/* High Risk Processing */}
<div>
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
<input
type="checkbox"
checked={formData.hasHighRiskProcessing || false}
onChange={(e) => setFormData((prev) => ({ ...prev, hasHighRiskProcessing: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="text-gray-700 font-medium">Hochrisiko-Verarbeitung</span>
<p className="text-sm text-gray-500">
z.B. Profiling, automatisierte Entscheidungen, systematische Überwachung
</p>
</div>
</label>
</div>
{/* Special Risks */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Besondere Risiken</h3>
<p className="text-sm text-gray-600 mb-4">
Identifizieren Sie spezifische Risiken Ihrer Datenverarbeitung.
</p>
<div className="flex gap-2 mb-3">
<input
type="text"
value={specialRiskInput}
onChange={(e) => setSpecialRiskInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addSpecialRisk())}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="z.B. Cloud-Abhängigkeit, Insider-Bedrohungen"
/>
<button
type="button"
onClick={addSpecialRisk}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
>
Hinzufügen
</button>
</div>
{formData.specialRisks && formData.specialRisks.length > 0 && (
<div className="flex flex-wrap gap-2">
{formData.specialRisks.map((risk, index) => (
<span
key={index}
className="inline-flex items-center gap-1 px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm"
>
{risk}
<button
type="button"
onClick={() => removeSpecialRisk(index)}
className="hover:text-red-600"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</span>
))}
</div>
)}
</div>
{/* Regulatory Requirements */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Regulatorische Anforderungen</h3>
<p className="text-sm text-gray-600 mb-4">
Welche regulatorischen Anforderungen gelten für Ihre Datenverarbeitung?
</p>
<div className="flex flex-wrap gap-2">
{REGULATORY_REQUIREMENTS.map((req) => (
<button
key={req}
type="button"
onClick={() => toggleRequirement(req)}
className={`px-4 py-2 rounded-full border text-sm font-medium transition-all ${
formData.regulatoryRequirements?.includes(req)
? 'bg-blue-100 border-blue-300 text-blue-800'
: 'bg-white border-gray-300 text-gray-600 hover:border-gray-400'
}`}
>
{req}
</button>
))}
</div>
</div>
</form>
)
}
export default RiskProtectionStep

View File

@@ -0,0 +1,403 @@
'use client'
// =============================================================================
// Step 1: Scope & Roles
// Company profile and role definition
// =============================================================================
import React, { useState, useEffect } from 'react'
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
import {
CompanyProfile,
CompanyRole,
CompanySize,
} from '@/lib/sdk/tom-generator/types'
// =============================================================================
// CONSTANTS
// =============================================================================
const COMPANY_SIZES: { value: CompanySize; label: string; description: string }[] = [
{ value: 'MICRO', label: 'Kleinstunternehmen', description: '< 10 Mitarbeiter' },
{ value: 'SMALL', label: 'Kleinunternehmen', description: '10-49 Mitarbeiter' },
{ value: 'MEDIUM', label: 'Mittelunternehmen', description: '50-249 Mitarbeiter' },
{ value: 'LARGE', label: 'Großunternehmen', description: '250-999 Mitarbeiter' },
{ value: 'ENTERPRISE', label: 'Konzern', description: '1000+ Mitarbeiter' },
]
const COMPANY_ROLES: { value: CompanyRole; label: string; description: string }[] = [
{
value: 'CONTROLLER',
label: 'Verantwortlicher',
description: 'Sie bestimmen Zweck und Mittel der Datenverarbeitung',
},
{
value: 'PROCESSOR',
label: 'Auftragsverarbeiter',
description: 'Sie verarbeiten Daten im Auftrag eines Verantwortlichen',
},
{
value: 'JOINT_CONTROLLER',
label: 'Gemeinsam Verantwortlicher',
description: 'Sie bestimmen gemeinsam mit anderen Zweck und Mittel',
},
]
const INDUSTRIES = [
'Software / IT',
'Finanzdienstleistungen',
'Gesundheitswesen',
'E-Commerce / Handel',
'Beratung / Professional Services',
'Produktion / Industrie',
'Bildung / Forschung',
'Öffentlicher Sektor',
'Medien / Kommunikation',
'Transport / Logistik',
'Sonstige',
]
// =============================================================================
// COMPONENT
// =============================================================================
export function ScopeRolesStep() {
const { state, setCompanyProfile, completeCurrentStep } = useTOMGenerator()
const [formData, setFormData] = useState<Partial<CompanyProfile>>({
id: '',
name: '',
industry: '',
size: 'MEDIUM',
role: 'CONTROLLER',
products: [],
dpoPerson: '',
dpoEmail: '',
itSecurityContact: '',
})
const [productInput, setProductInput] = useState('')
const [errors, setErrors] = useState<Record<string, string>>({})
// Load existing data
useEffect(() => {
if (state.companyProfile) {
setFormData(state.companyProfile)
}
}, [state.companyProfile])
// Validation
const validate = (): boolean => {
const newErrors: Record<string, string> = {}
if (!formData.name?.trim()) {
newErrors.name = 'Unternehmensname ist erforderlich'
}
if (!formData.industry) {
newErrors.industry = 'Bitte wählen Sie eine Branche'
}
if (!formData.role) {
newErrors.role = 'Bitte wählen Sie eine Rolle'
}
if (formData.dpoEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.dpoEmail)) {
newErrors.dpoEmail = 'Bitte geben Sie eine gültige E-Mail-Adresse ein'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
// Handle form changes
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = e.target
setFormData((prev) => ({ ...prev, [name]: value }))
// Clear error when field is edited
if (errors[name]) {
setErrors((prev) => ({ ...prev, [name]: '' }))
}
}
// Handle product addition
const addProduct = () => {
if (productInput.trim()) {
setFormData((prev) => ({
...prev,
products: [...(prev.products || []), productInput.trim()],
}))
setProductInput('')
}
}
const removeProduct = (index: number) => {
setFormData((prev) => ({
...prev,
products: (prev.products || []).filter((_, i) => i !== index),
}))
}
// Handle submit
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!validate()) return
const profile: CompanyProfile = {
id: formData.id || `company-${Date.now()}`,
name: formData.name!,
industry: formData.industry!,
size: formData.size!,
role: formData.role!,
products: formData.products || [],
dpoPerson: formData.dpoPerson || null,
dpoEmail: formData.dpoEmail || null,
itSecurityContact: formData.itSecurityContact || null,
}
setCompanyProfile(profile)
completeCurrentStep(profile)
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Company Name */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Unternehmensname <span className="text-red-500">*</span>
</label>
<input
type="text"
name="name"
value={formData.name || ''}
onChange={handleChange}
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
errors.name ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="z.B. Muster GmbH"
/>
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name}</p>}
</div>
{/* Industry */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Branche <span className="text-red-500">*</span>
</label>
<select
name="industry"
value={formData.industry || ''}
onChange={handleChange}
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
errors.industry ? 'border-red-500' : 'border-gray-300'
}`}
>
<option value="">Bitte wählen...</option>
{INDUSTRIES.map((industry) => (
<option key={industry} value={industry}>
{industry}
</option>
))}
</select>
{errors.industry && <p className="mt-1 text-sm text-red-500">{errors.industry}</p>}
</div>
{/* Company Size */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Unternehmensgröße <span className="text-red-500">*</span>
</label>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{COMPANY_SIZES.map((size) => (
<label
key={size.value}
className={`relative flex items-start p-3 border rounded-lg cursor-pointer transition-all ${
formData.size === size.value
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<input
type="radio"
name="size"
value={size.value}
checked={formData.size === size.value}
onChange={handleChange}
className="sr-only"
/>
<div>
<span className="font-medium text-gray-900">{size.label}</span>
<p className="text-sm text-gray-500">{size.description}</p>
</div>
{formData.size === size.value && (
<div className="absolute top-2 right-2">
<svg className="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
)}
</label>
))}
</div>
</div>
{/* Company Role */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Ihre Rolle nach DSGVO <span className="text-red-500">*</span>
</label>
<div className="space-y-3">
{COMPANY_ROLES.map((role) => (
<label
key={role.value}
className={`relative flex items-start p-4 border rounded-lg cursor-pointer transition-all ${
formData.role === role.value
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<input
type="radio"
name="role"
value={role.value}
checked={formData.role === role.value}
onChange={handleChange}
className="sr-only"
/>
<div className="flex-1">
<span className="font-medium text-gray-900">{role.label}</span>
<p className="text-sm text-gray-500 mt-1">{role.description}</p>
</div>
{formData.role === role.value && (
<div className="flex-shrink-0 ml-3">
<svg className="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
)}
</label>
))}
</div>
{errors.role && <p className="mt-1 text-sm text-red-500">{errors.role}</p>}
</div>
{/* Products/Services */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Produkte / Services
</label>
<div className="flex gap-2">
<input
type="text"
value={productInput}
onChange={(e) => setProductInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addProduct())}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="z.B. Cloud CRM, API Services"
/>
<button
type="button"
onClick={addProduct}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
>
Hinzufügen
</button>
</div>
{formData.products && formData.products.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{formData.products.map((product, index) => (
<span
key={index}
className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm"
>
{product}
<button
type="button"
onClick={() => removeProduct(index)}
className="hover:text-blue-600"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</span>
))}
</div>
)}
</div>
{/* Contact Information */}
<div className="border-t pt-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Kontaktinformationen</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Datenschutzbeauftragter
</label>
<input
type="text"
name="dpoPerson"
value={formData.dpoPerson || ''}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Name des DSB"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
E-Mail des DSB
</label>
<input
type="email"
name="dpoEmail"
value={formData.dpoEmail || ''}
onChange={handleChange}
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
errors.dpoEmail ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="dpo@example.de"
/>
{errors.dpoEmail && <p className="mt-1 text-sm text-red-500">{errors.dpoEmail}</p>}
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
IT-Security Ansprechpartner
</label>
<input
type="text"
name="itSecurityContact"
value={formData.itSecurityContact || ''}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Name oder Team"
/>
</div>
</div>
</div>
{/* Info Box */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex gap-3">
<svg className="w-5 h-5 text-blue-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
<div>
<h4 className="font-medium text-blue-900">Hinweis zur Rollenwahl</h4>
<p className="text-sm text-blue-700 mt-1">
Die Wahl Ihrer DSGVO-Rolle beeinflusst, welche TOMs für Sie relevant sind.
Als <strong>Auftragsverarbeiter</strong> gelten zusätzliche Anforderungen nach Art. 28 DSGVO.
</p>
</div>
</div>
</div>
</form>
)
}
export default ScopeRolesStep

View File

@@ -0,0 +1,417 @@
'use client'
// =============================================================================
// Step 4: Security Profile
// Authentication, encryption, and security configuration
// =============================================================================
import React, { useState, useEffect } from 'react'
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
import {
SecurityProfile,
AuthMethodType,
BackupFrequency,
} from '@/lib/sdk/tom-generator/types'
// =============================================================================
// CONSTANTS
// =============================================================================
const AUTH_METHODS: { value: AuthMethodType; label: string; description: string }[] = [
{ value: 'PASSWORD', label: 'Passwort', description: 'Standard-Passwortauthentifizierung' },
{ value: 'MFA', label: 'Multi-Faktor (MFA)', description: 'Zweiter Faktor erforderlich' },
{ value: 'SSO', label: 'Single Sign-On', description: 'Zentralisierte Anmeldung' },
{ value: 'CERTIFICATE', label: 'Zertifikat', description: 'Client-Zertifikate' },
{ value: 'BIOMETRIC', label: 'Biometrisch', description: 'Fingerabdruck, Gesicht, etc.' },
]
const BACKUP_FREQUENCIES: { value: BackupFrequency; label: string }[] = [
{ value: 'HOURLY', label: 'Stündlich' },
{ value: 'DAILY', label: 'Täglich' },
{ value: 'WEEKLY', label: 'Wöchentlich' },
{ value: 'MONTHLY', label: 'Monatlich' },
]
// =============================================================================
// COMPONENT
// =============================================================================
export function SecurityProfileStep() {
const { state, setSecurityProfile, completeCurrentStep } = useTOMGenerator()
const [formData, setFormData] = useState<Partial<SecurityProfile>>({
authMethods: [],
hasMFA: false,
hasSSO: false,
hasIAM: false,
hasPAM: false,
hasEncryptionAtRest: false,
hasEncryptionInTransit: false,
hasLogging: false,
logRetentionDays: 90,
hasBackup: false,
backupFrequency: 'DAILY',
backupRetentionDays: 30,
hasDRPlan: false,
rtoHours: null,
rpoHours: null,
hasVulnerabilityManagement: false,
hasPenetrationTests: false,
hasSecurityTraining: false,
})
// Load existing data
useEffect(() => {
if (state.securityProfile) {
setFormData(state.securityProfile)
}
}, [state.securityProfile])
// Sync auth methods with boolean flags
useEffect(() => {
const authTypes = formData.authMethods?.map((m) => m.type) || []
setFormData((prev) => ({
...prev,
hasMFA: authTypes.includes('MFA'),
hasSSO: authTypes.includes('SSO'),
}))
}, [formData.authMethods])
// Handle auth method toggle
const toggleAuthMethod = (method: AuthMethodType) => {
setFormData((prev) => {
const current = prev.authMethods || []
const exists = current.some((m) => m.type === method)
const updated = exists
? current.filter((m) => m.type !== method)
: [...current, { type: method, provider: null }]
return { ...prev, authMethods: updated }
})
}
// Handle submit
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const profile: SecurityProfile = {
authMethods: formData.authMethods || [],
hasMFA: formData.hasMFA || false,
hasSSO: formData.hasSSO || false,
hasIAM: formData.hasIAM || false,
hasPAM: formData.hasPAM || false,
hasEncryptionAtRest: formData.hasEncryptionAtRest || false,
hasEncryptionInTransit: formData.hasEncryptionInTransit || false,
hasLogging: formData.hasLogging || false,
logRetentionDays: formData.logRetentionDays || 90,
hasBackup: formData.hasBackup || false,
backupFrequency: formData.backupFrequency || 'DAILY',
backupRetentionDays: formData.backupRetentionDays || 30,
hasDRPlan: formData.hasDRPlan || false,
rtoHours: formData.rtoHours ?? null,
rpoHours: formData.rpoHours ?? null,
hasVulnerabilityManagement: formData.hasVulnerabilityManagement || false,
hasPenetrationTests: formData.hasPenetrationTests || false,
hasSecurityTraining: formData.hasSecurityTraining || false,
}
setSecurityProfile(profile)
completeCurrentStep(profile)
}
const selectedAuthMethods = formData.authMethods?.map((m) => m.type) || []
return (
<form onSubmit={handleSubmit} className="space-y-8">
{/* Authentication Methods */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Authentifizierungsmethoden</h3>
<p className="text-sm text-gray-600 mb-4">
Welche Authentifizierungsmethoden werden verwendet?
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{AUTH_METHODS.map((method) => (
<label
key={method.value}
className={`relative flex items-start p-3 border rounded-lg cursor-pointer transition-all ${
selectedAuthMethods.includes(method.value)
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<input
type="checkbox"
checked={selectedAuthMethods.includes(method.value)}
onChange={() => toggleAuthMethod(method.value)}
className="sr-only"
/>
<div className="flex-1">
<span className="font-medium text-gray-900">{method.label}</span>
<p className="text-xs text-gray-500 mt-0.5">{method.description}</p>
</div>
{selectedAuthMethods.includes(method.value) && (
<svg className="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
)}
</label>
))}
</div>
{/* MFA recommendation */}
{!selectedAuthMethods.includes('MFA') && (
<div className="mt-4 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<p className="text-sm text-yellow-800">
<strong>Empfehlung:</strong> Multi-Faktor-Authentifizierung (MFA) wird für alle sensiblen
Systeme dringend empfohlen und ist bei besonderen Datenkategorien oft erforderlich.
</p>
</div>
)}
</div>
{/* Identity & Access Management */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Identity & Access Management</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
<input
type="checkbox"
checked={formData.hasIAM || false}
onChange={(e) => setFormData((prev) => ({ ...prev, hasIAM: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="text-gray-700 font-medium">Identity & Access Management (IAM)</span>
<p className="text-sm text-gray-500">Zentralisierte Benutzer- und Berechtigungsverwaltung</p>
</div>
</label>
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
<input
type="checkbox"
checked={formData.hasPAM || false}
onChange={(e) => setFormData((prev) => ({ ...prev, hasPAM: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="text-gray-700 font-medium">Privileged Access Management (PAM)</span>
<p className="text-sm text-gray-500">Kontrolle privilegierter Zugänge (Admin-Konten)</p>
</div>
</label>
</div>
</div>
{/* Encryption */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Verschlüsselung</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
<input
type="checkbox"
checked={formData.hasEncryptionAtRest || false}
onChange={(e) => setFormData((prev) => ({ ...prev, hasEncryptionAtRest: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="text-gray-700 font-medium">Verschlüsselung ruhender Daten</span>
<p className="text-sm text-gray-500">AES-256 oder vergleichbar für gespeicherte Daten</p>
</div>
</label>
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
<input
type="checkbox"
checked={formData.hasEncryptionInTransit || false}
onChange={(e) => setFormData((prev) => ({ ...prev, hasEncryptionInTransit: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="text-gray-700 font-medium">Transportverschlüsselung</span>
<p className="text-sm text-gray-500">TLS 1.2+ für alle Datenübertragungen</p>
</div>
</label>
</div>
</div>
{/* Logging */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Protokollierung</h3>
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50 mb-4">
<input
type="checkbox"
checked={formData.hasLogging || false}
onChange={(e) => setFormData((prev) => ({ ...prev, hasLogging: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="text-gray-700 font-medium">Audit-Logging aktiviert</span>
<p className="text-sm text-gray-500">Protokollierung aller sicherheitsrelevanten Ereignisse</p>
</div>
</label>
{formData.hasLogging && (
<div className="pl-8">
<label className="block text-sm font-medium text-gray-700 mb-1">
Log-Aufbewahrung (Tage)
</label>
<input
type="number"
min="1"
value={formData.logRetentionDays || 90}
onChange={(e) => setFormData((prev) => ({ ...prev, logRetentionDays: parseInt(e.target.value) || 90 }))}
className="w-32 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
)}
</div>
{/* Backup & DR */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Backup & Disaster Recovery</h3>
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50 mb-4">
<input
type="checkbox"
checked={formData.hasBackup || false}
onChange={(e) => setFormData((prev) => ({ ...prev, hasBackup: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="text-gray-700 font-medium">Regelmäßige Backups</span>
<p className="text-sm text-gray-500">Automatisierte Datensicherung</p>
</div>
</label>
{formData.hasBackup && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pl-8 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Backup-Frequenz
</label>
<select
value={formData.backupFrequency || 'DAILY'}
onChange={(e) => setFormData((prev) => ({ ...prev, backupFrequency: e.target.value as BackupFrequency }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
{BACKUP_FREQUENCIES.map((freq) => (
<option key={freq.value} value={freq.value}>{freq.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Backup-Aufbewahrung (Tage)
</label>
<input
type="number"
min="1"
value={formData.backupRetentionDays || 30}
onChange={(e) => setFormData((prev) => ({ ...prev, backupRetentionDays: parseInt(e.target.value) || 30 }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
)}
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50 mb-4">
<input
type="checkbox"
checked={formData.hasDRPlan || false}
onChange={(e) => setFormData((prev) => ({ ...prev, hasDRPlan: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="text-gray-700 font-medium">Disaster Recovery Plan vorhanden</span>
<p className="text-sm text-gray-500">Dokumentierter Wiederherstellungsplan</p>
</div>
</label>
{formData.hasDRPlan && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pl-8">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
RTO (Recovery Time Objective) in Stunden
</label>
<input
type="number"
min="0"
step="0.5"
value={formData.rtoHours ?? ''}
onChange={(e) => setFormData((prev) => ({ ...prev, rtoHours: e.target.value ? parseFloat(e.target.value) : null }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="z.B. 4"
/>
<p className="text-xs text-gray-500 mt-1">Maximale Ausfallzeit</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
RPO (Recovery Point Objective) in Stunden
</label>
<input
type="number"
min="0"
step="0.25"
value={formData.rpoHours ?? ''}
onChange={(e) => setFormData((prev) => ({ ...prev, rpoHours: e.target.value ? parseFloat(e.target.value) : null }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="z.B. 1"
/>
<p className="text-xs text-gray-500 mt-1">Maximaler Datenverlust</p>
</div>
</div>
)}
</div>
{/* Security Testing & Training */}
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Sicherheitstests & Schulungen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
<input
type="checkbox"
checked={formData.hasVulnerabilityManagement || false}
onChange={(e) => setFormData((prev) => ({ ...prev, hasVulnerabilityManagement: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="text-gray-700 font-medium">Schwachstellenmanagement</span>
<p className="text-sm text-gray-500">Regelmäßige Schwachstellenscans und Patch-Management</p>
</div>
</label>
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
<input
type="checkbox"
checked={formData.hasPenetrationTests || false}
onChange={(e) => setFormData((prev) => ({ ...prev, hasPenetrationTests: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="text-gray-700 font-medium">Penetrationstests</span>
<p className="text-sm text-gray-500">Regelmäßige Sicherheitstests durch externe Prüfer</p>
</div>
</label>
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
<input
type="checkbox"
checked={formData.hasSecurityTraining || false}
onChange={(e) => setFormData((prev) => ({ ...prev, hasSecurityTraining: e.target.checked }))}
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div>
<span className="text-gray-700 font-medium">Security Awareness Training</span>
<p className="text-sm text-gray-500">Regelmäßige Schulungen für alle Mitarbeiter</p>
</div>
</label>
</div>
</div>
</form>
)
}
export default SecurityProfileStep