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>
404 lines
14 KiB
TypeScript
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
|