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