feat(sdk): Step 6 Wizard rebuilt — Verarbeitungstätigkeiten statt IT-Systeme
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 33s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 28s
CI / test-python-dsms-gateway (push) Successful in 21s

- Verarbeitungstätigkeiten-Checkliste nach Branche (Art. 30 DSGVO)
- DSGVO-Standard Datenkategorien als Checkboxen (10 + 5 Art. 9)
- Rechtsgrundlage pro Verarbeitungstätigkeit (Art. 6 Abs. 1)
- KI-Systeme vereinfacht: risk_category und human_oversight entfernt (Tool-Output)
- Full-width Layout, Mitarbeiterzahl-Dropdown entfernt, Adressfelder ergänzt
- Konzern-Hinweis bei Jahresumsatz, Regulierungen aus Zielmarkt entfernt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-08 23:31:19 +01:00
parent 53ff0722a4
commit e96f623af0
2 changed files with 543 additions and 157 deletions

View File

@@ -34,7 +34,7 @@ const BASE_WIZARD_STEPS = [
{ id: 3, name: 'Firmengroesse', description: 'Mitarbeiter und Umsatz' },
{ id: 4, name: 'Standorte', description: 'Hauptsitz und Zielmaerkte' },
{ id: 5, name: 'Datenschutz', description: 'Rollen und KI-Nutzung' },
{ id: 6, name: 'Systeme & KI', description: 'IT-Systeme und KI-Katalog' },
{ id: 6, name: 'Verarbeitung & KI', description: 'Verarbeitungstätigkeiten und KI-Systeme' },
{ id: 7, name: 'Rechtlicher Rahmen', description: 'Regulierungen und Prüfzyklen' },
]
@@ -200,7 +200,7 @@ const STEP_EXPLANATIONS: Record<number, string> = {
3: 'Die Unternehmensgröße bestimmt, ob Sie einen DSB benennen müssen (ab 20 MA), ob NIS2-Pflichten greifen und welche Audit-Anforderungen gelten.',
4: 'Standorte und Zielmärkte bestimmen, welche nationalen Datenschutzgesetze zusätzlich zur DSGVO greifen (z.B. BDSG, DSG-AT, UK GDPR, CCPA).',
5: 'Ob Sie Verantwortlicher oder Auftragsverarbeiter sind, bestimmt Ihre DSGVO-Pflichten grundlegend. KI-Nutzung löst zusätzliche AI-Act-Pflichten aus.',
6: 'Ihre IT-Systeme und KI-Anwendungen werden für das Verarbeitungsverzeichnis (VVT), die technisch-organisatorischen Maßnahmen (TOM) und die KI-Risikobewertung benötigt.',
6: 'Ihre Verarbeitungstätigkeiten bilden die Grundlage für das Verarbeitungsverzeichnis (Art. 30 DSGVO). KI-Systeme werden für die AI-Act-Analyse erfasst — die Risikoeinstufung übernimmt unser Tool.',
7: 'Regulierungsrahmen und Prüfzyklen definieren, welche Compliance-Module für Sie aktiviert werden und in welchem Rhythmus Audits stattfinden.',
8: 'Als Maschinenbauer gelten zusätzliche Anforderungen: CE-Kennzeichnung, Maschinenverordnung, Produktsicherheit und ggf. Hochrisiko-KI im Sinne des AI Act.',
}
@@ -342,37 +342,25 @@ function StepCompanySize({
</div>
</div>
<div className="grid grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Mitarbeiterzahl</label>
<select
value={data.employeeCount || ''}
onChange={e => onChange({ employeeCount: e.target.value })}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="">Bitte wählen...</option>
<option value="1-9">1-9 Mitarbeiter</option>
<option value="10-49">10-49 Mitarbeiter</option>
<option value="50-249">50-249 Mitarbeiter</option>
<option value="250-999">250-999 Mitarbeiter</option>
<option value="1000+">1.000+ Mitarbeiter</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Jahresumsatz</label>
<select
value={data.annualRevenue || ''}
onChange={e => onChange({ annualRevenue: e.target.value })}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="">Bitte wählen...</option>
<option value="< 2 Mio">&lt; 2 Mio. Euro</option>
<option value="2-10 Mio">2-10 Mio. Euro</option>
<option value="10-50 Mio">10-50 Mio. Euro</option>
<option value="> 50 Mio">&gt; 50 Mio. Euro</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Jahresumsatz</label>
<select
value={data.annualRevenue || ''}
onChange={e => onChange({ annualRevenue: e.target.value })}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="">Bitte wählen...</option>
<option value="< 2 Mio">&lt; 2 Mio. Euro</option>
<option value="2-10 Mio">2-10 Mio. Euro</option>
<option value="10-50 Mio">10-50 Mio. Euro</option>
<option value="> 50 Mio">&gt; 50 Mio. Euro</option>
</select>
{(data.companySize === 'medium' || data.companySize === 'large' || data.companySize === 'enterprise') && (
<p className="text-xs text-amber-600 mt-2">
Geben Sie den konsolidierten Konzernumsatz an, wenn der Compliance-Check für Mutter- und Tochtergesellschaften gelten soll.
Für eine einzelne Einheit eines Konzerns geben Sie nur deren Umsatz an.
</p>
)}
</div>
</div>
)
@@ -394,45 +382,104 @@ function StepLocations({
}
}
const stateLabel = data.headquartersCountry === 'CH' ? 'Kanton' :
data.headquartersCountry === 'AT' ? 'Bundesland' :
data.headquartersCountry === 'DE' ? 'Bundesland' : 'Region / Provinz'
return (
<div className="space-y-8">
<div className="grid grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Land des Hauptsitzes <span className="text-red-500">*</span>
</label>
<select
value={data.headquartersCountry || ''}
onChange={e => onChange({ headquartersCountry: e.target.value })}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="">Bitte wählen...</option>
<option value="DE">Deutschland</option>
<option value="AT">Österreich</option>
<option value="CH">Schweiz</option>
<option value="other">Anderes Land</option>
</select>
</div>
{/* Country */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Land des Hauptsitzes <span className="text-red-500">*</span>
</label>
<select
value={data.headquartersCountry || ''}
onChange={e => onChange({ headquartersCountry: e.target.value, headquartersCountryOther: '' })}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="">Bitte wählen...</option>
<option value="DE">Deutschland</option>
<option value="AT">Österreich</option>
<option value="CH">Schweiz</option>
<option value="LI">Liechtenstein</option>
<option value="LU">Luxemburg</option>
<option value="NL">Niederlande</option>
<option value="FR">Frankreich</option>
<option value="IT">Italien</option>
<option value="other">Anderes Land</option>
</select>
</div>
{/* Other country free text */}
{data.headquartersCountry === 'other' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Land (Freitext)</label>
<input
type="text"
value={data.headquartersCountryOther || ''}
onChange={e => onChange({ headquartersCountryOther: e.target.value })}
placeholder="z.B. Vereinigtes Königreich"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
)}
{/* Street + House Number */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Straße und Hausnummer</label>
<input
type="text"
value={data.headquartersStreet || ''}
onChange={e => onChange({ headquartersStreet: e.target.value })}
placeholder="Musterstraße 42"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
{/* PLZ + City */}
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">PLZ</label>
<input
type="text"
value={data.headquartersZip || ''}
onChange={e => onChange({ headquartersZip: e.target.value })}
placeholder="10115"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">Stadt</label>
<input
type="text"
value={data.headquartersCity || ''}
onChange={e => onChange({ headquartersCity: e.target.value })}
placeholder="z.B. Berlin"
placeholder="Berlin"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
</div>
{/* State / Bundesland / Kanton */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">{stateLabel}</label>
<input
type="text"
value={data.headquartersState || ''}
onChange={e => onChange({ headquartersState: e.target.value })}
placeholder={data.headquartersCountry === 'CH' ? 'z.B. Zürich' : data.headquartersCountry === 'AT' ? 'z.B. Wien' : 'z.B. Bayern'}
className="w-full px-4 py-3 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-4">
Zielmärkte <span className="text-red-500">*</span>
<span className="text-gray-400 font-normal ml-2">Wo verkaufen/operieren Sie?</span>
</label>
<div className="space-y-3">
{Object.entries(TARGET_MARKET_LABELS).map(([value, { label, description, regulations }]) => (
{Object.entries(TARGET_MARKET_LABELS).map(([value, { label, description }]) => (
<button
key={value}
type="button"
@@ -443,16 +490,8 @@ function StepLocations({
: 'border-gray-200 hover:border-purple-300'
}`}
>
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-gray-900">{label}</div>
<div className="text-sm text-gray-500">{description}</div>
</div>
<div className="text-right">
<div className="text-xs text-gray-400">Relevante Regulierungen:</div>
<div className="text-xs text-purple-600">{regulations.join(', ')}</div>
</div>
</div>
<div className="font-medium text-gray-900">{label}</div>
<div className="text-sm text-gray-500">{description}</div>
</button>
))}
</div>
@@ -553,48 +592,189 @@ function StepDataProtection({
}
// =============================================================================
// STEP 6: SYSTEME & KI
// STEP 6: VERARBEITUNGSTAETIGKEITEN & KI
// =============================================================================
interface ProcessingSystem {
// DSGVO-Standard Datenkategorien
const DATA_CATEGORIES = [
{ id: 'stammdaten', label: 'Stammdaten', desc: 'Name, Geburtsdatum, Geschlecht' },
{ id: 'kontaktdaten', label: 'Kontaktdaten', desc: 'E-Mail, Telefon, Adresse' },
{ id: 'vertragsdaten', label: 'Vertragsdaten', desc: 'Vertragsnummer, Laufzeit, Konditionen' },
{ id: 'zahlungsdaten', label: 'Zahlungs-/Bankdaten', desc: 'IBAN, Kreditkarte, Rechnungen' },
{ id: 'beschaeftigtendaten', label: 'Beschäftigtendaten', desc: 'Gehalt, Arbeitszeiten, Urlaub' },
{ id: 'kommunikation', label: 'Kommunikationsdaten', desc: 'E-Mail-Inhalte, Chat-Verläufe' },
{ id: 'nutzungsdaten', label: 'Nutzungs-/Logdaten', desc: 'IP-Adressen, Login-Zeiten, Klicks' },
{ id: 'standortdaten', label: 'Standortdaten', desc: 'GPS, Check-in, Lieferadressen' },
{ id: 'bilddaten', label: 'Bild-/Videodaten', desc: 'Fotos, Videoaufnahmen, Profilbilder' },
{ id: 'bewerberdaten', label: 'Bewerberdaten', desc: 'Lebenslauf, Zeugnisse, Anschreiben' },
] as const
const SPECIAL_DATA_CATEGORIES = [
{ id: 'gesundheit', label: 'Gesundheitsdaten', desc: 'Krankheitstage, Atteste, Diagnosen' },
{ id: 'biometrie', label: 'Biometrische Daten', desc: 'Fingerabdruck, Gesichtserkennung' },
{ id: 'religion', label: 'Religiöse Überzeugungen', desc: 'Konfession, Feiertage' },
{ id: 'gewerkschaft', label: 'Gewerkschaftszugehörigkeit', desc: 'Mitgliedschaft' },
{ id: 'genetik', label: 'Genetische Daten', desc: 'DNA, Erbkrankheiten' },
] as const
// Rechtsgrundlagen nach DSGVO Art. 6
const LEGAL_BASES = [
{ id: 'consent', label: 'Einwilligung (Art. 6 Abs. 1a)' },
{ id: 'contract', label: 'Vertragserfüllung (Art. 6 Abs. 1b)' },
{ id: 'legal', label: 'Rechtliche Verpflichtung (Art. 6 Abs. 1c)' },
{ id: 'interest', label: 'Berechtigtes Interesse (Art. 6 Abs. 1f)' },
] as const
// Verarbeitungstätigkeiten-Vorlagen nach Branche
interface ProcessingActivityTemplate {
id: string
name: string
vendor: string
hosting: string
personal_data_categories: string[]
purpose: string
default_categories: string[]
default_legal_basis: string
}
const COMMON_ACTIVITIES: ProcessingActivityTemplate[] = [
{ id: 'personal', name: 'Personalverwaltung', purpose: 'Verwaltung von Mitarbeiterdaten, Gehaltsabrechnung, Arbeitszeitverwaltung', default_categories: ['stammdaten', 'kontaktdaten', 'beschaeftigtendaten', 'zahlungsdaten'], default_legal_basis: 'contract' },
{ id: 'buchhaltung', name: 'Buchhaltung / Rechnungswesen', purpose: 'Rechnungsstellung, Zahlungsabwicklung, Steuerliche Pflichten', default_categories: ['stammdaten', 'kontaktdaten', 'vertragsdaten', 'zahlungsdaten'], default_legal_basis: 'legal' },
{ id: 'bewerbung', name: 'Bewerbermanagement', purpose: 'Verwaltung von Bewerbungen und Auswahlverfahren', default_categories: ['stammdaten', 'kontaktdaten', 'bewerberdaten'], default_legal_basis: 'consent' },
{ id: 'website', name: 'Website-Betrieb', purpose: 'Bereitstellung der Unternehmenswebsite, Analytics, Kontaktformulare', default_categories: ['nutzungsdaten', 'kontaktdaten'], default_legal_basis: 'interest' },
{ id: 'email', name: 'E-Mail-Kommunikation', purpose: 'Geschäftliche E-Mail-Korrespondenz', default_categories: ['stammdaten', 'kontaktdaten', 'kommunikation'], default_legal_basis: 'interest' },
]
const INDUSTRY_ACTIVITIES: Record<string, ProcessingActivityTemplate[]> = {
'Technologie / IT': [
{ id: 'crm', name: 'CRM / Kundenverwaltung', purpose: 'Pflege von Kundenbeziehungen, Vertriebspipeline', default_categories: ['stammdaten', 'kontaktdaten', 'vertragsdaten', 'kommunikation'], default_legal_basis: 'contract' },
{ id: 'support', name: 'Support / Ticketsystem', purpose: 'Kundenanfragen bearbeiten, Fehlerbehebung', default_categories: ['stammdaten', 'kontaktdaten', 'kommunikation', 'nutzungsdaten'], default_legal_basis: 'contract' },
{ id: 'saas', name: 'SaaS-Plattform / Nutzerkonten', purpose: 'Bereitstellung der Software, Nutzerverwaltung', default_categories: ['stammdaten', 'kontaktdaten', 'nutzungsdaten', 'vertragsdaten'], default_legal_basis: 'contract' },
],
'E-Commerce / Handel': [
{ id: 'bestellung', name: 'Bestellabwicklung', purpose: 'Bestellannahme, Zahlungsabwicklung, Versand', default_categories: ['stammdaten', 'kontaktdaten', 'zahlungsdaten', 'vertragsdaten', 'standortdaten'], default_legal_basis: 'contract' },
{ id: 'kundenkonto', name: 'Kundenkonto-Verwaltung', purpose: 'Registrierung, Login, Bestellhistorie', default_categories: ['stammdaten', 'kontaktdaten', 'nutzungsdaten'], default_legal_basis: 'contract' },
{ id: 'newsletter', name: 'Newsletter / Marketing', purpose: 'E-Mail-Marketing, Werbeaktionen', default_categories: ['stammdaten', 'kontaktdaten', 'nutzungsdaten'], default_legal_basis: 'consent' },
],
'Finanzdienstleistungen': [
{ id: 'konto', name: 'Kontoverwaltung', purpose: 'Kontoeröffnung, KYC, Kontopflege', default_categories: ['stammdaten', 'kontaktdaten', 'zahlungsdaten', 'vertragsdaten'], default_legal_basis: 'contract' },
{ id: 'compliance_fin', name: 'Regulatorische Pflichten', purpose: 'Geldwäscheprävention, Meldepflichten', default_categories: ['stammdaten', 'zahlungsdaten'], default_legal_basis: 'legal' },
],
'Gesundheitswesen': [
{ id: 'patient', name: 'Patientenverwaltung', purpose: 'Aufnahme, Behandlung, Dokumentation', default_categories: ['stammdaten', 'kontaktdaten', 'gesundheit'], default_legal_basis: 'contract' },
{ id: 'termin', name: 'Terminplanung', purpose: 'Terminvergabe und -verwaltung', default_categories: ['stammdaten', 'kontaktdaten'], default_legal_basis: 'contract' },
{ id: 'abrechnung_kv', name: 'Abrechnung / KV', purpose: 'Kassenärztliche Abrechnung, Privatliquidation', default_categories: ['stammdaten', 'zahlungsdaten', 'gesundheit'], default_legal_basis: 'legal' },
],
'Beratung / Consulting': [
{ id: 'crm', name: 'CRM / Kundenverwaltung', purpose: 'Pflege von Kundenbeziehungen, Projekthistorie', default_categories: ['stammdaten', 'kontaktdaten', 'vertragsdaten', 'kommunikation'], default_legal_basis: 'contract' },
{ id: 'projekt', name: 'Projektmanagement', purpose: 'Projektverwaltung, Zeiterfassung, Dokumentation', default_categories: ['stammdaten', 'beschaeftigtendaten', 'kommunikation'], default_legal_basis: 'contract' },
],
'Bildung': [
{ id: 'schueler', name: 'Schüler-/Teilnehmerverwaltung', purpose: 'Verwaltung von Lernenden, Noten, Anwesenheit', default_categories: ['stammdaten', 'kontaktdaten', 'nutzungsdaten'], default_legal_basis: 'contract' },
{ id: 'lernplattform', name: 'Lernplattform / LMS', purpose: 'Bereitstellung von Lernmaterialien, Prüfungen', default_categories: ['stammdaten', 'nutzungsdaten', 'kommunikation'], default_legal_basis: 'contract' },
],
'Marketing / Agentur': [
{ id: 'crm', name: 'CRM / Kundenverwaltung', purpose: 'Pflege von Kundenbeziehungen, Kampagnen', default_categories: ['stammdaten', 'kontaktdaten', 'vertragsdaten', 'kommunikation'], default_legal_basis: 'contract' },
{ id: 'campaign', name: 'Kampagnen / Analytics', purpose: 'Werbekampagnen, Tracking, Auswertung', default_categories: ['nutzungsdaten', 'kontaktdaten', 'standortdaten'], default_legal_basis: 'consent' },
],
'Produktion / Industrie': [
{ id: 'lieferanten', name: 'Lieferantenverwaltung', purpose: 'Bestellungen, Lieferantenqualifizierung', default_categories: ['stammdaten', 'kontaktdaten', 'vertragsdaten', 'zahlungsdaten'], default_legal_basis: 'contract' },
{ id: 'zutrittskontrolle', name: 'Zutrittskontrolle', purpose: 'Werksgelände-Zugang, Besuchermanagement', default_categories: ['stammdaten', 'standortdaten', 'bilddaten'], default_legal_basis: 'interest' },
],
}
// Get processing activity templates for the selected industry
function getActivityTemplates(industry: string): ProcessingActivityTemplate[] {
const industrySpecific = INDUSTRY_ACTIVITIES[industry] || []
// Merge common + industry-specific, avoiding duplicate IDs
const existingIds = new Set(industrySpecific.map(a => a.id))
const common = COMMON_ACTIVITIES.filter(a => !existingIds.has(a.id))
return [...industrySpecific, ...common]
}
interface ProcessingActivity {
id: string
name: string
purpose: string
data_categories: string[]
legal_basis: string
custom?: boolean
}
interface AISystem {
name: string
purpose: string
risk_category: string
vendor: string
has_human_oversight: boolean
purpose: string
processes_personal_data: boolean
}
function StepSystemsAndAI({
function StepProcessingAndAI({
data,
onChange,
}: {
data: Partial<CompanyProfile> & { processingSystems?: ProcessingSystem[]; aiSystems?: AISystem[] }
data: Partial<CompanyProfile> & { processingSystems?: ProcessingActivity[]; aiSystems?: AISystem[] }
onChange: (updates: Record<string, unknown>) => void
}) {
const systems = (data as any).processingSystems || []
const aiSystems = (data as any).aiSystems || []
const activities: ProcessingActivity[] = (data as any).processingSystems || []
const aiSystems: AISystem[] = (data as any).aiSystems || []
const industry = data.industry || ''
const [expandedActivity, setExpandedActivity] = useState<string | null>(null)
const addSystem = () => {
onChange({ processingSystems: [...systems, { name: '', vendor: '', hosting: 'cloud', personal_data_categories: [] }] })
}
const removeSystem = (i: number) => {
onChange({ processingSystems: systems.filter((_: ProcessingSystem, idx: number) => idx !== i) })
}
const updateSystem = (i: number, updates: Partial<ProcessingSystem>) => {
const updated = [...systems]
updated[i] = { ...updated[i], ...updates }
onChange({ processingSystems: updated })
// Get suggested templates based on industry
const templates = getActivityTemplates(industry)
const activeIds = new Set(activities.map(a => a.id))
const toggleActivity = (template: ProcessingActivityTemplate) => {
if (activeIds.has(template.id)) {
onChange({ processingSystems: activities.filter(a => a.id !== template.id) })
} else {
onChange({
processingSystems: [...activities, {
id: template.id,
name: template.name,
purpose: template.purpose,
data_categories: [...template.default_categories],
legal_basis: template.default_legal_basis,
}],
})
}
}
const updateActivity = (id: string, updates: Partial<ProcessingActivity>) => {
onChange({
processingSystems: activities.map(a => a.id === id ? { ...a, ...updates } : a),
})
}
const toggleDataCategory = (activityId: string, categoryId: string) => {
const activity = activities.find(a => a.id === activityId)
if (!activity) return
const cats = activity.data_categories.includes(categoryId)
? activity.data_categories.filter(c => c !== categoryId)
: [...activity.data_categories, categoryId]
updateActivity(activityId, { data_categories: cats })
}
const addCustomActivity = () => {
const id = `custom_${Date.now()}`
onChange({
processingSystems: [...activities, {
id,
name: '',
purpose: '',
data_categories: [],
legal_basis: 'contract',
custom: true,
}],
})
setExpandedActivity(id)
}
const removeActivity = (id: string) => {
onChange({ processingSystems: activities.filter(a => a.id !== id) })
if (expandedActivity === id) setExpandedActivity(null)
}
// AI Systems
const addAISystem = () => {
onChange({ aiSystems: [...aiSystems, { name: '', purpose: '', risk_category: 'limited', vendor: '', has_human_oversight: true }] })
onChange({ aiSystems: [...aiSystems, { name: '', vendor: '', purpose: '', processes_personal_data: false }] })
}
const removeAISystem = (i: number) => {
onChange({ aiSystems: aiSystems.filter((_: AISystem, idx: number) => idx !== i) })
@@ -607,58 +787,254 @@ function StepSystemsAndAI({
return (
<div className="space-y-8">
{/* Processing Systems */}
{/* Processing Activities */}
<div>
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-sm font-medium text-gray-700">IT-Systeme mit personenbezogenen Daten</h3>
<p className="text-xs text-gray-500">Systeme, die personenbezogene Daten verarbeiten (fuer VVT-Generierung)</p>
</div>
<button type="button" onClick={addSystem} className="px-3 py-1.5 text-sm bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200">
+ System
</button>
<h3 className="text-sm font-medium text-gray-700 mb-1">Verarbeitungstätigkeiten</h3>
<p className="text-xs text-gray-500 mb-4">
Wählen Sie aus, welche Verarbeitungstätigkeiten in Ihrem Unternehmen stattfinden. Diese bilden die Grundlage für Ihr Verarbeitungsverzeichnis (VVT) nach Art. 30 DSGVO.
</p>
{/* Template checkboxes */}
<div className="space-y-2 mb-4">
{templates.map(template => {
const isActive = activeIds.has(template.id)
const activity = activities.find(a => a.id === template.id)
const isExpanded = expandedActivity === template.id
return (
<div key={template.id}>
<div
className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${
isActive ? 'border-purple-500 bg-purple-50' : 'border-gray-200 hover:border-purple-300'
}`}
onClick={() => {
if (!isActive) {
toggleActivity(template)
setExpandedActivity(template.id)
} else {
setExpandedActivity(isExpanded ? null : template.id)
}
}}
>
<input
type="checkbox"
checked={isActive}
onChange={e => {
e.stopPropagation()
toggleActivity(template)
}}
className="w-4 h-4 text-purple-600 rounded focus:ring-purple-500 flex-shrink-0"
/>
<div className="flex-1 min-w-0">
<span className="text-sm font-medium text-gray-900">{template.name}</span>
<p className="text-xs text-gray-500 truncate">{template.purpose}</p>
</div>
{isActive && (
<span className="text-xs text-purple-600 flex-shrink-0">
{activity?.data_categories.length || 0} Kategorien
</span>
)}
{isActive && (
<svg className={`w-4 h-4 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
)}
</div>
{/* Expanded detail: data categories + legal basis */}
{isActive && isExpanded && activity && (
<div className="ml-4 mt-2 p-4 bg-gray-50 rounded-lg border border-gray-200 space-y-4">
{/* Data Categories */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">Betroffene Datenkategorien</label>
<div className="grid grid-cols-2 gap-1.5">
{DATA_CATEGORIES.map(cat => (
<label key={cat.id} className="flex items-center gap-2 text-xs p-1.5 rounded hover:bg-gray-100 cursor-pointer">
<input
type="checkbox"
checked={activity.data_categories.includes(cat.id)}
onChange={() => toggleDataCategory(activity.id, cat.id)}
className="w-3.5 h-3.5 text-purple-600 rounded focus:ring-purple-500"
/>
<span className="text-gray-700" title={cat.desc}>{cat.label}</span>
</label>
))}
</div>
</div>
{/* Special Categories (Art. 9) */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">
Besondere Kategorien (Art. 9 DSGVO)
<span className="font-normal text-gray-400 ml-1"> nur wenn zutreffend</span>
</label>
<div className="grid grid-cols-2 gap-1.5">
{SPECIAL_DATA_CATEGORIES.map(cat => (
<label key={cat.id} className="flex items-center gap-2 text-xs p-1.5 rounded hover:bg-red-50 cursor-pointer">
<input
type="checkbox"
checked={activity.data_categories.includes(cat.id)}
onChange={() => toggleDataCategory(activity.id, cat.id)}
className="w-3.5 h-3.5 text-red-600 rounded focus:ring-red-500"
/>
<span className="text-gray-700" title={cat.desc}>{cat.label}</span>
</label>
))}
</div>
</div>
{/* Legal Basis */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">Rechtsgrundlage</label>
<select
value={activity.legal_basis}
onChange={e => updateActivity(activity.id, { legal_basis: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{LEGAL_BASES.map(lb => (
<option key={lb.id} value={lb.id}>{lb.label}</option>
))}
</select>
</div>
{/* Remove button for template-based activities */}
<button
type="button"
onClick={() => toggleActivity(template)}
className="text-xs text-red-500 hover:text-red-700"
>
Verarbeitungstätigkeit entfernen
</button>
</div>
)}
</div>
)
})}
</div>
{systems.length === 0 && (
<div className="text-center py-6 text-gray-400 border-2 border-dashed rounded-lg">Noch keine Systeme hinzugefuegt</div>
)}
<div className="space-y-3">
{systems.map((sys: ProcessingSystem, i: number) => (
<div key={i} className="border border-gray-200 rounded-lg p-4 space-y-3">
<div className="flex justify-between items-center">
<span className="text-xs font-medium text-gray-400">System {i + 1}</span>
<button type="button" onClick={() => removeSystem(i)} className="text-red-400 hover:text-red-600 text-xs">Entfernen</button>
</div>
<div className="grid grid-cols-2 gap-3">
<input type="text" value={sys.name} onChange={e => updateSystem(i, { name: e.target.value })} placeholder="Name (z.B. SAP HR)" className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
<input type="text" value={sys.vendor} onChange={e => updateSystem(i, { vendor: e.target.value })} placeholder="Hersteller" className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
</div>
<div className="grid grid-cols-2 gap-3">
<select value={sys.hosting} onChange={e => updateSystem(i, { hosting: e.target.value })} className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="on-premise">On-Premise</option>
<option value="cloud">Cloud (EU)</option>
<option value="us-cloud">Cloud (US)</option>
<option value="hybrid">Hybrid</option>
</select>
<input type="text" value={sys.personal_data_categories.join(', ')} onChange={e => updateSystem(i, { personal_data_categories: e.target.value.split(',').map(s => s.trim()).filter(Boolean) })} placeholder="Datenkategorien (kommagetrennt)" className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
{/* Custom activities */}
{activities.filter(a => a.custom).map(activity => (
<div key={activity.id} className="mb-2">
<div
className="flex items-center gap-3 p-3 rounded-lg border-2 border-purple-500 bg-purple-50 cursor-pointer"
onClick={() => setExpandedActivity(expandedActivity === activity.id ? null : activity.id)}
>
<span className="w-4 h-4 flex items-center justify-center text-purple-600 flex-shrink-0 text-sm"></span>
<div className="flex-1 min-w-0">
<span className="text-sm font-medium text-gray-900">{activity.name || 'Neue Verarbeitungstätigkeit'}</span>
</div>
<svg className={`w-4 h-4 text-gray-400 transition-transform ${expandedActivity === activity.id ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
))}
</div>
{expandedActivity === activity.id && (
<div className="ml-4 mt-2 p-4 bg-gray-50 rounded-lg border border-gray-200 space-y-4">
<div className="grid grid-cols-1 gap-3">
<input
type="text"
value={activity.name}
onChange={e => updateActivity(activity.id, { name: e.target.value })}
placeholder="Name der Verarbeitungstätigkeit"
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
<input
type="text"
value={activity.purpose}
onChange={e => updateActivity(activity.id, { purpose: e.target.value })}
placeholder="Zweck der Verarbeitung"
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
{/* Data Categories */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">Betroffene Datenkategorien</label>
<div className="grid grid-cols-2 gap-1.5">
{DATA_CATEGORIES.map(cat => (
<label key={cat.id} className="flex items-center gap-2 text-xs p-1.5 rounded hover:bg-gray-100 cursor-pointer">
<input
type="checkbox"
checked={activity.data_categories.includes(cat.id)}
onChange={() => toggleDataCategory(activity.id, cat.id)}
className="w-3.5 h-3.5 text-purple-600 rounded focus:ring-purple-500"
/>
<span className="text-gray-700">{cat.label}</span>
</label>
))}
</div>
</div>
{/* Special Categories */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">Besondere Kategorien (Art. 9)</label>
<div className="grid grid-cols-2 gap-1.5">
{SPECIAL_DATA_CATEGORIES.map(cat => (
<label key={cat.id} className="flex items-center gap-2 text-xs p-1.5 rounded hover:bg-red-50 cursor-pointer">
<input
type="checkbox"
checked={activity.data_categories.includes(cat.id)}
onChange={() => toggleDataCategory(activity.id, cat.id)}
className="w-3.5 h-3.5 text-red-600 rounded focus:ring-red-500"
/>
<span className="text-gray-700">{cat.label}</span>
</label>
))}
</div>
</div>
{/* Legal Basis */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-2">Rechtsgrundlage</label>
<select
value={activity.legal_basis}
onChange={e => updateActivity(activity.id, { legal_basis: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{LEGAL_BASES.map(lb => (
<option key={lb.id} value={lb.id}>{lb.label}</option>
))}
</select>
</div>
<button
type="button"
onClick={() => removeActivity(activity.id)}
className="text-xs text-red-500 hover:text-red-700"
>
Verarbeitungstätigkeit entfernen
</button>
</div>
)}
</div>
))}
{/* Add custom activity button */}
<button
type="button"
onClick={addCustomActivity}
className="w-full px-3 py-2 text-sm text-purple-700 bg-purple-50 border-2 border-dashed border-purple-200 rounded-lg hover:bg-purple-100 hover:border-purple-300 transition-colors"
>
+ Eigene Verarbeitungstätigkeit hinzufügen
</button>
</div>
{/* AI Systems */}
{/* AI Systems — simplified */}
<div className="border-t border-gray-200 pt-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-sm font-medium text-gray-700">KI-Systeme</h3>
<p className="text-xs text-gray-500">Strukturierter KI-Katalog fuer AI Act Compliance</p>
</div>
<div className="flex items-center justify-between mb-1">
<h3 className="text-sm font-medium text-gray-700">KI-Systeme</h3>
<button type="button" onClick={addAISystem} className="px-3 py-1.5 text-sm bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200">
+ KI-System
</button>
</div>
<p className="text-xs text-gray-500 mb-4">
Welche KI-Systeme setzen Sie ein? Die Risikoeinstufung nach AI Act ermittelt unser Tool automatisch.
</p>
{aiSystems.length === 0 && (
<div className="text-center py-6 text-gray-400 border-2 border-dashed rounded-lg">Noch keine KI-Systeme</div>
<div className="text-center py-6 text-gray-400 border-2 border-dashed rounded-lg">
Noch keine KI-Systeme falls Sie keine einsetzen, einfach leer lassen
</div>
)}
<div className="space-y-3">
{aiSystems.map((ai: AISystem, i: number) => (
@@ -668,22 +1044,19 @@ function StepSystemsAndAI({
<button type="button" onClick={() => removeAISystem(i)} className="text-red-400 hover:text-red-600 text-xs">Entfernen</button>
</div>
<div className="grid grid-cols-2 gap-3">
<input type="text" value={ai.name} onChange={e => updateAISystem(i, { name: e.target.value })} placeholder="Name (z.B. Chatbot)" className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
<input type="text" value={ai.vendor} onChange={e => updateAISystem(i, { vendor: e.target.value })} placeholder="Anbieter" className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
</div>
<input type="text" value={ai.purpose} onChange={e => updateAISystem(i, { purpose: e.target.value })} placeholder="Zweck (z.B. Kundensupport)" className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
<div className="grid grid-cols-2 gap-3">
<select value={ai.risk_category} onChange={e => updateAISystem(i, { risk_category: e.target.value })} className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="minimal">Minimal Risk</option>
<option value="limited">Limited Risk</option>
<option value="high">High Risk</option>
<option value="unacceptable">Unacceptable Risk</option>
</select>
<label className="flex items-center gap-2 px-3 py-2">
<input type="checkbox" checked={ai.has_human_oversight} onChange={e => updateAISystem(i, { has_human_oversight: e.target.checked })} className="w-4 h-4 text-purple-600 rounded focus:ring-purple-500" />
<span className="text-sm text-gray-700">Human Oversight</span>
</label>
<input type="text" value={ai.name} onChange={e => updateAISystem(i, { name: e.target.value })} placeholder="Name (z.B. ChatGPT, Copilot)" className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
<input type="text" value={ai.vendor} onChange={e => updateAISystem(i, { vendor: e.target.value })} placeholder="Anbieter (z.B. OpenAI, Microsoft)" className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
</div>
<input type="text" value={ai.purpose} onChange={e => updateAISystem(i, { purpose: e.target.value })} placeholder="Einsatzzweck (z.B. Kundensupport, Code-Assistenz)" className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
<label className="flex items-center gap-2 px-1">
<input
type="checkbox"
checked={ai.processes_personal_data}
onChange={e => updateAISystem(i, { processes_personal_data: e.target.checked })}
className="w-4 h-4 text-purple-600 rounded focus:ring-purple-500"
/>
<span className="text-sm text-gray-700">Verarbeitet personenbezogene Daten</span>
</label>
</div>
))}
</div>
@@ -1329,7 +1702,11 @@ export default function CompanyProfilePage() {
employeeCount: '',
annualRevenue: '',
headquartersCountry: 'DE',
headquartersCountryOther: '',
headquartersStreet: '',
headquartersZip: '',
headquartersCity: '',
headquartersState: '',
hasInternationalLocations: false,
internationalCountries: [],
targetMarkets: [],
@@ -1373,7 +1750,11 @@ export default function CompanyProfilePage() {
employeeCount: data.employee_count || '',
annualRevenue: data.annual_revenue || '',
headquartersCountry: data.headquarters_country || 'DE',
headquartersCountryOther: data.headquarters_country_other || '',
headquartersStreet: data.headquarters_street || '',
headquartersZip: data.headquarters_zip || '',
headquartersCity: data.headquarters_city || '',
headquartersState: data.headquarters_state || '',
hasInternationalLocations: data.has_international_locations || false,
internationalCountries: data.international_countries || [],
targetMarkets: data.target_markets || [],
@@ -1440,7 +1821,11 @@ export default function CompanyProfilePage() {
employee_count: formData.employeeCount || '',
annual_revenue: formData.annualRevenue || '',
headquarters_country: formData.headquartersCountry || 'DE',
headquarters_country_other: formData.headquartersCountryOther || '',
headquarters_street: formData.headquartersStreet || '',
headquarters_zip: formData.headquartersZip || '',
headquarters_city: formData.headquartersCity || '',
headquarters_state: formData.headquartersState || '',
has_international_locations: formData.hasInternationalLocations || false,
international_countries: formData.internationalCountries || [],
target_markets: formData.targetMarkets || [],
@@ -1699,9 +2084,9 @@ export default function CompanyProfilePage() {
</div>
{/* Content */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div>
{/* Form */}
<div className="lg:col-span-2">
<div>
<div className="bg-white rounded-xl border border-gray-200 p-8">
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-900">
@@ -1718,7 +2103,7 @@ export default function CompanyProfilePage() {
{currentStep === 3 && <StepCompanySize data={formData} onChange={updateFormData} />}
{currentStep === 4 && <StepLocations data={formData} onChange={updateFormData} />}
{currentStep === 5 && <StepDataProtection data={formData} onChange={updateFormData} />}
{currentStep === 6 && <StepSystemsAndAI data={formData} onChange={updateFormData} />}
{currentStep === 6 && <StepProcessingAndAI data={formData} onChange={updateFormData} />}
{currentStep === 7 && <StepLegalFramework data={formData} onChange={updateFormData} />}
{currentStep === 8 && showMachineBuilderStep && <StepMachineBuilder data={formData} onChange={updateFormData} />}
@@ -1751,6 +2136,17 @@ export default function CompanyProfilePage() {
{isLastStep ? 'Profil speichern & weiter' : 'Weiter'}
</button>
</div>
{/* Generate Documents CTA (only when profile is complete) */}
{formData.isComplete && (
<div className="mt-6 bg-gradient-to-br from-purple-50 to-indigo-50 rounded-xl border border-purple-200 p-6">
<h3 className="font-semibold text-purple-900 mb-2">Dokumente generieren</h3>
<p className="text-sm text-purple-700 mb-4">
Basierend auf Ihrem Profil können DSFA, VVT, TOM, Löschfristen und Pflichten automatisch als Entwürfe generiert werden.
</p>
<GenerateDocumentsButton />
</div>
)}
</div>
</div>
@@ -1781,20 +2177,6 @@ export default function CompanyProfilePage() {
</div>
</div>
)}
{/* Sidebar */}
<div className="lg:col-span-1">
{/* Generate Documents CTA */}
{formData.isComplete && (
<div className="bg-gradient-to-br from-purple-50 to-indigo-50 rounded-xl border border-purple-200 p-6">
<h3 className="font-semibold text-purple-900 mb-2">Dokumente generieren</h3>
<p className="text-sm text-purple-700 mb-4">
Basierend auf Ihrem Profil können DSFA, VVT, TOM, Löschfristen und Pflichten automatisch als Entwürfe generiert werden.
</p>
<GenerateDocumentsButton />
</div>
)}
</div>
</div>
</div>
</div>

View File

@@ -163,7 +163,11 @@ export interface CompanyProfile {
// Locations
headquartersCountry: string // ISO country code, e.g., "DE"
headquartersCountryOther: string // Free text if country not in list
headquartersStreet: string
headquartersZip: string
headquartersCity: string
headquartersState: string // Bundesland / Kanton / Region
hasInternationalLocations: boolean
internationalCountries: string[] // ISO country codes