Files
breakpilot-compliance/admin-compliance/app/sdk/company-profile/_components/StepMachineBuilder.tsx
Sharang Parnerkar f7b77fd504 refactor(admin): split company-profile page.tsx (3017 LOC) into colocated components
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>
2026-04-11 18:50:30 +02:00

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>
)
}