Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 19s
Nach Wizard-Abschluss wird ein Ergebnis-Panel angezeigt: - Bei UCCA-API-Erfolg: AssessmentResultCard mit Regeln, Kontrollen, Architektur - Bei API-Fehler: Lokale Risikobewertung mit Score, Massnahmen, Regulations - Badge zeigt Quelle (API vs Lokal) - Nutzer kann Ergebnis pruefen bevor "Use Case speichern" geklickt wird Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1522 lines
68 KiB
TypeScript
1522 lines
68 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import Link from 'next/link'
|
|
import { useSDK, UseCaseAssessment, UseCaseIntake } from '@/lib/sdk'
|
|
import { AssessmentResultCard } from '@/components/sdk/use-case-assessment/AssessmentResultCard'
|
|
|
|
// =============================================================================
|
|
// WIZARD STEPS
|
|
// =============================================================================
|
|
|
|
const WIZARD_STEPS = [
|
|
{ id: 1, name: 'Grunddaten', description: 'Name, Beschreibung, Kategorie und Branche' },
|
|
{ id: 2, name: 'Datenkategorien', description: 'Welche Daten werden verarbeitet?' },
|
|
{ id: 3, name: 'Verarbeitungszweck', description: 'Rechtsgrundlage und Zweck' },
|
|
{ id: 4, name: 'Technologie & Modell', description: 'KI-Technologien und Modell-Nutzung' },
|
|
{ id: 5, name: 'Automatisierung', description: 'Grad der Automatisierung' },
|
|
{ id: 6, name: 'Hosting & Transfer', description: 'Hosting und Datentransfer' },
|
|
{ id: 7, name: 'Datenhaltung', description: 'Aufbewahrung und Vertraege' },
|
|
{ id: 8, name: 'Zusammenfassung', description: 'Ueberpruefung und Abschluss' },
|
|
]
|
|
|
|
const TOTAL_STEPS = WIZARD_STEPS.length
|
|
|
|
const DOMAINS = [
|
|
{ value: 'healthcare', label: 'Gesundheit' },
|
|
{ value: 'finance', label: 'Finanzen' },
|
|
{ value: 'education', label: 'Bildung' },
|
|
{ value: 'retail', label: 'Handel' },
|
|
{ value: 'it_services', label: 'IT-Dienstleistungen' },
|
|
{ value: 'consulting', label: 'Beratung' },
|
|
{ value: 'manufacturing', label: 'Produktion' },
|
|
{ value: 'hr', label: 'Personalwesen' },
|
|
{ value: 'marketing', label: 'Marketing' },
|
|
{ value: 'legal', label: 'Recht' },
|
|
{ value: 'public', label: 'Oeffentlicher Sektor' },
|
|
{ value: 'general', label: 'Allgemein' },
|
|
]
|
|
|
|
// =============================================================================
|
|
// RISK CALCULATION (client-side)
|
|
// =============================================================================
|
|
|
|
function calculateRiskScore(form: WizardFormData): { score: number; level: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; dsfaRequired: boolean } {
|
|
let score = 0
|
|
if (form.processesPersonalData) score += 10
|
|
if (form.specialCategories) score += 25
|
|
if (form.biometricData) score += 20
|
|
if (form.healthData) score += 15
|
|
if (form.minorsData) score += 15
|
|
if (form.financialData) score += 5
|
|
if (form.purposeProfiling) score += 15
|
|
if (form.purposeAutomatedDecision) score += 20
|
|
if (form.automation === 'fully_automated') score += 25
|
|
else if (form.automation === 'semi_automated') score += 10
|
|
if (form.internationalTransfer) score += 10
|
|
if (form.modelTraining) score += 10
|
|
if (form.modelFinetune) score += 5
|
|
|
|
let level: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
|
|
if (score >= 60) level = 'CRITICAL'
|
|
else if (score >= 40) level = 'HIGH'
|
|
else if (score >= 20) level = 'MEDIUM'
|
|
else level = 'LOW'
|
|
|
|
const dsfaRequired = score >= 40 || form.specialCategories || (form.purposeAutomatedDecision && form.automation === 'fully_automated')
|
|
|
|
return { score, level, dsfaRequired }
|
|
}
|
|
|
|
// =============================================================================
|
|
// USE CASE CARD
|
|
// =============================================================================
|
|
|
|
function UseCaseCard({
|
|
useCase,
|
|
isActive,
|
|
onSelect,
|
|
onDelete,
|
|
}: {
|
|
useCase: UseCaseAssessment
|
|
isActive: boolean
|
|
onSelect: () => void
|
|
onDelete: () => void
|
|
}) {
|
|
const completionPercent = Math.round((useCase.stepsCompleted / TOTAL_STEPS) * 100)
|
|
|
|
return (
|
|
<div
|
|
className={`relative bg-white rounded-xl border-2 p-6 transition-all cursor-pointer ${
|
|
isActive ? 'border-purple-500 shadow-lg' : 'border-gray-200 hover:border-purple-300'
|
|
}`}
|
|
onClick={onSelect}
|
|
>
|
|
{/* Delete Button */}
|
|
<button
|
|
onClick={e => {
|
|
e.stopPropagation()
|
|
onDelete()
|
|
}}
|
|
className="absolute top-4 right-4 p-1 text-gray-400 hover:text-red-500 transition-colors"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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 className="flex items-start gap-4">
|
|
<div
|
|
className={`w-12 h-12 rounded-xl flex items-center justify-center ${
|
|
completionPercent === 100
|
|
? 'bg-green-100 text-green-600'
|
|
: 'bg-purple-100 text-purple-600'
|
|
}`}
|
|
>
|
|
{completionPercent === 100 ? (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
|
</svg>
|
|
)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-lg font-semibold text-gray-900 truncate">{useCase.name}</h3>
|
|
<p className="text-sm text-gray-500 line-clamp-2">{useCase.description}</p>
|
|
<div className="mt-3">
|
|
<div className="flex items-center justify-between text-sm mb-1">
|
|
<span className="text-gray-500">Fortschritt</span>
|
|
<span className="font-medium">{completionPercent}%</span>
|
|
</div>
|
|
<div className="h-2 bg-gray-100 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full rounded-full transition-all ${
|
|
completionPercent === 100 ? 'bg-green-500' : 'bg-purple-600'
|
|
}`}
|
|
style={{ width: `${completionPercent}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{useCase.assessmentResult && (
|
|
<div className="mt-3 flex items-center gap-2">
|
|
<span
|
|
className={`px-2 py-1 text-xs rounded-full ${
|
|
useCase.assessmentResult.riskLevel === 'CRITICAL'
|
|
? 'bg-red-100 text-red-700'
|
|
: useCase.assessmentResult.riskLevel === 'HIGH'
|
|
? 'bg-orange-100 text-orange-700'
|
|
: useCase.assessmentResult.riskLevel === 'MEDIUM'
|
|
? 'bg-yellow-100 text-yellow-700'
|
|
: 'bg-green-100 text-green-700'
|
|
}`}
|
|
>
|
|
Risiko: {useCase.assessmentResult.riskLevel}
|
|
</span>
|
|
{useCase.assessmentResult.dsfaRequired && (
|
|
<span className="px-2 py-1 text-xs bg-purple-100 text-purple-700 rounded-full">
|
|
DSFA erforderlich
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// WIZARD FORM DATA
|
|
// =============================================================================
|
|
|
|
interface WizardFormData {
|
|
// Step 1: Grunddaten
|
|
name: string
|
|
description: string
|
|
category: string
|
|
domain: string
|
|
// Step 2: Datenkategorien
|
|
dataCategories: string[]
|
|
processesPersonalData: boolean
|
|
specialCategories: boolean
|
|
healthData: boolean
|
|
biometricData: boolean
|
|
minorsData: boolean
|
|
financialData: boolean
|
|
customDataTypes: string[]
|
|
// Step 3: Verarbeitungszweck
|
|
legalBasis: string
|
|
purposeProfiling: boolean
|
|
purposeAutomatedDecision: boolean
|
|
purposeMarketing: boolean
|
|
purposeAnalytics: boolean
|
|
purposeServiceDelivery: boolean
|
|
// Step 4: Technologie & Modell
|
|
aiTechnologies: string[]
|
|
modelInference: boolean
|
|
modelRag: boolean
|
|
modelFinetune: boolean
|
|
modelTraining: boolean
|
|
// Step 5: Automatisierung
|
|
automation: 'assistive' | 'semi_automated' | 'fully_automated'
|
|
// Step 6: Hosting & Transfer
|
|
hostingProvider: string
|
|
hostingRegion: string
|
|
internationalTransfer: boolean
|
|
transferCountries: string[]
|
|
transferMechanism: string
|
|
// Step 7: Datenhaltung & Vertraege
|
|
retentionDays: number
|
|
retentionPurpose: string
|
|
hasDpa: boolean
|
|
hasAiaDocumentation: boolean
|
|
hasRiskAssessment: boolean
|
|
subprocessors: string
|
|
// Notes
|
|
notes: string
|
|
}
|
|
|
|
// =============================================================================
|
|
// WIZARD COMPONENT
|
|
// =============================================================================
|
|
|
|
function UseCaseWizard({
|
|
onComplete,
|
|
onCancel,
|
|
}: {
|
|
onComplete: (useCase: UseCaseAssessment) => void
|
|
onCancel: () => void
|
|
}) {
|
|
const [currentStep, setCurrentStep] = useState(1)
|
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
const [uccaError, setUccaError] = useState<string | null>(null)
|
|
const [uccaResult, setUccaResult] = useState<Record<string, unknown> | null>(null)
|
|
const [completedUseCase, setCompletedUseCase] = useState<UseCaseAssessment | null>(null)
|
|
const [resultSource, setResultSource] = useState<'api' | 'local'>('local')
|
|
const [formData, setFormData] = useState<WizardFormData>({
|
|
name: '',
|
|
description: '',
|
|
category: '',
|
|
domain: 'general',
|
|
dataCategories: [],
|
|
processesPersonalData: false,
|
|
specialCategories: false,
|
|
healthData: false,
|
|
biometricData: false,
|
|
minorsData: false,
|
|
financialData: false,
|
|
customDataTypes: [],
|
|
legalBasis: 'consent',
|
|
purposeProfiling: false,
|
|
purposeAutomatedDecision: false,
|
|
purposeMarketing: false,
|
|
purposeAnalytics: false,
|
|
purposeServiceDelivery: false,
|
|
aiTechnologies: [],
|
|
modelInference: true,
|
|
modelRag: false,
|
|
modelFinetune: false,
|
|
modelTraining: false,
|
|
automation: 'assistive',
|
|
hostingProvider: 'self_hosted',
|
|
hostingRegion: 'eu',
|
|
internationalTransfer: false,
|
|
transferCountries: [],
|
|
transferMechanism: 'none',
|
|
retentionDays: 90,
|
|
retentionPurpose: '',
|
|
hasDpa: false,
|
|
hasAiaDocumentation: false,
|
|
hasRiskAssessment: false,
|
|
subprocessors: '',
|
|
notes: '',
|
|
})
|
|
|
|
const updateFormData = (updates: Partial<WizardFormData>) => {
|
|
setFormData(prev => ({ ...prev, ...updates }))
|
|
}
|
|
|
|
const buildIntake = (): UseCaseIntake => ({
|
|
domain: formData.domain,
|
|
dataCategories: formData.dataCategories,
|
|
processesPersonalData: formData.processesPersonalData,
|
|
specialCategories: formData.specialCategories,
|
|
healthData: formData.healthData,
|
|
biometricData: formData.biometricData,
|
|
minorsData: formData.minorsData,
|
|
financialData: formData.financialData,
|
|
customDataTypes: formData.customDataTypes.filter(s => s.trim()),
|
|
legalBasis: formData.legalBasis,
|
|
purposes: {
|
|
profiling: formData.purposeProfiling,
|
|
automatedDecision: formData.purposeAutomatedDecision,
|
|
marketing: formData.purposeMarketing,
|
|
analytics: formData.purposeAnalytics,
|
|
serviceDelivery: formData.purposeServiceDelivery,
|
|
},
|
|
automation: formData.automation,
|
|
hosting: {
|
|
provider: formData.hostingProvider,
|
|
region: formData.hostingRegion,
|
|
},
|
|
modelUsage: {
|
|
inference: formData.modelInference,
|
|
rag: formData.modelRag,
|
|
finetune: formData.modelFinetune,
|
|
training: formData.modelTraining,
|
|
},
|
|
aiTechnologies: formData.aiTechnologies,
|
|
internationalTransfer: {
|
|
enabled: formData.internationalTransfer,
|
|
countries: formData.transferCountries,
|
|
mechanism: formData.transferMechanism,
|
|
},
|
|
retention: {
|
|
days: formData.retentionDays,
|
|
purpose: formData.retentionPurpose,
|
|
},
|
|
contracts: {
|
|
hasDpa: formData.hasDpa,
|
|
hasAiaDocumentation: formData.hasAiaDocumentation,
|
|
hasRiskAssessment: formData.hasRiskAssessment,
|
|
subprocessors: formData.subprocessors,
|
|
},
|
|
})
|
|
|
|
const handleNext = async () => {
|
|
if (currentStep < TOTAL_STEPS) {
|
|
setCurrentStep(prev => prev + 1)
|
|
return
|
|
}
|
|
|
|
// Final step — create use case + call UCCA API
|
|
setIsSubmitting(true)
|
|
setUccaError(null)
|
|
|
|
const risk = calculateRiskScore(formData)
|
|
const intake = buildIntake()
|
|
|
|
const newUseCase: UseCaseAssessment = {
|
|
id: `uc-${Date.now()}`,
|
|
name: formData.name,
|
|
description: formData.description,
|
|
category: formData.category,
|
|
stepsCompleted: TOTAL_STEPS,
|
|
steps: WIZARD_STEPS.map(s => ({
|
|
id: `step-${s.id}`,
|
|
name: s.name,
|
|
completed: true,
|
|
data: {},
|
|
})),
|
|
assessmentResult: {
|
|
riskLevel: risk.level,
|
|
applicableRegulations: ['DSGVO', 'AI Act'],
|
|
recommendedControls: buildRecommendations(formData, risk),
|
|
dsfaRequired: risk.dsfaRequired,
|
|
aiActClassification: formData.aiTechnologies.length > 0
|
|
? (risk.level === 'CRITICAL' || risk.level === 'HIGH' ? 'HIGH' : 'LIMITED')
|
|
: 'MINIMAL',
|
|
},
|
|
intake,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
}
|
|
|
|
// Call UCCA API in background
|
|
try {
|
|
const uccaIntake = {
|
|
title: formData.name,
|
|
use_case_text: formData.description,
|
|
domain: formData.domain,
|
|
data_types: {
|
|
personal_data: formData.processesPersonalData,
|
|
special_categories: formData.specialCategories,
|
|
health_data: formData.healthData,
|
|
biometric_data: formData.biometricData,
|
|
minors_data: formData.minorsData,
|
|
financial_data: formData.financialData,
|
|
custom_data_types: formData.customDataTypes.filter(s => s.trim()),
|
|
},
|
|
purpose: {
|
|
profiling: formData.purposeProfiling,
|
|
automated_decision: formData.purposeAutomatedDecision,
|
|
marketing: formData.purposeMarketing,
|
|
analytics: formData.purposeAnalytics,
|
|
service_delivery: formData.purposeServiceDelivery,
|
|
},
|
|
automation: formData.automation,
|
|
hosting: { provider: formData.hostingProvider, region: formData.hostingRegion },
|
|
model_usage: {
|
|
inference: formData.modelInference,
|
|
rag: formData.modelRag,
|
|
finetune: formData.modelFinetune,
|
|
training: formData.modelTraining,
|
|
},
|
|
legal_basis: formData.legalBasis,
|
|
international_transfer: {
|
|
enabled: formData.internationalTransfer,
|
|
countries: formData.transferCountries,
|
|
mechanism: formData.transferMechanism,
|
|
},
|
|
retention: { days: formData.retentionDays, purpose: formData.retentionPurpose },
|
|
contracts: {
|
|
has_dpa: formData.hasDpa,
|
|
has_aia_documentation: formData.hasAiaDocumentation,
|
|
has_risk_assessment: formData.hasRiskAssessment,
|
|
subprocessors: formData.subprocessors,
|
|
},
|
|
store_raw_text: true,
|
|
}
|
|
|
|
const resp = await fetch('/api/sdk/v1/ucca/assess', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(uccaIntake),
|
|
})
|
|
|
|
if (resp.ok) {
|
|
const data = await resp.json()
|
|
// Merge UCCA result if available
|
|
if (data.assessment?.id) {
|
|
newUseCase.uccaAssessmentId = data.assessment.id
|
|
}
|
|
if (data.result) {
|
|
newUseCase.assessmentResult = {
|
|
...newUseCase.assessmentResult!,
|
|
riskLevel: data.result.risk_level || newUseCase.assessmentResult!.riskLevel,
|
|
dsfaRequired: data.result.dsfa_required ?? newUseCase.assessmentResult!.dsfaRequired,
|
|
}
|
|
setUccaResult(data.result)
|
|
setResultSource('api')
|
|
}
|
|
} else {
|
|
setUccaError('UCCA-API nicht erreichbar — lokale Risikobewertung wird verwendet.')
|
|
setResultSource('local')
|
|
}
|
|
} catch {
|
|
setUccaError('UCCA-API nicht erreichbar — lokale Risikobewertung wird verwendet.')
|
|
setResultSource('local')
|
|
}
|
|
|
|
setIsSubmitting(false)
|
|
setCompletedUseCase(newUseCase)
|
|
}
|
|
|
|
const handleBack = () => {
|
|
if (currentStep > 1) setCurrentStep(prev => prev - 1)
|
|
}
|
|
|
|
const risk = calculateRiskScore(formData)
|
|
|
|
// ========== RESULT VIEW (after completion) ==========
|
|
if (completedUseCase) {
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-xl font-bold text-gray-900">Assessment-Ergebnis: {completedUseCase.name}</h2>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
{resultSource === 'api'
|
|
? 'Regelbasierte Bewertung durch UCCA Policy Engine'
|
|
: 'Lokale Risikobewertung (UCCA-API nicht verfuegbar)'}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className={`px-3 py-1 text-xs rounded-full font-medium ${
|
|
resultSource === 'api' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'
|
|
}`}>
|
|
{resultSource === 'api' ? 'UCCA API' : 'Lokal'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* UCCA API Result (detailed) */}
|
|
{uccaResult && (
|
|
<AssessmentResultCard result={uccaResult as Parameters<typeof AssessmentResultCard>[0]['result']} />
|
|
)}
|
|
|
|
{/* Local result fallback (when API not available) */}
|
|
{!uccaResult && completedUseCase.assessmentResult && (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
|
|
<div className="flex items-center gap-4">
|
|
<div className={`w-16 h-16 rounded-full flex items-center justify-center text-lg font-bold ${
|
|
completedUseCase.assessmentResult.riskLevel === 'CRITICAL' ? 'bg-red-100 text-red-700' :
|
|
completedUseCase.assessmentResult.riskLevel === 'HIGH' ? 'bg-orange-100 text-orange-700' :
|
|
completedUseCase.assessmentResult.riskLevel === 'MEDIUM' ? 'bg-yellow-100 text-yellow-700' :
|
|
'bg-green-100 text-green-700'
|
|
}`}>
|
|
{risk.score}
|
|
</div>
|
|
<div>
|
|
<div className="text-lg font-semibold text-gray-900">
|
|
Risikostufe: {completedUseCase.assessmentResult.riskLevel}
|
|
</div>
|
|
<div className="text-sm text-gray-500">
|
|
AI Act: {completedUseCase.assessmentResult.aiActClassification}
|
|
</div>
|
|
</div>
|
|
{completedUseCase.assessmentResult.dsfaRequired && (
|
|
<span className="px-3 py-1 text-sm bg-orange-100 text-orange-700 rounded-full ml-auto">
|
|
DSFA empfohlen
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Recommended Controls */}
|
|
{completedUseCase.assessmentResult.recommendedControls.length > 0 && (
|
|
<div>
|
|
<h4 className="text-sm font-medium text-gray-700 mb-2">Empfohlene Massnahmen</h4>
|
|
<div className="space-y-1">
|
|
{completedUseCase.assessmentResult.recommendedControls.map((ctrl, i) => (
|
|
<div key={i} className="flex items-center gap-2 text-sm text-gray-600">
|
|
<svg className="w-4 h-4 text-purple-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
{ctrl}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Applicable Regulations */}
|
|
<div className="flex items-center gap-2">
|
|
{completedUseCase.assessmentResult.applicableRegulations.map(reg => (
|
|
<span key={reg} className="px-2 py-1 text-xs bg-purple-100 text-purple-700 rounded-full">
|
|
{reg}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* UCCA Error info */}
|
|
{uccaError && (
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 text-sm text-blue-700">
|
|
{uccaError}
|
|
</div>
|
|
)}
|
|
|
|
{/* Action buttons */}
|
|
<div className="flex items-center justify-between">
|
|
<button
|
|
onClick={onCancel}
|
|
className="px-4 py-2 text-gray-600 hover:text-gray-900"
|
|
>
|
|
Schliessen
|
|
</button>
|
|
<button
|
|
onClick={() => onComplete(completedUseCase)}
|
|
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 font-medium transition-colors"
|
|
>
|
|
Use Case speichern
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
{/* Header */}
|
|
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-lg font-semibold text-gray-900">Neuer Use Case</h2>
|
|
<button onClick={onCancel} 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 */}
|
|
<div className="mt-4 flex items-center gap-1">
|
|
{WIZARD_STEPS.map((step, index) => (
|
|
<React.Fragment key={step.id}>
|
|
<div
|
|
className={`w-7 h-7 rounded-full flex items-center justify-center text-xs font-medium shrink-0 ${
|
|
step.id < currentStep
|
|
? 'bg-green-500 text-white'
|
|
: step.id === currentStep
|
|
? 'bg-purple-600 text-white'
|
|
: 'bg-gray-200 text-gray-500'
|
|
}`}
|
|
title={step.name}
|
|
>
|
|
{step.id < currentStep ? (
|
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
) : (
|
|
step.id
|
|
)}
|
|
</div>
|
|
{index < WIZARD_STEPS.length - 1 && (
|
|
<div
|
|
className={`flex-1 h-1 rounded ${
|
|
step.id < currentStep ? 'bg-green-500' : 'bg-gray-200'
|
|
}`}
|
|
/>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
<p className="mt-2 text-sm text-gray-500">
|
|
Schritt {currentStep} von {TOTAL_STEPS}: {WIZARD_STEPS[currentStep - 1].description}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6">
|
|
{/* ============================================ */}
|
|
{/* STEP 1: Grunddaten */}
|
|
{/* ============================================ */}
|
|
{currentStep === 1 && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Name des Use Cases *</label>
|
|
<input
|
|
type="text"
|
|
value={formData.name}
|
|
onChange={e => updateFormData({ name: e.target.value })}
|
|
placeholder="z.B. Marketing-KI fuer Kundensegmentierung"
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung *</label>
|
|
<textarea
|
|
value={formData.description}
|
|
onChange={e => updateFormData({ description: e.target.value })}
|
|
placeholder="Beschreiben Sie den Anwendungsfall und den Geschaeftszweck..."
|
|
rows={4}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Kategorie</label>
|
|
<select
|
|
value={formData.category}
|
|
onChange={e => updateFormData({ category: e.target.value })}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
>
|
|
<option value="">Kategorie waehlen...</option>
|
|
<option value="marketing">Marketing & Vertrieb</option>
|
|
<option value="hr">Personal & HR</option>
|
|
<option value="finance">Finanzen & Controlling</option>
|
|
<option value="operations">Betrieb & Produktion</option>
|
|
<option value="customer">Kundenservice</option>
|
|
<option value="other">Sonstiges</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Branche</label>
|
|
<select
|
|
value={formData.domain}
|
|
onChange={e => updateFormData({ domain: e.target.value })}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
>
|
|
{DOMAINS.map(d => (
|
|
<option key={d.value} value={d.value}>{d.label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ============================================ */}
|
|
{/* STEP 2: Datenkategorien */}
|
|
{/* ============================================ */}
|
|
{currentStep === 2 && (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Welche Daten werden verarbeitet?</h3>
|
|
|
|
{/* Personenbezogene Daten Ja/Nein */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Werden personenbezogene Daten verarbeitet?
|
|
</label>
|
|
<div className="flex items-center gap-4">
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="radio"
|
|
checked={formData.processesPersonalData}
|
|
onChange={() => updateFormData({ processesPersonalData: true })}
|
|
className="w-4 h-4 text-purple-600"
|
|
/>
|
|
<span>Ja</span>
|
|
</label>
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="radio"
|
|
checked={!formData.processesPersonalData}
|
|
onChange={() => updateFormData({ processesPersonalData: false })}
|
|
className="w-4 h-4 text-purple-600"
|
|
/>
|
|
<span>Nein</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{formData.processesPersonalData && (
|
|
<>
|
|
{/* Standard-Datenkategorien */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Welche Datenkategorien? (Mehrfachauswahl)
|
|
</label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{['Name/Kontakt', 'E-Mail', 'Adresse', 'Telefon', 'Geburtsdatum', 'Finanzdaten', 'Standort', 'Nutzungsverhalten'].map(
|
|
cat => (
|
|
<label key={cat} className="flex items-center gap-2 p-2 border rounded-lg hover:bg-gray-50">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.dataCategories.includes(cat)}
|
|
onChange={e => {
|
|
if (e.target.checked) {
|
|
updateFormData({ dataCategories: [...formData.dataCategories, cat] })
|
|
} else {
|
|
updateFormData({ dataCategories: formData.dataCategories.filter(c => c !== cat) })
|
|
}
|
|
}}
|
|
className="w-4 h-4 text-purple-600"
|
|
/>
|
|
<span className="text-sm">{cat}</span>
|
|
</label>
|
|
)
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Art. 9 + besondere Kategorien */}
|
|
<div>
|
|
<label className="flex items-center gap-2 mb-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.specialCategories}
|
|
onChange={e => updateFormData({ specialCategories: e.target.checked })}
|
|
className="w-4 h-4 text-purple-600"
|
|
/>
|
|
<span className="text-sm font-medium text-gray-700">
|
|
Besondere Kategorien (Art. 9 DSGVO): Gesundheit, Biometrie, Religion, etc.
|
|
</span>
|
|
</label>
|
|
{formData.specialCategories && (
|
|
<p className="text-sm text-amber-600 bg-amber-50 p-3 rounded-lg">
|
|
Bei besonderen Kategorien ist eine DSFA in der Regel erforderlich!
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Spezifische sensitive Daten */}
|
|
{[
|
|
{ key: 'healthData' as const, label: 'Gesundheitsdaten', desc: 'Diagnosen, Medikation, Fitness' },
|
|
{ key: 'biometricData' as const, label: 'Biometrische Daten', desc: 'Gesichtserkennung, Fingerabdruck, Stimme' },
|
|
{ key: 'minorsData' as const, label: 'Daten von Minderjaehrigen', desc: 'Unter 16 Jahren' },
|
|
{ key: 'financialData' as const, label: 'Finanzdaten', desc: 'Kontodaten, Transaktionen, Kreditwuerdigkeit' },
|
|
].map(item => (
|
|
<label key={item.key} className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData[item.key]}
|
|
onChange={e => updateFormData({ [item.key]: e.target.checked })}
|
|
className="mt-1 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{item.label}</div>
|
|
<div className="text-sm text-gray-500">{item.desc}</div>
|
|
</div>
|
|
</label>
|
|
))}
|
|
|
|
{/* Sonstige Datentypen */}
|
|
<div className="border border-gray-200 rounded-lg p-4 space-y-3">
|
|
<div className="font-medium text-gray-900">Sonstige Datentypen</div>
|
|
<p className="text-sm text-gray-500">
|
|
z.B. Kennzeichen, Fahrzeug-Identifikationsnummer (VIN), Geraete-IDs, IP-Adressen
|
|
</p>
|
|
{formData.customDataTypes.map((dt, idx) => (
|
|
<div key={idx} className="flex items-center gap-2">
|
|
<input
|
|
type="text"
|
|
value={dt}
|
|
onChange={e => {
|
|
const updated = [...formData.customDataTypes]
|
|
updated[idx] = e.target.value
|
|
updateFormData({ customDataTypes: updated })
|
|
}}
|
|
placeholder="Datentyp eingeben..."
|
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
<button
|
|
onClick={() => {
|
|
updateFormData({ customDataTypes: formData.customDataTypes.filter((_, i) => i !== idx) })
|
|
}}
|
|
className="p-2 text-red-500 hover:bg-red-50 rounded-lg"
|
|
title="Entfernen"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
|
|
</button>
|
|
</div>
|
|
))}
|
|
<button
|
|
onClick={() => updateFormData({ customDataTypes: [...formData.customDataTypes, ''] })}
|
|
className="flex items-center gap-1 text-sm text-purple-600 hover:text-purple-700 font-medium"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>
|
|
Weiteren Datentyp hinzufuegen
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* ============================================ */}
|
|
{/* STEP 3: Verarbeitungszweck */}
|
|
{/* ============================================ */}
|
|
{currentStep === 3 && (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Verarbeitungszweck & Rechtsgrundlage</h3>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Rechtsgrundlage (Art. 6 DSGVO)</label>
|
|
<select
|
|
value={formData.legalBasis}
|
|
onChange={e => updateFormData({ legalBasis: e.target.value })}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
>
|
|
<option value="consent">Einwilligung (Art. 6 Abs. 1a)</option>
|
|
<option value="contract">Vertragserfullung (Art. 6 Abs. 1b)</option>
|
|
<option value="legal_obligation">Rechtliche Verpflichtung (Art. 6 Abs. 1c)</option>
|
|
<option value="vital_interest">Lebenswichtige Interessen (Art. 6 Abs. 1d)</option>
|
|
<option value="public_interest">Oeffentliches Interesse (Art. 6 Abs. 1e)</option>
|
|
<option value="legitimate_interest">Berechtigtes Interesse (Art. 6 Abs. 1f)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<h4 className="text-sm font-medium text-gray-700 mt-4">Zweck der Verarbeitung</h4>
|
|
{[
|
|
{ key: 'purposeProfiling' as const, label: 'Profiling', desc: 'Automatisierte Analyse personenbezogener Aspekte' },
|
|
{ key: 'purposeAutomatedDecision' as const, label: 'Automatisierte Entscheidung', desc: 'Art. 22 DSGVO — Entscheidung ohne menschliches Zutun' },
|
|
{ key: 'purposeMarketing' as const, label: 'Marketing', desc: 'Werbung, Personalisierung, Targeting' },
|
|
{ key: 'purposeAnalytics' as const, label: 'Analytics', desc: 'Statistische Auswertung, Business Intelligence' },
|
|
{ key: 'purposeServiceDelivery' as const, label: 'Serviceerbringung', desc: 'Kernfunktion des Produkts/Services' },
|
|
].map(item => (
|
|
<label key={item.key} className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData[item.key]}
|
|
onChange={e => updateFormData({ [item.key]: e.target.checked })}
|
|
className="mt-1 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{item.label}</div>
|
|
<div className="text-sm text-gray-500">{item.desc}</div>
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* ============================================ */}
|
|
{/* STEP 4: Technologie & Modell */}
|
|
{/* ============================================ */}
|
|
{currentStep === 4 && (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Technologie & Modell-Nutzung</h3>
|
|
|
|
{/* KI-Technologien */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Eingesetzte KI-Technologien (Mehrfachauswahl)
|
|
</label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{[
|
|
'Machine Learning',
|
|
'Deep Learning',
|
|
'Natural Language Processing',
|
|
'Computer Vision',
|
|
'Generative AI (LLM)',
|
|
'Empfehlungssysteme',
|
|
'Predictive Analytics',
|
|
'Chatbots/Assistenten',
|
|
].map(tech => (
|
|
<label key={tech} className="flex items-center gap-2 p-2 border rounded-lg hover:bg-gray-50">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.aiTechnologies.includes(tech)}
|
|
onChange={e => {
|
|
if (e.target.checked) {
|
|
updateFormData({ aiTechnologies: [...formData.aiTechnologies, tech] })
|
|
} else {
|
|
updateFormData({ aiTechnologies: formData.aiTechnologies.filter(t => t !== tech) })
|
|
}
|
|
}}
|
|
className="w-4 h-4 text-purple-600"
|
|
/>
|
|
<span className="text-sm">{tech}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modell-Nutzung mit Erklaerungen */}
|
|
<h4 className="text-sm font-medium text-gray-700 mt-4">Wie wird das KI-Modell genutzt?</h4>
|
|
<p className="text-sm text-gray-500">Waehlen Sie alle zutreffenden Optionen.</p>
|
|
|
|
{[
|
|
{
|
|
key: 'modelInference' as const,
|
|
label: 'Inferenz',
|
|
desc: 'Ein fertiges, vortrainiertes Modell wird direkt genutzt — z.B. ChatGPT, Claude, DeepL. Das Modell wird nicht veraendert.',
|
|
example: 'Sie nutzen die OpenAI API, um Texte zusammenzufassen.',
|
|
},
|
|
{
|
|
key: 'modelRag' as const,
|
|
label: 'RAG (Retrieval-Augmented Generation)',
|
|
desc: 'Das Modell erhaelt zusaetzlichen Kontext aus Ihren eigenen Dokumenten. Das Modell selbst wird nicht veraendert.',
|
|
example: 'Ein Chatbot durchsucht Ihre Firmen-FAQ und beantwortet Fragen basierend auf den gefundenen Dokumenten.',
|
|
},
|
|
{
|
|
key: 'modelFinetune' as const,
|
|
label: 'Fine-Tuning',
|
|
desc: 'Ein bestehendes Modell wird mit Ihren eigenen Daten nachtrainiert. Die Originaldaten fliessen ins Modell ein.',
|
|
example: 'Sie trainieren GPT-3.5 mit 1.000 Ihrer Support-Tickets, damit es Ihren Kommunikationsstil uebernimmt.',
|
|
},
|
|
{
|
|
key: 'modelTraining' as const,
|
|
label: 'Training (eigenes Modell)',
|
|
desc: 'Sie trainieren ein komplett eigenes KI-Modell von Grund auf. Hoechster Kontrollgrad, aber auch hoechstes Datenrisiko.',
|
|
example: 'Sie trainieren ein eigenes Bilderkennungsmodell fuer Qualitaetskontrolle in der Produktion.',
|
|
},
|
|
].map(item => (
|
|
<div key={item.key} className="bg-gray-50 rounded-lg border border-gray-200 overflow-hidden">
|
|
<label className="flex items-start gap-3 p-3 cursor-pointer hover:bg-gray-100">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData[item.key]}
|
|
onChange={e => updateFormData({ [item.key]: e.target.checked })}
|
|
className="mt-1 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{item.label}</div>
|
|
<div className="text-sm text-gray-500">{item.desc}</div>
|
|
<div className="text-xs text-purple-600 mt-1 bg-purple-50 inline-block px-2 py-0.5 rounded">
|
|
Beispiel: {item.example}
|
|
</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
))}
|
|
|
|
{/* Glossar */}
|
|
<details className="bg-amber-50 border border-amber-200 rounded-lg overflow-hidden">
|
|
<summary className="px-4 py-3 text-sm font-medium text-amber-800 cursor-pointer hover:bg-amber-100">
|
|
Glossar: ML, DL, NLP, LLM, RAG, Fine-Tuning — Was bedeutet das?
|
|
</summary>
|
|
<div className="px-4 pb-4 space-y-3 text-sm text-amber-900">
|
|
<div>
|
|
<span className="font-semibold">ML (Machine Learning)</span> —
|
|
Computer lernt Muster aus Daten, statt explizit programmiert zu werden.
|
|
Beispiel: Spam-Filter, der aus markierten E-Mails lernt.
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold">DL (Deep Learning)</span> —
|
|
Spezielle Form von ML mit kuenstlichen neuronalen Netzen (viele Schichten).
|
|
Beispiel: Bilderkennung, Spracherkennung, Textgenerierung.
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold">NLP (Natural Language Processing)</span> —
|
|
KI, die menschliche Sprache versteht und verarbeitet.
|
|
Beispiel: ChatGPT, DeepL, Sentiment-Analyse von Kundenbewertungen.
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold">LLM (Large Language Model)</span> —
|
|
Sehr grosses Sprachmodell, trainiert auf riesigen Textmengen.
|
|
Beispiel: GPT-4, Claude, Llama, Gemini.
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold">RAG (Retrieval-Augmented Generation)</span> —
|
|
Das LLM erhaelt vor der Antwort relevante Dokumente aus einer Datenbank als Kontext.
|
|
Vorteil: Aktuelle und firmenspezifische Antworten, ohne das Modell neu zu trainieren.
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold">Fine-Tuning</span> —
|
|
Ein bestehendes Modell wird mit eigenen Daten weiter trainiert, um es zu spezialisieren.
|
|
Achtung: Ihre Trainingsdaten werden Teil des Modells.
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
)}
|
|
|
|
{/* ============================================ */}
|
|
{/* STEP 5: Automatisierung */}
|
|
{/* ============================================ */}
|
|
{currentStep === 5 && (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Grad der Automatisierung</h3>
|
|
<p className="text-sm text-gray-600">
|
|
Wie stark greift die KI in Entscheidungen ein? Je hoeher der Automatisierungsgrad, desto strenger die regulatorischen Anforderungen.
|
|
</p>
|
|
|
|
{[
|
|
{
|
|
value: 'assistive' as const,
|
|
label: 'Assistiv (Mensch entscheidet)',
|
|
desc: 'Die KI liefert Informationen oder Vorschlaege, aber ein Mensch trifft immer die finale Entscheidung.',
|
|
examples: 'Rechtschreibkorrektur, Suchvorschlaege, Zusammenfassungen, Uebersetzungshilfe',
|
|
},
|
|
{
|
|
value: 'semi_automated' as const,
|
|
label: 'Teilautomatisiert (Mensch prueft)',
|
|
desc: 'Die KI erstellt Ergebnisse oder Entwuerfe, die ein Mensch vor der Ausfuehrung prueft und bestaetigt.',
|
|
examples: 'E-Mail-Entwuerfe mit manueller Freigabe, vorgeschlagene Diagnosen mit Arztbestaetigung, KI-generierte Vertraege mit juristischer Pruefung',
|
|
},
|
|
{
|
|
value: 'fully_automated' as const,
|
|
label: 'Vollautomatisiert (KI entscheidet)',
|
|
desc: 'Die KI trifft Entscheidungen eigenstaendig. Ein Mensch ueberwacht nur stichprobenartig oder bei Ausnahmen.',
|
|
examples: 'Automatische Kreditentscheidungen, automatisierte Bewerbungs-Vorauswahl, autonome Chatbot-Antworten ohne Pruefung',
|
|
},
|
|
].map(item => (
|
|
<label
|
|
key={item.value}
|
|
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-all ${
|
|
formData.automation === item.value
|
|
? 'border-purple-500 bg-purple-50'
|
|
: 'border-gray-200 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="automation"
|
|
value={item.value}
|
|
checked={formData.automation === item.value}
|
|
onChange={e => updateFormData({ automation: e.target.value as typeof formData.automation })}
|
|
className="mt-1 text-purple-600 focus:ring-purple-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{item.label}</div>
|
|
<div className="text-sm text-gray-500">{item.desc}</div>
|
|
<div className="text-xs text-gray-400 mt-1">Beispiele: {item.examples}</div>
|
|
</div>
|
|
</label>
|
|
))}
|
|
|
|
{/* Art. 22 DSGVO Info-Box */}
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 text-sm text-blue-800">
|
|
<div className="font-medium mb-1">Art. 22 DSGVO — Automatisierte Einzelentscheidungen</div>
|
|
<p>
|
|
Vollautomatisierte Systeme, die Personen erheblich beeinflussen (z.B. Kreditvergabe, Bewerbungsauswahl),
|
|
unterliegen strengen Auflagen: Informationspflicht, Recht auf menschliche Ueberpruefung und Anfechtungsmoeglichkeit.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ============================================ */}
|
|
{/* STEP 6: Hosting & Transfer */}
|
|
{/* ============================================ */}
|
|
{currentStep === 6 && (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Hosting & Datentransfer</h3>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Hosting-Provider</label>
|
|
<select
|
|
value={formData.hostingProvider}
|
|
onChange={e => updateFormData({ hostingProvider: e.target.value })}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
>
|
|
<option value="self_hosted">Eigenes Hosting</option>
|
|
<option value="aws">AWS</option>
|
|
<option value="azure">Microsoft Azure</option>
|
|
<option value="gcp">Google Cloud</option>
|
|
<option value="hetzner">Hetzner (DE)</option>
|
|
<option value="other">Anderer Anbieter</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Region</label>
|
|
<select
|
|
value={formData.hostingRegion}
|
|
onChange={e => updateFormData({ hostingRegion: e.target.value })}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
>
|
|
<option value="eu">EU</option>
|
|
<option value="de">Deutschland</option>
|
|
<option value="us">USA</option>
|
|
<option value="other">Andere</option>
|
|
</select>
|
|
</div>
|
|
|
|
<hr className="my-4" />
|
|
|
|
<h4 className="text-sm font-medium text-gray-700">Internationaler Datentransfer</h4>
|
|
<label className="flex items-start gap-3 p-4 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.internationalTransfer}
|
|
onChange={e => updateFormData({ internationalTransfer: e.target.checked })}
|
|
className="mt-1 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900">Daten werden in Drittlaender uebermittelt</div>
|
|
<div className="text-sm text-gray-500">Ausserhalb des EWR (z.B. USA, UK, Schweiz)</div>
|
|
</div>
|
|
</label>
|
|
|
|
{formData.internationalTransfer && (
|
|
<>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Ziellaender</label>
|
|
<input
|
|
type="text"
|
|
value={formData.transferCountries.join(', ')}
|
|
onChange={e => updateFormData({ transferCountries: e.target.value.split(',').map(s => s.trim()).filter(Boolean) })}
|
|
placeholder="z.B. USA, UK, CH"
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">Kommagetrennte Laenderkuerzel</p>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Transfer-Mechanismus</label>
|
|
<select
|
|
value={formData.transferMechanism}
|
|
onChange={e => updateFormData({ transferMechanism: e.target.value })}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
>
|
|
<option value="none">Noch nicht festgelegt</option>
|
|
<option value="adequacy">Angemessenheitsbeschluss</option>
|
|
<option value="scc">Standardvertragsklauseln (SCC)</option>
|
|
<option value="bcr">Binding Corporate Rules (BCR)</option>
|
|
<option value="derogation">Ausnahmeregelung (Art. 49 DSGVO)</option>
|
|
</select>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* ============================================ */}
|
|
{/* STEP 7: Datenhaltung & Vertraege */}
|
|
{/* ============================================ */}
|
|
{currentStep === 7 && (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Datenhaltung & Vertraege</h3>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Aufbewahrungsdauer (Tage)</label>
|
|
<input
|
|
type="number"
|
|
min={0}
|
|
value={formData.retentionDays}
|
|
onChange={e => updateFormData({ retentionDays: parseInt(e.target.value) || 0 })}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Zweck der Aufbewahrung</label>
|
|
<textarea
|
|
value={formData.retentionPurpose}
|
|
onChange={e => updateFormData({ retentionPurpose: e.target.value })}
|
|
rows={3}
|
|
placeholder="z.B. Vertragliche Pflichten, gesetzliche Aufbewahrungsfristen..."
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<hr className="my-4" />
|
|
|
|
<h4 className="text-sm font-medium text-gray-700">Compliance-Dokumentation</h4>
|
|
{[
|
|
{ key: 'hasDpa' as const, label: 'Auftragsverarbeitungsvertrag (AVV/DPA)', desc: 'Vertrag mit KI-Anbieter / Subprozessor nach Art. 28 DSGVO' },
|
|
{ key: 'hasAiaDocumentation' as const, label: 'AI Act Dokumentation', desc: 'Risikoklassifizierung und technische Dokumentation nach EU AI Act' },
|
|
{ key: 'hasRiskAssessment' as const, label: 'Risikobewertung / DSFA', desc: 'Datenschutz-Folgenabschaetzung nach Art. 35 DSGVO' },
|
|
].map(item => (
|
|
<label key={item.key} className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData[item.key]}
|
|
onChange={e => updateFormData({ [item.key]: e.target.checked })}
|
|
className="mt-1 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{item.label}</div>
|
|
<div className="text-sm text-gray-500">{item.desc}</div>
|
|
</div>
|
|
</label>
|
|
))}
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Subprozessoren</label>
|
|
<textarea
|
|
value={formData.subprocessors}
|
|
onChange={e => updateFormData({ subprocessors: e.target.value })}
|
|
rows={3}
|
|
placeholder="z.B. OpenAI (USA, SCC), Hetzner Cloud (DE)..."
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ============================================ */}
|
|
{/* STEP 8: Zusammenfassung */}
|
|
{/* ============================================ */}
|
|
{currentStep === 8 && (
|
|
<div className="space-y-6">
|
|
<h3 className="text-lg font-semibold text-gray-900">Zusammenfassung</h3>
|
|
|
|
{/* Risiko-Vorschau */}
|
|
<div className={`rounded-lg p-4 border-2 ${
|
|
risk.level === 'CRITICAL' ? 'bg-red-50 border-red-300' :
|
|
risk.level === 'HIGH' ? 'bg-orange-50 border-orange-300' :
|
|
risk.level === 'MEDIUM' ? 'bg-yellow-50 border-yellow-300' :
|
|
'bg-green-50 border-green-300'
|
|
}`}>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="text-sm font-medium text-gray-700">Automatische Risikobewertung</div>
|
|
<div className="text-2xl font-bold mt-1">
|
|
<span className={
|
|
risk.level === 'CRITICAL' ? 'text-red-700' :
|
|
risk.level === 'HIGH' ? 'text-orange-700' :
|
|
risk.level === 'MEDIUM' ? 'text-yellow-700' :
|
|
'text-green-700'
|
|
}>
|
|
{risk.level}
|
|
</span>
|
|
<span className="text-sm font-normal text-gray-500 ml-2">(Score: {risk.score})</span>
|
|
</div>
|
|
</div>
|
|
{risk.dsfaRequired && (
|
|
<span className="px-3 py-1 text-sm bg-purple-100 text-purple-700 rounded-full font-medium">
|
|
DSFA empfohlen
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Zusammenfassung der Eingaben */}
|
|
<div className="bg-gray-50 rounded-lg p-6">
|
|
<dl className="space-y-3">
|
|
<SummaryRow label="Name" value={formData.name || '-'} />
|
|
<SummaryRow label="Kategorie" value={formData.category || '-'} />
|
|
<SummaryRow label="Branche" value={DOMAINS.find(d => d.value === formData.domain)?.label || formData.domain} />
|
|
<SummaryRow label="Personenbezogene Daten" value={formData.processesPersonalData ? 'Ja' : 'Nein'} />
|
|
{formData.processesPersonalData && (
|
|
<>
|
|
<SummaryRow label="Datenkategorien" value={formData.dataCategories.join(', ') || '-'} />
|
|
<SummaryRow label="Art. 9 Kategorien" value={formData.specialCategories ? 'Ja' : 'Nein'} />
|
|
{formData.healthData && <SummaryRow label="Gesundheitsdaten" value="Ja" />}
|
|
{formData.biometricData && <SummaryRow label="Biometrische Daten" value="Ja" />}
|
|
{formData.minorsData && <SummaryRow label="Daten Minderjaehriger" value="Ja" />}
|
|
{formData.financialData && <SummaryRow label="Finanzdaten" value="Ja" />}
|
|
{formData.customDataTypes.filter(s => s.trim()).length > 0 && (
|
|
<SummaryRow label="Sonstige Datentypen" value={formData.customDataTypes.filter(s => s.trim()).join(', ')} />
|
|
)}
|
|
</>
|
|
)}
|
|
<SummaryRow label="Rechtsgrundlage" value={formatLegalBasis(formData.legalBasis)} />
|
|
<SummaryRow
|
|
label="Verarbeitungszwecke"
|
|
value={formatPurposes(formData)}
|
|
/>
|
|
<SummaryRow label="KI-Technologien" value={formData.aiTechnologies.join(', ') || '-'} />
|
|
<SummaryRow
|
|
label="Modell-Nutzung"
|
|
value={formatModelUsage(formData)}
|
|
/>
|
|
<SummaryRow
|
|
label="Automatisierung"
|
|
value={
|
|
formData.automation === 'assistive' ? 'Assistiv' :
|
|
formData.automation === 'semi_automated' ? 'Teilautomatisiert' :
|
|
'Vollautomatisiert'
|
|
}
|
|
/>
|
|
<SummaryRow label="Hosting" value={`${formData.hostingProvider} (${formData.hostingRegion})`} />
|
|
<SummaryRow label="Datentransfer" value={formData.internationalTransfer ? `Ja (${formData.transferCountries.join(', ') || 'k.A.'})` : 'Nein'} />
|
|
<SummaryRow label="Aufbewahrung" value={`${formData.retentionDays} Tage`} />
|
|
<SummaryRow label="AVV/DPA" value={formData.hasDpa ? 'Vorhanden' : 'Nicht vorhanden'} />
|
|
<SummaryRow label="AI Act Doku" value={formData.hasAiaDocumentation ? 'Vorhanden' : 'Nicht vorhanden'} />
|
|
<SummaryRow label="DSFA" value={formData.hasRiskAssessment ? 'Vorhanden' : 'Nicht vorhanden'} />
|
|
</dl>
|
|
</div>
|
|
|
|
{/* DSFA Warnung */}
|
|
{risk.dsfaRequired && (
|
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
|
|
<div className="flex items-start gap-3">
|
|
<svg className="w-5 h-5 text-amber-600 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
<div>
|
|
<h4 className="font-medium text-amber-800">Datenschutz-Folgenabschaetzung empfohlen</h4>
|
|
<p className="text-sm text-amber-700 mt-1">
|
|
Basierend auf Ihren Angaben wird eine DSFA nach Art. 35 DSGVO empfohlen.
|
|
Sie koennen diese im DSFA-Modul erstellen, nachdem der Use Case angelegt wurde.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* UCCA Error */}
|
|
{uccaError && (
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 text-sm text-blue-700">
|
|
{uccaError}
|
|
</div>
|
|
)}
|
|
|
|
{/* Notizen */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Notizen (optional)</label>
|
|
<textarea
|
|
value={formData.notes}
|
|
onChange={e => updateFormData({ notes: e.target.value })}
|
|
placeholder="Zusaetzliche Anmerkungen..."
|
|
rows={3}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50 flex items-center justify-between">
|
|
<button
|
|
onClick={currentStep === 1 ? onCancel : handleBack}
|
|
className="px-4 py-2 text-gray-600 hover:text-gray-900"
|
|
>
|
|
{currentStep === 1 ? 'Abbrechen' : 'Zurueck'}
|
|
</button>
|
|
<button
|
|
onClick={handleNext}
|
|
disabled={(currentStep === 1 && !formData.name) || isSubmitting}
|
|
className={`px-6 py-2 rounded-lg font-medium transition-colors flex items-center gap-2 ${
|
|
(currentStep === 1 && !formData.name) || isSubmitting
|
|
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
|
: currentStep === TOTAL_STEPS
|
|
? 'bg-green-600 text-white hover:bg-green-700'
|
|
: 'bg-purple-600 text-white hover:bg-purple-700'
|
|
}`}
|
|
>
|
|
{isSubmitting ? (
|
|
<>
|
|
<svg className="w-5 h-5 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 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
</svg>
|
|
Bewerte...
|
|
</>
|
|
) : currentStep === TOTAL_STEPS ? (
|
|
'Abschliessen'
|
|
) : (
|
|
'Weiter'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// HELPER COMPONENTS & FUNCTIONS
|
|
// =============================================================================
|
|
|
|
function SummaryRow({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div className="flex justify-between gap-4">
|
|
<dt className="text-gray-500 shrink-0">{label}:</dt>
|
|
<dd className="font-medium text-gray-900 text-right">{value}</dd>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function formatLegalBasis(basis: string): string {
|
|
const map: Record<string, string> = {
|
|
consent: 'Einwilligung (Art. 6.1a)',
|
|
contract: 'Vertragserfullung (Art. 6.1b)',
|
|
legal_obligation: 'Rechtliche Verpflichtung (Art. 6.1c)',
|
|
vital_interest: 'Lebenswichtige Interessen (Art. 6.1d)',
|
|
public_interest: 'Oeffentliches Interesse (Art. 6.1e)',
|
|
legitimate_interest: 'Berechtigtes Interesse (Art. 6.1f)',
|
|
}
|
|
return map[basis] || basis
|
|
}
|
|
|
|
function formatPurposes(form: WizardFormData): string {
|
|
const purposes: string[] = []
|
|
if (form.purposeProfiling) purposes.push('Profiling')
|
|
if (form.purposeAutomatedDecision) purposes.push('Autom. Entscheidung')
|
|
if (form.purposeMarketing) purposes.push('Marketing')
|
|
if (form.purposeAnalytics) purposes.push('Analytics')
|
|
if (form.purposeServiceDelivery) purposes.push('Serviceerbringung')
|
|
return purposes.join(', ') || '-'
|
|
}
|
|
|
|
function formatModelUsage(form: WizardFormData): string {
|
|
const usage: string[] = []
|
|
if (form.modelInference) usage.push('Inferenz')
|
|
if (form.modelRag) usage.push('RAG')
|
|
if (form.modelFinetune) usage.push('Fine-Tuning')
|
|
if (form.modelTraining) usage.push('Training')
|
|
return usage.join(', ') || '-'
|
|
}
|
|
|
|
function buildRecommendations(form: WizardFormData, risk: { dsfaRequired: boolean }): string[] {
|
|
const recs: string[] = []
|
|
if (risk.dsfaRequired) recs.push('Datenschutz-Folgenabschaetzung (DSFA)')
|
|
if (form.processesPersonalData) recs.push('Technische und organisatorische Massnahmen (TOM)')
|
|
if (form.internationalTransfer) recs.push('Datentransfer-Vereinbarung pruefen')
|
|
if (form.purposeAutomatedDecision) recs.push('Human-in-the-Loop Prozess einrichten')
|
|
if (form.modelTraining || form.modelFinetune) recs.push('Trainingsdaten-Dokumentation')
|
|
if (!form.hasDpa && form.hostingProvider !== 'self_hosted') recs.push('Auftragsverarbeitungsvertrag (AVV) abschliessen')
|
|
if (form.specialCategories) recs.push('Explizite Einwilligung fuer Art. 9 Daten')
|
|
if (form.minorsData) recs.push('Einwilligung der Erziehungsberechtigten')
|
|
if (recs.length === 0) recs.push('Standard-Compliance-Massnahmen')
|
|
return recs
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN PAGE
|
|
// =============================================================================
|
|
|
|
export default function AdvisoryBoardPage() {
|
|
const { state, dispatch } = useSDK()
|
|
const [showWizard, setShowWizard] = useState(false)
|
|
|
|
const handleCreateUseCase = (useCase: UseCaseAssessment) => {
|
|
dispatch({ type: 'ADD_USE_CASE', payload: useCase })
|
|
dispatch({ type: 'SET_ACTIVE_USE_CASE', payload: useCase.id })
|
|
setShowWizard(false)
|
|
}
|
|
|
|
const handleDeleteUseCase = (id: string) => {
|
|
if (confirm('Moechten Sie diesen Use Case wirklich loeschen?')) {
|
|
dispatch({ type: 'DELETE_USE_CASE', payload: id })
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Use Case Workshop</h1>
|
|
<p className="mt-1 text-gray-500">
|
|
Erfassen Sie Ihre KI-Anwendungsfaelle und erhalten Sie eine erste Compliance-Bewertung
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Link
|
|
href="/sdk/advisory-board/documentation"
|
|
className="inline-flex items-center gap-2 px-4 py-2 text-sm text-purple-600 hover:text-purple-700 hover:bg-purple-50 border border-purple-300 rounded-lg transition-colors"
|
|
>
|
|
UCCA-System Dokumentation ansehen
|
|
</Link>
|
|
{!showWizard && (
|
|
<button
|
|
onClick={() => setShowWizard(true)}
|
|
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
Neuer Use Case
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Wizard or List */}
|
|
{showWizard ? (
|
|
<UseCaseWizard onComplete={handleCreateUseCase} onCancel={() => setShowWizard(false)} />
|
|
) : state.useCases.length === 0 ? (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
|
<div className="w-16 h-16 mx-auto bg-purple-100 rounded-full flex items-center justify-center mb-4">
|
|
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-gray-900">Noch keine Use Cases</h3>
|
|
<p className="mt-2 text-gray-500 max-w-md mx-auto">
|
|
Erstellen Sie Ihren ersten Use Case, um mit dem Compliance Assessment zu beginnen.
|
|
</p>
|
|
<button
|
|
onClick={() => setShowWizard(true)}
|
|
className="mt-6 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
|
>
|
|
Ersten Use Case erstellen
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{state.useCases.map(useCase => (
|
|
<UseCaseCard
|
|
key={useCase.id}
|
|
useCase={useCase}
|
|
isActive={state.activeUseCase === useCase.id}
|
|
onSelect={() => dispatch({ type: 'SET_ACTIVE_USE_CASE', payload: useCase.id })}
|
|
onDelete={() => handleDeleteUseCase(useCase.id)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|