All checks were successful
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) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 19s
Paket A — Kritische Blocker: - compliance_scope_routes.py: GET + POST UPSERT für sdk_states JSONB-Feld - compliance/api/__init__.py: compliance_scope_router registriert - import/route.ts: POST-Proxy für multipart/form-data Upload - screening/route.ts: POST-Proxy für Dependency-File Upload Paket B — Backend + UI: - company_profile_routes.py: DELETE-Endpoint (DSGVO Art. 17) - company-profile/route.ts: DELETE-Proxy - company-profile/page.tsx: Profil-löschen-Button mit Bestätigungs-Dialog - source-policy/pii-rules/[id]/route.ts: GET ergänzt - source-policy/operations/[id]/route.ts: GET + DELETE ergänzt Paket C — Tests + UI: - test_compliance_scope_routes.py: 27 Tests (neu) - test_import_routes.py: +36 Tests → 60 gesamt - test_screening_routes.py: +28 Tests → 80+ gesamt - source-policy/page.tsx: "Blockierte Inhalte" Tab mit Tabelle + Remove Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1537 lines
59 KiB
TypeScript
1537 lines
59 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect } from 'react'
|
|
import { useSDK } from '@/lib/sdk'
|
|
import {
|
|
CompanyProfile,
|
|
BusinessModel,
|
|
OfferingType,
|
|
TargetMarket,
|
|
CompanySize,
|
|
LegalForm,
|
|
MachineBuilderProfile,
|
|
MachineProductType,
|
|
AIIntegrationType,
|
|
HumanOversightLevel,
|
|
CriticalSector,
|
|
BUSINESS_MODEL_LABELS,
|
|
OFFERING_TYPE_LABELS,
|
|
TARGET_MARKET_LABELS,
|
|
COMPANY_SIZE_LABELS,
|
|
MACHINE_PRODUCT_TYPE_LABELS,
|
|
AI_INTEGRATION_TYPE_LABELS,
|
|
HUMAN_OVERSIGHT_LABELS,
|
|
CRITICAL_SECTOR_LABELS,
|
|
SDKCoverageAssessment,
|
|
} from '@/lib/sdk/types'
|
|
|
|
// =============================================================================
|
|
// WIZARD STEPS
|
|
// =============================================================================
|
|
|
|
const BASE_WIZARD_STEPS = [
|
|
{ id: 1, name: 'Basisinfos', description: 'Firmenname und Rechtsform' },
|
|
{ id: 2, name: 'Geschaeftsmodell', description: 'B2B, B2C und Angebote' },
|
|
{ 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' },
|
|
]
|
|
|
|
const MACHINE_BUILDER_STEP = { id: 6, name: 'Produkt & Maschine', description: 'Software, KI und CE in Ihrem Produkt' }
|
|
|
|
function getWizardSteps(industry: string) {
|
|
if (isMachineBuilderIndustry(industry)) {
|
|
return [...BASE_WIZARD_STEPS, MACHINE_BUILDER_STEP]
|
|
}
|
|
return BASE_WIZARD_STEPS
|
|
}
|
|
|
|
// Keep WIZARD_STEPS for backwards compat in static references
|
|
const WIZARD_STEPS = BASE_WIZARD_STEPS
|
|
|
|
// =============================================================================
|
|
// LEGAL FORMS
|
|
// =============================================================================
|
|
|
|
const LEGAL_FORM_LABELS: Record<LegalForm, string> = {
|
|
einzelunternehmen: 'Einzelunternehmen',
|
|
gbr: 'GbR',
|
|
ohg: 'OHG',
|
|
kg: 'KG',
|
|
gmbh: 'GmbH',
|
|
ug: 'UG (haftungsbeschränkt)',
|
|
ag: 'AG',
|
|
gmbh_co_kg: 'GmbH & Co. KG',
|
|
ev: 'e.V. (Verein)',
|
|
stiftung: 'Stiftung',
|
|
other: 'Sonstige',
|
|
}
|
|
|
|
// =============================================================================
|
|
// INDUSTRIES
|
|
// =============================================================================
|
|
|
|
const INDUSTRIES = [
|
|
'Technologie / IT',
|
|
'E-Commerce / Handel',
|
|
'Finanzdienstleistungen',
|
|
'Gesundheitswesen',
|
|
'Bildung',
|
|
'Beratung / Consulting',
|
|
'Marketing / Agentur',
|
|
'Produktion / Industrie',
|
|
'Logistik / Transport',
|
|
'Immobilien',
|
|
'Maschinenbau',
|
|
'Anlagenbau',
|
|
'Automatisierung',
|
|
'Robotik',
|
|
'Messtechnik',
|
|
'Sonstige',
|
|
]
|
|
|
|
const MACHINE_BUILDER_INDUSTRIES = [
|
|
'Maschinenbau',
|
|
'Anlagenbau',
|
|
'Automatisierung',
|
|
'Robotik',
|
|
'Messtechnik',
|
|
]
|
|
|
|
const isMachineBuilderIndustry = (industry: string) =>
|
|
MACHINE_BUILDER_INDUSTRIES.includes(industry)
|
|
|
|
// =============================================================================
|
|
// HELPER: ASSESS SDK COVERAGE
|
|
// =============================================================================
|
|
|
|
function assessSDKCoverage(profile: Partial<CompanyProfile>): SDKCoverageAssessment {
|
|
const coveredRegulations: string[] = ['DSGVO', 'BDSG', 'TTDSG', 'AI Act']
|
|
const partiallyCoveredRegulations: string[] = []
|
|
const notCoveredRegulations: string[] = []
|
|
const reasons: string[] = []
|
|
const recommendations: string[] = []
|
|
|
|
// Check target markets
|
|
const targetMarkets = profile.targetMarkets || []
|
|
|
|
if (targetMarkets.includes('worldwide')) {
|
|
notCoveredRegulations.push('CCPA (Kalifornien)', 'LGPD (Brasilien)', 'POPIA (Südafrika)')
|
|
reasons.push('Weltweiter Betrieb erfordert Kenntnisse lokaler Datenschutzgesetze')
|
|
recommendations.push('Für außereuropäische Märkte empfehlen wir die Konsultation lokaler Rechtsanwälte')
|
|
}
|
|
|
|
if (targetMarkets.includes('eu_uk')) {
|
|
partiallyCoveredRegulations.push('UK GDPR', 'UK AI Framework')
|
|
reasons.push('UK-Recht weicht nach Brexit teilweise von EU-Recht ab')
|
|
recommendations.push('Prüfen Sie UK-spezifische Anpassungen Ihrer Datenschutzerklärung')
|
|
}
|
|
|
|
// Check company size
|
|
if (profile.companySize === 'enterprise' || profile.companySize === 'large') {
|
|
coveredRegulations.push('NIS2')
|
|
reasons.push('Als größeres Unternehmen können NIS2-Pflichten relevant sein')
|
|
}
|
|
|
|
// Check offerings
|
|
const offerings = profile.offerings || []
|
|
if (offerings.includes('webshop')) {
|
|
coveredRegulations.push('Fernabsatzrecht')
|
|
recommendations.push('Widerrufsbelehrung und AGB-Generator sind im SDK enthalten')
|
|
}
|
|
|
|
// Determine if fully covered
|
|
const requiresLegalCounsel = notCoveredRegulations.length > 0 || targetMarkets.includes('worldwide')
|
|
const isFullyCovered = !requiresLegalCounsel && notCoveredRegulations.length === 0
|
|
|
|
return {
|
|
isFullyCovered,
|
|
coveredRegulations,
|
|
partiallyCoveredRegulations,
|
|
notCoveredRegulations,
|
|
requiresLegalCounsel,
|
|
reasons,
|
|
recommendations,
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// STEP COMPONENTS
|
|
// =============================================================================
|
|
|
|
function StepBasicInfo({
|
|
data,
|
|
onChange,
|
|
}: {
|
|
data: Partial<CompanyProfile>
|
|
onChange: (updates: Partial<CompanyProfile>) => void
|
|
}) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Firmenname <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={data.companyName || ''}
|
|
onChange={e => onChange({ companyName: e.target.value })}
|
|
placeholder="Ihre Firma GmbH"
|
|
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-2">
|
|
Rechtsform <span className="text-red-500">*</span>
|
|
</label>
|
|
<select
|
|
value={data.legalForm || ''}
|
|
onChange={e => onChange({ legalForm: e.target.value as LegalForm })}
|
|
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>
|
|
{Object.entries(LEGAL_FORM_LABELS).map(([value, label]) => (
|
|
<option key={value} value={value}>
|
|
{label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Branche</label>
|
|
<select
|
|
value={data.industry || ''}
|
|
onChange={e => onChange({ industry: 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>
|
|
{INDUSTRIES.map(industry => (
|
|
<option key={industry} value={industry}>
|
|
{industry}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Gründungsjahr</label>
|
|
<input
|
|
type="number"
|
|
value={data.foundedYear || ''}
|
|
onChange={e => onChange({ foundedYear: parseInt(e.target.value) || null })}
|
|
placeholder="2020"
|
|
min="1800"
|
|
max={new Date().getFullYear()}
|
|
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>
|
|
)
|
|
}
|
|
|
|
function StepBusinessModel({
|
|
data,
|
|
onChange,
|
|
}: {
|
|
data: Partial<CompanyProfile>
|
|
onChange: (updates: Partial<CompanyProfile>) => void
|
|
}) {
|
|
const toggleOffering = (offering: OfferingType) => {
|
|
const current = data.offerings || []
|
|
if (current.includes(offering)) {
|
|
onChange({ offerings: current.filter(o => o !== offering) })
|
|
} else {
|
|
onChange({ offerings: [...current, offering] })
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-4">
|
|
Geschäftsmodell <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
{Object.entries(BUSINESS_MODEL_LABELS).map(([value, label]) => (
|
|
<button
|
|
key={value}
|
|
type="button"
|
|
onClick={() => onChange({ businessModel: value as BusinessModel })}
|
|
className={`p-4 rounded-xl border-2 text-center transition-all ${
|
|
data.businessModel === value
|
|
? 'border-purple-500 bg-purple-50 text-purple-700'
|
|
: 'border-gray-200 hover:border-purple-300'
|
|
}`}
|
|
>
|
|
<div className="text-2xl mb-2">
|
|
{value === 'B2B' ? '🏢' : value === 'B2C' ? '👥' : '🏢👥'}
|
|
</div>
|
|
<div className="font-medium">{label}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-4">
|
|
Was bieten Sie an? <span className="text-gray-400">(Mehrfachauswahl möglich)</span>
|
|
</label>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{Object.entries(OFFERING_TYPE_LABELS).map(([value, { label, description }]) => (
|
|
<button
|
|
key={value}
|
|
type="button"
|
|
onClick={() => toggleOffering(value as OfferingType)}
|
|
className={`p-4 rounded-xl border-2 text-left transition-all ${
|
|
(data.offerings || []).includes(value as OfferingType)
|
|
? 'border-purple-500 bg-purple-50'
|
|
: 'border-gray-200 hover:border-purple-300'
|
|
}`}
|
|
>
|
|
<div className="font-medium text-gray-900">{label}</div>
|
|
<div className="text-sm text-gray-500">{description}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function StepCompanySize({
|
|
data,
|
|
onChange,
|
|
}: {
|
|
data: Partial<CompanyProfile>
|
|
onChange: (updates: Partial<CompanyProfile>) => void
|
|
}) {
|
|
return (
|
|
<div className="space-y-8">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-4">
|
|
Unternehmensgröße <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="space-y-3">
|
|
{Object.entries(COMPANY_SIZE_LABELS).map(([value, label]) => (
|
|
<button
|
|
key={value}
|
|
type="button"
|
|
onClick={() => onChange({ companySize: value as CompanySize })}
|
|
className={`w-full p-4 rounded-xl border-2 text-left transition-all ${
|
|
data.companySize === value
|
|
? 'border-purple-500 bg-purple-50'
|
|
: 'border-gray-200 hover:border-purple-300'
|
|
}`}
|
|
>
|
|
<div className="font-medium text-gray-900">{label}</div>
|
|
</button>
|
|
))}
|
|
</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">< 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">> 50 Mio. Euro</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function StepLocations({
|
|
data,
|
|
onChange,
|
|
}: {
|
|
data: Partial<CompanyProfile>
|
|
onChange: (updates: Partial<CompanyProfile>) => void
|
|
}) {
|
|
const toggleMarket = (market: TargetMarket) => {
|
|
const current = data.targetMarkets || []
|
|
if (current.includes(market)) {
|
|
onChange({ targetMarkets: current.filter(m => m !== market) })
|
|
} else {
|
|
onChange({ targetMarkets: [...current, market] })
|
|
}
|
|
}
|
|
|
|
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>
|
|
|
|
<div>
|
|
<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"
|
|
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>
|
|
|
|
<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 }]) => (
|
|
<button
|
|
key={value}
|
|
type="button"
|
|
onClick={() => toggleMarket(value as TargetMarket)}
|
|
className={`w-full p-4 rounded-xl border-2 text-left transition-all ${
|
|
(data.targetMarkets || []).includes(value as TargetMarket)
|
|
? 'border-purple-500 bg-purple-50'
|
|
: '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>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function StepDataProtection({
|
|
data,
|
|
onChange,
|
|
}: {
|
|
data: Partial<CompanyProfile>
|
|
onChange: (updates: Partial<CompanyProfile>) => void
|
|
}) {
|
|
return (
|
|
<div className="space-y-8">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-4">
|
|
Datenschutz-Rolle nach DSGVO
|
|
</label>
|
|
<div className="space-y-3">
|
|
<label className="flex items-start gap-4 p-4 rounded-xl border-2 border-gray-200 hover:border-purple-300 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={data.isDataController ?? true}
|
|
onChange={e => onChange({ isDataController: e.target.checked })}
|
|
className="mt-1 w-5 h-5 text-purple-600 rounded focus:ring-purple-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900">Verantwortlicher (Art. 4 Nr. 7 DSGVO)</div>
|
|
<div className="text-sm text-gray-500">
|
|
Wir entscheiden selbst über Zwecke und Mittel der Datenverarbeitung
|
|
</div>
|
|
</div>
|
|
</label>
|
|
|
|
<label className="flex items-start gap-4 p-4 rounded-xl border-2 border-gray-200 hover:border-purple-300 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={data.isDataProcessor ?? false}
|
|
onChange={e => onChange({ isDataProcessor: e.target.checked })}
|
|
className="mt-1 w-5 h-5 text-purple-600 rounded focus:ring-purple-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900">Auftragsverarbeiter (Art. 4 Nr. 8 DSGVO)</div>
|
|
<div className="text-sm text-gray-500">
|
|
Wir verarbeiten personenbezogene Daten im Auftrag anderer Unternehmen
|
|
</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="flex items-start gap-4 p-4 rounded-xl border-2 border-gray-200 hover:border-purple-300 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={data.usesAI ?? false}
|
|
onChange={e => onChange({ usesAI: e.target.checked })}
|
|
className="mt-1 w-5 h-5 text-purple-600 rounded focus:ring-purple-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900">Wir setzen KI/ML-Systeme ein</div>
|
|
<div className="text-sm text-gray-500">
|
|
Chatbots, Empfehlungssysteme, automatisierte Entscheidungen, etc.
|
|
</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-6">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Datenschutzbeauftragter (Name)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={data.dpoName || ''}
|
|
onChange={e => onChange({ dpoName: e.target.value || null })}
|
|
placeholder="Optional"
|
|
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-2">DSB E-Mail</label>
|
|
<input
|
|
type="email"
|
|
value={data.dpoEmail || ''}
|
|
onChange={e => onChange({ dpoEmail: e.target.value || null })}
|
|
placeholder="dsb@firma.de"
|
|
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>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// STEP 6: PRODUKT & MASCHINE (nur fuer Maschinenbauer)
|
|
// =============================================================================
|
|
|
|
const EMPTY_MACHINE_BUILDER: MachineBuilderProfile = {
|
|
productTypes: [],
|
|
productDescription: '',
|
|
productPride: '',
|
|
containsSoftware: false,
|
|
containsFirmware: false,
|
|
containsAI: false,
|
|
aiIntegrationType: [],
|
|
hasSafetyFunction: false,
|
|
safetyFunctionDescription: '',
|
|
autonomousBehavior: false,
|
|
humanOversightLevel: 'full',
|
|
isNetworked: false,
|
|
hasRemoteAccess: false,
|
|
hasOTAUpdates: false,
|
|
updateMechanism: '',
|
|
exportMarkets: [],
|
|
criticalSectorClients: false,
|
|
criticalSectors: [],
|
|
oemClients: false,
|
|
ceMarkingRequired: false,
|
|
existingCEProcess: false,
|
|
hasRiskAssessment: false,
|
|
}
|
|
|
|
function StepMachineBuilder({
|
|
data,
|
|
onChange,
|
|
}: {
|
|
data: Partial<CompanyProfile>
|
|
onChange: (updates: Partial<CompanyProfile>) => void
|
|
}) {
|
|
const mb = data.machineBuilder || EMPTY_MACHINE_BUILDER
|
|
|
|
const updateMB = (updates: Partial<MachineBuilderProfile>) => {
|
|
onChange({ machineBuilder: { ...mb, ...updates } })
|
|
}
|
|
|
|
const toggleProductType = (type: MachineProductType) => {
|
|
const current = mb.productTypes || []
|
|
if (current.includes(type)) {
|
|
updateMB({ productTypes: current.filter(t => t !== type) })
|
|
} else {
|
|
updateMB({ productTypes: [...current, type] })
|
|
}
|
|
}
|
|
|
|
const toggleAIType = (type: AIIntegrationType) => {
|
|
const current = mb.aiIntegrationType || []
|
|
if (current.includes(type)) {
|
|
updateMB({ aiIntegrationType: current.filter(t => t !== type) })
|
|
} else {
|
|
updateMB({ aiIntegrationType: [...current, type] })
|
|
}
|
|
}
|
|
|
|
const toggleCriticalSector = (sector: CriticalSector) => {
|
|
const current = mb.criticalSectors || []
|
|
if (current.includes(sector)) {
|
|
updateMB({ criticalSectors: current.filter(s => s !== sector) })
|
|
} else {
|
|
updateMB({ criticalSectors: [...current, sector] })
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Block 1: Erzaehlen Sie uns von Ihrer Anlage */}
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-1">Erzaehlen Sie uns von Ihrer Anlage</h3>
|
|
<p className="text-sm text-gray-500 mb-4">
|
|
Je besser wir Ihr Produkt verstehen, desto praeziser koennen wir die relevanten Vorschriften identifizieren.
|
|
</p>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Was baut Ihr Unternehmen? <span className="text-red-500">*</span>
|
|
</label>
|
|
<textarea
|
|
value={mb.productDescription}
|
|
onChange={e => updateMB({ productDescription: e.target.value })}
|
|
placeholder="z.B. Wir bauen automatisierte Pruefstaende fuer die Qualitaetskontrolle in der Automobilindustrie..."
|
|
rows={3}
|
|
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-2">
|
|
Was macht Ihre Anlage besonders?
|
|
</label>
|
|
<textarea
|
|
value={mb.productPride}
|
|
onChange={e => updateMB({ productPride: e.target.value })}
|
|
placeholder="z.B. Unsere Anlage kann 500 Teile/Stunde mit 99.9% Erkennungsrate pruefen..."
|
|
rows={2}
|
|
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-3">
|
|
Produkttyp <span className="text-gray-400">(Mehrfachauswahl)</span>
|
|
</label>
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
|
{Object.entries(MACHINE_PRODUCT_TYPE_LABELS).map(([value, label]) => (
|
|
<button
|
|
key={value}
|
|
type="button"
|
|
onClick={() => toggleProductType(value as MachineProductType)}
|
|
className={`px-4 py-3 rounded-lg border-2 text-sm font-medium transition-all ${
|
|
mb.productTypes.includes(value as MachineProductType)
|
|
? 'border-purple-500 bg-purple-50 text-purple-700'
|
|
: 'border-gray-200 hover:border-purple-300 text-gray-700'
|
|
}`}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Block 2: Software & KI */}
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Software & KI in Ihrem Produkt</h3>
|
|
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
{[
|
|
{ key: 'containsSoftware', label: 'Enthaelt Software', desc: 'Anwendungssoftware in der Maschine' },
|
|
{ key: 'containsFirmware', label: 'Enthaelt Firmware', desc: 'Embedded Software / Steuerung' },
|
|
{ key: 'containsAI', label: 'Enthaelt KI/ML', desc: 'Kuenstliche Intelligenz / Machine Learning' },
|
|
].map(item => (
|
|
<label
|
|
key={item.key}
|
|
className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
|
(mb as any)[item.key]
|
|
? 'border-purple-500 bg-purple-50'
|
|
: 'border-gray-200 hover:border-purple-300'
|
|
}`}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={(mb as any)[item.key] ?? false}
|
|
onChange={e => updateMB({ [item.key]: e.target.checked } as any)}
|
|
className="mt-1 w-5 h-5 text-purple-600 rounded focus:ring-purple-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900 text-sm">{item.label}</div>
|
|
<div className="text-xs text-gray-500">{item.desc}</div>
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
|
|
{mb.containsAI && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
Art der KI-Integration
|
|
</label>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{Object.entries(AI_INTEGRATION_TYPE_LABELS).map(([value, label]) => (
|
|
<button
|
|
key={value}
|
|
type="button"
|
|
onClick={() => toggleAIType(value as AIIntegrationType)}
|
|
className={`px-4 py-2 rounded-lg border text-sm transition-all ${
|
|
mb.aiIntegrationType.includes(value as AIIntegrationType)
|
|
? 'border-purple-500 bg-purple-50 text-purple-700'
|
|
: 'border-gray-200 hover:border-purple-300 text-gray-700'
|
|
}`}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
|
mb.hasSafetyFunction ? 'border-red-400 bg-red-50' : 'border-gray-200 hover:border-gray-300'
|
|
}`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={mb.hasSafetyFunction}
|
|
onChange={e => updateMB({ hasSafetyFunction: e.target.checked })}
|
|
className="mt-1 w-5 h-5 text-red-600 rounded focus:ring-red-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900 text-sm">Sicherheitsrelevante Funktion</div>
|
|
<div className="text-xs text-gray-500">KI/SW hat sicherheitsrelevante Funktion</div>
|
|
</div>
|
|
</label>
|
|
|
|
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
|
mb.autonomousBehavior ? 'border-amber-400 bg-amber-50' : 'border-gray-200 hover:border-gray-300'
|
|
}`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={mb.autonomousBehavior}
|
|
onChange={e => updateMB({ autonomousBehavior: e.target.checked })}
|
|
className="mt-1 w-5 h-5 text-amber-600 rounded focus:ring-amber-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900 text-sm">Autonomes Verhalten</div>
|
|
<div className="text-xs text-gray-500">System lernt oder handelt eigenstaendig</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
{mb.hasSafetyFunction && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Beschreibung der Sicherheitsfunktion
|
|
</label>
|
|
<textarea
|
|
value={mb.safetyFunctionDescription}
|
|
onChange={e => updateMB({ safetyFunctionDescription: e.target.value })}
|
|
placeholder="z.B. KI-Vision ueberwacht den Schutzbereich und stoppt den Roboter bei Personenerkennung..."
|
|
rows={2}
|
|
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-2">
|
|
Human Oversight Level
|
|
</label>
|
|
<select
|
|
value={mb.humanOversightLevel}
|
|
onChange={e => updateMB({ humanOversightLevel: e.target.value as HumanOversightLevel })}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
>
|
|
{Object.entries(HUMAN_OVERSIGHT_LABELS).map(([value, label]) => (
|
|
<option key={value} value={value}>{label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Block 3: Konnektivitaet & Updates */}
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Konnektivitaet & Updates</h3>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
|
|
{[
|
|
{ key: 'isNetworked', label: 'Vernetzt', desc: 'Maschine ist mit Netzwerk verbunden' },
|
|
{ key: 'hasRemoteAccess', label: 'Remote-Zugriff', desc: 'Fernwartung / Remote-Zugang' },
|
|
{ key: 'hasOTAUpdates', label: 'OTA-Updates', desc: 'Drahtlose Software-/Firmware-Updates' },
|
|
].map(item => (
|
|
<label
|
|
key={item.key}
|
|
className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
|
(mb as any)[item.key]
|
|
? 'border-purple-500 bg-purple-50'
|
|
: 'border-gray-200 hover:border-purple-300'
|
|
}`}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={(mb as any)[item.key] ?? false}
|
|
onChange={e => updateMB({ [item.key]: e.target.checked } as any)}
|
|
className="mt-1 w-5 h-5 text-purple-600 rounded focus:ring-purple-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900 text-sm">{item.label}</div>
|
|
<div className="text-xs text-gray-500">{item.desc}</div>
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
|
|
{(mb.hasOTAUpdates || mb.hasRemoteAccess) && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Wie werden Updates eingespielt?
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={mb.updateMechanism}
|
|
onChange={e => updateMB({ updateMechanism: e.target.value })}
|
|
placeholder="z.B. VPN-gesicherter Remote-Zugang mit manueller Freigabe..."
|
|
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>
|
|
|
|
{/* Block 4: Markt & Kunden */}
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Markt & Kunden</h3>
|
|
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
|
mb.criticalSectorClients ? 'border-red-400 bg-red-50' : 'border-gray-200 hover:border-gray-300'
|
|
}`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={mb.criticalSectorClients}
|
|
onChange={e => updateMB({ criticalSectorClients: e.target.checked })}
|
|
className="mt-1 w-5 h-5 text-red-600 rounded focus:ring-red-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900 text-sm">Liefert an KRITIS-Betreiber</div>
|
|
<div className="text-xs text-gray-500">Kunden in kritischer Infrastruktur</div>
|
|
</div>
|
|
</label>
|
|
|
|
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
|
mb.oemClients ? 'border-purple-500 bg-purple-50' : 'border-gray-200 hover:border-gray-300'
|
|
}`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={mb.oemClients}
|
|
onChange={e => updateMB({ oemClients: e.target.checked })}
|
|
className="mt-1 w-5 h-5 text-purple-600 rounded focus:ring-purple-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900 text-sm">OEM-Zulieferer</div>
|
|
<div className="text-xs text-gray-500">Liefern Komponenten an andere Hersteller</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
{mb.criticalSectorClients && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
Kritische Sektoren Ihrer Kunden
|
|
</label>
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
|
{Object.entries(CRITICAL_SECTOR_LABELS).map(([value, label]) => (
|
|
<button
|
|
key={value}
|
|
type="button"
|
|
onClick={() => toggleCriticalSector(value as CriticalSector)}
|
|
className={`px-3 py-2 rounded-lg border text-sm transition-all ${
|
|
mb.criticalSectors.includes(value as CriticalSector)
|
|
? 'border-red-400 bg-red-50 text-red-700'
|
|
: 'border-gray-200 hover:border-gray-300 text-gray-700'
|
|
}`}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
|
mb.ceMarkingRequired ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300'
|
|
}`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={mb.ceMarkingRequired}
|
|
onChange={e => updateMB({ ceMarkingRequired: e.target.checked })}
|
|
className="mt-1 w-5 h-5 text-blue-600 rounded focus:ring-blue-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900 text-sm">CE-Kennzeichnung erforderlich</div>
|
|
<div className="text-xs text-gray-500">Produkt benoetigt CE-Zertifizierung</div>
|
|
</div>
|
|
</label>
|
|
|
|
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
|
mb.existingCEProcess ? 'border-green-400 bg-green-50' : 'border-gray-200 hover:border-gray-300'
|
|
}`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={mb.existingCEProcess}
|
|
onChange={e => updateMB({ existingCEProcess: e.target.checked })}
|
|
className="mt-1 w-5 h-5 text-green-600 rounded focus:ring-green-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900 text-sm">Bestehender CE-Prozess</div>
|
|
<div className="text-xs text-gray-500">Bereits ein CE-Verfahren etabliert</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
{mb.ceMarkingRequired && (
|
|
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
|
mb.hasRiskAssessment ? 'border-green-400 bg-green-50' : 'border-red-400 bg-red-50'
|
|
}`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={mb.hasRiskAssessment}
|
|
onChange={e => updateMB({ hasRiskAssessment: e.target.checked })}
|
|
className="mt-1 w-5 h-5 text-green-600 rounded focus:ring-green-500"
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-gray-900 text-sm">Bestehende Risikobeurteilung</div>
|
|
<div className="text-xs text-gray-500">
|
|
{mb.hasRiskAssessment
|
|
? 'Risikobeurteilung vorhanden'
|
|
: 'Keine bestehende Risikobeurteilung - IACE hilft Ihnen dabei!'}
|
|
</div>
|
|
</div>
|
|
</label>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// COVERAGE ASSESSMENT COMPONENT
|
|
// =============================================================================
|
|
|
|
function CoverageAssessmentPanel({ profile }: { profile: Partial<CompanyProfile> }) {
|
|
const assessment = assessSDKCoverage(profile)
|
|
|
|
return (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">SDK-Abdeckung</h3>
|
|
|
|
{/* Status */}
|
|
<div
|
|
className={`p-4 rounded-lg mb-4 ${
|
|
assessment.isFullyCovered
|
|
? 'bg-green-50 border border-green-200'
|
|
: 'bg-amber-50 border border-amber-200'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
{assessment.isFullyCovered ? (
|
|
<>
|
|
<svg
|
|
className="w-5 h-5 text-green-600"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
<span className="font-medium text-green-800">Vollständig durch SDK abgedeckt</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg
|
|
className="w-5 h-5 text-amber-600"
|
|
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>
|
|
<span className="font-medium text-amber-800">Teilweise Einschränkungen</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Covered Regulations */}
|
|
{assessment.coveredRegulations.length > 0 && (
|
|
<div className="mb-4">
|
|
<div className="text-sm font-medium text-gray-700 mb-2">Abgedeckte Regulierungen</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{assessment.coveredRegulations.map(reg => (
|
|
<span key={reg} className="px-2 py-1 bg-green-100 text-green-700 text-sm rounded-full">
|
|
{reg}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Not Covered */}
|
|
{assessment.notCoveredRegulations.length > 0 && (
|
|
<div className="mb-4">
|
|
<div className="text-sm font-medium text-gray-700 mb-2">Nicht abgedeckt</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{assessment.notCoveredRegulations.map(reg => (
|
|
<span key={reg} className="px-2 py-1 bg-red-100 text-red-700 text-sm rounded-full">
|
|
{reg}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Recommendations */}
|
|
{assessment.recommendations.length > 0 && (
|
|
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
|
|
<div className="text-sm font-medium text-blue-800 mb-2">Empfehlungen</div>
|
|
<ul className="text-sm text-blue-700 space-y-1">
|
|
{assessment.recommendations.map((rec, i) => (
|
|
<li key={i} className="flex items-start gap-2">
|
|
<span>•</span>
|
|
<span>{rec}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{/* Legal Counsel Warning */}
|
|
{assessment.requiresLegalCounsel && (
|
|
<div className="mt-4 p-4 bg-amber-50 border border-amber-200 rounded-lg">
|
|
<div className="flex items-start gap-3">
|
|
<svg
|
|
className="w-5 h-5 text-amber-600 mt-0.5"
|
|
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>
|
|
<div className="font-medium text-amber-800">Rechtsberatung empfohlen</div>
|
|
<div className="text-sm text-amber-700 mt-1">
|
|
Basierend auf Ihrem Profil empfehlen wir die Konsultation eines spezialisierten
|
|
Rechtsanwalts für Bereiche, die über den Scope dieses SDKs hinausgehen.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN COMPONENT
|
|
// =============================================================================
|
|
|
|
export default function CompanyProfilePage() {
|
|
const { state, dispatch, setCompanyProfile, goToNextStep } = useSDK()
|
|
const [currentStep, setCurrentStep] = useState(1)
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
|
const [isDeleting, setIsDeleting] = useState(false)
|
|
const [formData, setFormData] = useState<Partial<CompanyProfile>>({
|
|
companyName: '',
|
|
legalForm: undefined,
|
|
industry: '',
|
|
foundedYear: null,
|
|
businessModel: undefined,
|
|
offerings: [],
|
|
companySize: undefined,
|
|
employeeCount: '',
|
|
annualRevenue: '',
|
|
headquartersCountry: 'DE',
|
|
headquartersCity: '',
|
|
hasInternationalLocations: false,
|
|
internationalCountries: [],
|
|
targetMarkets: [],
|
|
primaryJurisdiction: 'DE',
|
|
isDataController: true,
|
|
isDataProcessor: false,
|
|
usesAI: false,
|
|
aiUseCases: [],
|
|
dpoName: null,
|
|
dpoEmail: null,
|
|
legalContactName: null,
|
|
legalContactEmail: null,
|
|
isComplete: false,
|
|
completedAt: null,
|
|
})
|
|
|
|
const showMachineBuilderStep = isMachineBuilderIndustry(formData.industry || '')
|
|
const wizardSteps = getWizardSteps(formData.industry || '')
|
|
const totalSteps = wizardSteps.length
|
|
const lastStep = wizardSteps[wizardSteps.length - 1].id
|
|
|
|
// Load existing profile: first try backend, then SDK state as fallback
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
|
|
async function loadFromBackend() {
|
|
try {
|
|
const response = await fetch('/api/sdk/v1/company-profile?tenant_id=default')
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
if (data && !cancelled) {
|
|
const backendProfile: Partial<CompanyProfile> = {
|
|
companyName: data.company_name || '',
|
|
legalForm: data.legal_form || undefined,
|
|
industry: data.industry || '',
|
|
foundedYear: data.founded_year || undefined,
|
|
businessModel: data.business_model || undefined,
|
|
offerings: data.offerings || [],
|
|
companySize: data.company_size || undefined,
|
|
employeeCount: data.employee_count || '',
|
|
annualRevenue: data.annual_revenue || '',
|
|
headquartersCountry: data.headquarters_country || 'DE',
|
|
headquartersCity: data.headquarters_city || '',
|
|
hasInternationalLocations: data.has_international_locations || false,
|
|
internationalCountries: data.international_countries || [],
|
|
targetMarkets: data.target_markets || [],
|
|
primaryJurisdiction: data.primary_jurisdiction || 'DE',
|
|
isDataController: data.is_data_controller ?? true,
|
|
isDataProcessor: data.is_data_processor ?? false,
|
|
usesAI: data.uses_ai ?? false,
|
|
aiUseCases: data.ai_use_cases || [],
|
|
dpoName: data.dpo_name || '',
|
|
dpoEmail: data.dpo_email || '',
|
|
isComplete: data.is_complete || false,
|
|
}
|
|
setFormData(backendProfile)
|
|
if (backendProfile.isComplete) {
|
|
setCurrentStep(5)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
} catch {
|
|
// Backend not available, fall through to SDK state
|
|
}
|
|
|
|
// Fallback: use SDK state
|
|
if (!cancelled && state.companyProfile) {
|
|
setFormData(state.companyProfile)
|
|
if (state.companyProfile.isComplete) {
|
|
setCurrentStep(5)
|
|
}
|
|
}
|
|
}
|
|
|
|
loadFromBackend()
|
|
|
|
return () => { cancelled = true }
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
|
|
const updateFormData = (updates: Partial<CompanyProfile>) => {
|
|
setFormData(prev => ({ ...prev, ...updates }))
|
|
}
|
|
|
|
const handleNext = () => {
|
|
if (currentStep < lastStep) {
|
|
// Skip step 6 if not a machine builder
|
|
const nextStep = currentStep + 1
|
|
if (nextStep === 6 && !showMachineBuilderStep) {
|
|
// Complete profile (was step 5, last step for non-machine-builders)
|
|
completeAndSaveProfile()
|
|
return
|
|
}
|
|
setCurrentStep(nextStep)
|
|
} else {
|
|
// Complete profile
|
|
completeAndSaveProfile()
|
|
}
|
|
}
|
|
|
|
const completeAndSaveProfile = async () => {
|
|
const completeProfile: CompanyProfile = {
|
|
...formData,
|
|
isComplete: true,
|
|
completedAt: new Date(),
|
|
} as CompanyProfile
|
|
|
|
setCompanyProfile(completeProfile)
|
|
dispatch({ type: 'COMPLETE_STEP', payload: 'company-profile' })
|
|
|
|
// Also persist to dedicated backend endpoint
|
|
try {
|
|
await fetch('/api/sdk/v1/company-profile', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
company_name: formData.companyName || '',
|
|
legal_form: formData.legalForm || 'GmbH',
|
|
industry: formData.industry || '',
|
|
founded_year: formData.foundedYear || null,
|
|
business_model: formData.businessModel || 'B2B',
|
|
offerings: formData.offerings || [],
|
|
company_size: formData.companySize || 'small',
|
|
employee_count: formData.employeeCount || '',
|
|
annual_revenue: formData.annualRevenue || '',
|
|
headquarters_country: formData.headquartersCountry || 'DE',
|
|
headquarters_city: formData.headquartersCity || '',
|
|
has_international_locations: formData.hasInternationalLocations || false,
|
|
international_countries: formData.internationalCountries || [],
|
|
target_markets: formData.targetMarkets || [],
|
|
primary_jurisdiction: formData.primaryJurisdiction || 'DE',
|
|
is_data_controller: formData.isDataController ?? true,
|
|
is_data_processor: formData.isDataProcessor ?? false,
|
|
uses_ai: formData.usesAI ?? false,
|
|
ai_use_cases: formData.aiUseCases || [],
|
|
dpo_name: formData.dpoName || '',
|
|
dpo_email: formData.dpoEmail || '',
|
|
is_complete: true,
|
|
// Machine builder fields (if applicable)
|
|
...(formData.machineBuilder ? {
|
|
machine_builder: {
|
|
product_types: formData.machineBuilder.productTypes || [],
|
|
product_description: formData.machineBuilder.productDescription || '',
|
|
product_pride: formData.machineBuilder.productPride || '',
|
|
contains_software: formData.machineBuilder.containsSoftware || false,
|
|
contains_firmware: formData.machineBuilder.containsFirmware || false,
|
|
contains_ai: formData.machineBuilder.containsAI || false,
|
|
ai_integration_type: formData.machineBuilder.aiIntegrationType || [],
|
|
has_safety_function: formData.machineBuilder.hasSafetyFunction || false,
|
|
safety_function_description: formData.machineBuilder.safetyFunctionDescription || '',
|
|
autonomous_behavior: formData.machineBuilder.autonomousBehavior || false,
|
|
human_oversight_level: formData.machineBuilder.humanOversightLevel || 'full',
|
|
is_networked: formData.machineBuilder.isNetworked || false,
|
|
has_remote_access: formData.machineBuilder.hasRemoteAccess || false,
|
|
has_ota_updates: formData.machineBuilder.hasOTAUpdates || false,
|
|
update_mechanism: formData.machineBuilder.updateMechanism || '',
|
|
export_markets: formData.machineBuilder.exportMarkets || [],
|
|
critical_sector_clients: formData.machineBuilder.criticalSectorClients || false,
|
|
critical_sectors: formData.machineBuilder.criticalSectors || [],
|
|
oem_clients: formData.machineBuilder.oemClients || false,
|
|
ce_marking_required: formData.machineBuilder.ceMarkingRequired || false,
|
|
existing_ce_process: formData.machineBuilder.existingCEProcess || false,
|
|
has_risk_assessment: formData.machineBuilder.hasRiskAssessment || false,
|
|
},
|
|
} : {}),
|
|
}),
|
|
})
|
|
} catch (err) {
|
|
console.error('Failed to save company profile to backend:', err)
|
|
}
|
|
|
|
goToNextStep()
|
|
}
|
|
|
|
const handleBack = () => {
|
|
if (currentStep > 1) {
|
|
setCurrentStep(prev => prev - 1)
|
|
}
|
|
}
|
|
|
|
const handleDeleteProfile = async () => {
|
|
setIsDeleting(true)
|
|
try {
|
|
const response = await fetch('/api/sdk/v1/company-profile?tenant_id=default', {
|
|
method: 'DELETE',
|
|
})
|
|
if (response.ok) {
|
|
// Reset form and SDK state
|
|
setFormData({
|
|
companyName: '',
|
|
legalForm: undefined,
|
|
industry: '',
|
|
foundedYear: null,
|
|
businessModel: undefined,
|
|
offerings: [],
|
|
companySize: undefined,
|
|
employeeCount: '',
|
|
annualRevenue: '',
|
|
headquartersCountry: 'DE',
|
|
headquartersCity: '',
|
|
hasInternationalLocations: false,
|
|
internationalCountries: [],
|
|
targetMarkets: [],
|
|
primaryJurisdiction: 'DE',
|
|
isDataController: true,
|
|
isDataProcessor: false,
|
|
usesAI: false,
|
|
aiUseCases: [],
|
|
dpoName: null,
|
|
dpoEmail: null,
|
|
legalContactName: null,
|
|
legalContactEmail: null,
|
|
isComplete: false,
|
|
completedAt: null,
|
|
})
|
|
setCurrentStep(1)
|
|
dispatch({ type: 'SET_STATE', payload: { companyProfile: undefined } })
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to delete company profile:', err)
|
|
} finally {
|
|
setIsDeleting(false)
|
|
setShowDeleteConfirm(false)
|
|
}
|
|
}
|
|
|
|
const canProceed = () => {
|
|
switch (currentStep) {
|
|
case 1:
|
|
return formData.companyName && formData.legalForm
|
|
case 2:
|
|
return formData.businessModel && (formData.offerings?.length || 0) > 0
|
|
case 3:
|
|
return formData.companySize
|
|
case 4:
|
|
return formData.headquartersCountry && (formData.targetMarkets?.length || 0) > 0
|
|
case 5:
|
|
return true
|
|
case 6:
|
|
// Machine builder step: require at least product description
|
|
return (formData.machineBuilder?.productDescription?.length || 0) > 0
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
const isLastStep = currentStep === lastStep || (currentStep === 5 && !showMachineBuilderStep)
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 py-8">
|
|
<div className="max-w-6xl mx-auto px-4">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-gray-900">Unternehmensprofil</h1>
|
|
<p className="text-gray-600 mt-2">
|
|
Helfen Sie uns, Ihr Unternehmen zu verstehen, damit wir die relevanten Regulierungen
|
|
identifizieren können.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Progress Steps */}
|
|
<div className="mb-8">
|
|
<div className="flex items-center justify-between">
|
|
{wizardSteps.map((step, index) => (
|
|
<React.Fragment key={step.id}>
|
|
<div className="flex items-center">
|
|
<div
|
|
className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium ${
|
|
step.id < currentStep
|
|
? 'bg-purple-600 text-white'
|
|
: step.id === currentStep
|
|
? 'bg-purple-100 text-purple-600 border-2 border-purple-600'
|
|
: 'bg-gray-100 text-gray-400'
|
|
}`}
|
|
>
|
|
{step.id < currentStep ? (
|
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
) : (
|
|
step.id
|
|
)}
|
|
</div>
|
|
<div className="ml-3 hidden sm:block">
|
|
<div
|
|
className={`text-sm font-medium ${
|
|
step.id <= currentStep ? 'text-gray-900' : 'text-gray-400'
|
|
}`}
|
|
>
|
|
{step.name}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{index < wizardSteps.length - 1 && (
|
|
<div
|
|
className={`flex-1 h-0.5 mx-4 ${
|
|
step.id < currentStep ? 'bg-purple-600' : 'bg-gray-200'
|
|
}`}
|
|
/>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
{/* Form */}
|
|
<div className="lg:col-span-2">
|
|
<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">
|
|
{(wizardSteps.find(s => s.id === currentStep) || wizardSteps[0]).name}
|
|
</h2>
|
|
<p className="text-gray-500">{(wizardSteps.find(s => s.id === currentStep) || wizardSteps[0]).description}</p>
|
|
</div>
|
|
|
|
{currentStep === 1 && <StepBasicInfo data={formData} onChange={updateFormData} />}
|
|
{currentStep === 2 && <StepBusinessModel data={formData} onChange={updateFormData} />}
|
|
{currentStep === 3 && <StepCompanySize data={formData} onChange={updateFormData} />}
|
|
{currentStep === 4 && <StepLocations data={formData} onChange={updateFormData} />}
|
|
{currentStep === 5 && <StepDataProtection data={formData} onChange={updateFormData} />}
|
|
{currentStep === 6 && showMachineBuilderStep && <StepMachineBuilder data={formData} onChange={updateFormData} />}
|
|
|
|
{/* Navigation */}
|
|
<div className="flex justify-between mt-8 pt-6 border-t border-gray-200">
|
|
<button
|
|
onClick={handleBack}
|
|
disabled={currentStep === 1}
|
|
className="px-6 py-3 text-gray-600 hover:text-gray-900 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Zurück
|
|
</button>
|
|
<button
|
|
onClick={handleNext}
|
|
disabled={!canProceed()}
|
|
className="px-8 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{isLastStep ? 'Profil speichern & weiter' : 'Weiter'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Delete Confirmation Modal */}
|
|
{showDeleteConfirm && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
|
<div className="bg-white rounded-xl p-6 w-full max-w-md shadow-2xl">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Profil löschen?</h3>
|
|
<p className="text-sm text-gray-600 mb-6">
|
|
Alle gespeicherten Unternehmensdaten werden unwiderruflich gelöscht (DSGVO Art. 17).
|
|
Diese Aktion kann nicht rückgängig gemacht werden.
|
|
</p>
|
|
<div className="flex justify-end gap-3">
|
|
<button
|
|
onClick={() => setShowDeleteConfirm(false)}
|
|
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onClick={handleDeleteProfile}
|
|
disabled={isDeleting}
|
|
className="px-4 py-2 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50"
|
|
>
|
|
{isDeleting ? 'Lösche...' : 'Endgültig löschen'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Sidebar: Coverage Assessment */}
|
|
<div className="lg:col-span-1">
|
|
<CoverageAssessmentPanel profile={formData} />
|
|
|
|
{/* Info Box */}
|
|
<div className="mt-6 bg-blue-50 rounded-xl border border-blue-200 p-6">
|
|
<div className="flex items-start gap-3">
|
|
<svg
|
|
className="w-5 h-5 text-blue-600 mt-0.5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
<div>
|
|
<div className="font-medium text-blue-800">Warum diese Fragen?</div>
|
|
<div className="text-sm text-blue-700 mt-1">
|
|
Diese Informationen helfen uns, die für Ihr Unternehmen relevanten Regulierungen
|
|
zu identifizieren und ehrlich zu kommunizieren, wo unsere Grenzen liegen.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Delete Profile Button */}
|
|
{formData.companyName && (
|
|
<div className="mt-6">
|
|
<button
|
|
onClick={() => setShowDeleteConfirm(true)}
|
|
className="w-full px-4 py-2 text-sm text-red-600 border border-red-200 rounded-lg hover:bg-red-50 transition-colors"
|
|
>
|
|
Profil löschen (Art. 17 DSGVO)
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|