Extract the monolithic company-profile wizard into _components/ and _hooks/ following Next.js 15 conventions from AGENTS.typescript.md: - _components/constants.ts: wizard steps, legal forms, industries, certifications - _components/types.ts: local interfaces (ProcessingActivity, AISystem, etc.) - _components/activity-data.ts: DSGVO data categories, department/activity templates - _components/ai-system-data.ts: AI system template catalog - _components/StepBasicInfo.tsx: step 1 (company name, legal form, industry) - _components/StepBusinessModel.tsx: step 2 (B2B/B2C, offerings) - _components/StepCompanySize.tsx: step 3 (size, revenue) - _components/StepLocations.tsx: step 4 (headquarters, target markets) - _components/StepDataProtection.tsx: step 5 (DSGVO roles, DPO) - _components/StepProcessing.tsx: processing activities with category checkboxes - _components/StepAISystems.tsx: AI system inventory - _components/StepLegalFramework.tsx: certifications and contacts - _components/StepMachineBuilder.tsx: machine builder profile (step 7) - _components/ProfileSummary.tsx: completion summary view - _hooks/useCompanyProfileForm.ts: form state, auto-save, navigation logic - page.tsx: thin orchestrator (160 LOC), imports and composes sections All 16 files are under 500 LOC (largest: StepProcessing at 343). Build verified: npx next build passes cleanly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
254 lines
15 KiB
TypeScript
254 lines
15 KiB
TypeScript
'use client'
|
|
|
|
import {
|
|
CompanyProfile,
|
|
MachineBuilderProfile,
|
|
MachineProductType,
|
|
AIIntegrationType,
|
|
HumanOversightLevel,
|
|
CriticalSector,
|
|
MACHINE_PRODUCT_TYPE_LABELS,
|
|
AI_INTEGRATION_TYPE_LABELS,
|
|
HUMAN_OVERSIGHT_LABELS,
|
|
CRITICAL_SECTOR_LABELS,
|
|
} from '@/lib/sdk/types'
|
|
|
|
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,
|
|
}
|
|
|
|
export 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 || []
|
|
updateMB({ productTypes: current.includes(type) ? current.filter(t => t !== type) : [...current, type] })
|
|
}
|
|
|
|
const toggleAIType = (type: AIIntegrationType) => {
|
|
const current = mb.aiIntegrationType || []
|
|
updateMB({ aiIntegrationType: current.includes(type) ? current.filter(t => t !== type) : [...current, type] })
|
|
}
|
|
|
|
const toggleCriticalSector = (sector: CriticalSector) => {
|
|
const current = mb.criticalSectors || []
|
|
updateMB({ criticalSectors: current.includes(sector) ? current.filter(s => s !== sector) : [...current, sector] })
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Block 1: Product description */}
|
|
<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>
|
|
)
|
|
}
|