Files
breakpilot-compliance/admin-compliance/components/sdk/tom-generator/steps/ScopeRolesStep.tsx
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

404 lines
14 KiB
TypeScript

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