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>
144 lines
8.4 KiB
TypeScript
144 lines
8.4 KiB
TypeScript
'use client'
|
|
|
|
import { CompanyProfile } from '@/lib/sdk/types'
|
|
import { CertificationEntry } from './types'
|
|
import { CERTIFICATIONS } from './constants'
|
|
|
|
export function StepLegalFramework({
|
|
data,
|
|
onChange,
|
|
}: {
|
|
data: Partial<CompanyProfile>
|
|
onChange: (updates: Record<string, unknown>) => void
|
|
}) {
|
|
const contacts = (data as any).technicalContacts || []
|
|
const existingCerts: CertificationEntry[] = (data as any).existingCertifications || []
|
|
const targetCerts: string[] = (data as any).targetCertifications || []
|
|
const targetCertOther: string = (data as any).targetCertificationOther || ''
|
|
|
|
const toggleExistingCert = (certId: string) => {
|
|
const exists = existingCerts.find((c: CertificationEntry) => c.certId === certId)
|
|
if (exists) {
|
|
onChange({ existingCertifications: existingCerts.filter((c: CertificationEntry) => c.certId !== certId) })
|
|
} else {
|
|
onChange({ existingCertifications: [...existingCerts, { certId }] })
|
|
}
|
|
}
|
|
|
|
const updateExistingCert = (certId: string, updates: Partial<CertificationEntry>) => {
|
|
onChange({ existingCertifications: existingCerts.map((c: CertificationEntry) => c.certId === certId ? { ...c, ...updates } : c) })
|
|
}
|
|
|
|
const toggleTargetCert = (certId: string) => {
|
|
if (targetCerts.includes(certId)) {
|
|
onChange({ targetCertifications: targetCerts.filter((c: string) => c !== certId) })
|
|
} else {
|
|
onChange({ targetCertifications: [...targetCerts, certId] })
|
|
}
|
|
}
|
|
|
|
const addContact = () => { onChange({ technicalContacts: [...contacts, { name: '', role: '', email: '' }] }) }
|
|
const removeContact = (i: number) => { onChange({ technicalContacts: contacts.filter((_: { name: string; role: string; email: string }, idx: number) => idx !== i) }) }
|
|
const updateContact = (i: number, updates: Partial<{ name: string; role: string; email: string }>) => {
|
|
const updated = [...contacts]
|
|
updated[i] = { ...updated[i], ...updates }
|
|
onChange({ technicalContacts: updated })
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Bestehende Zertifizierungen */}
|
|
<div>
|
|
<h3 className="text-sm font-medium text-gray-700 mb-1">Bestehende Zertifizierungen</h3>
|
|
<p className="text-sm text-gray-500 mb-3">Ueber welche Zertifizierungen verfuegt Ihr Unternehmen aktuell? Mehrfachauswahl moeglich.</p>
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
|
{CERTIFICATIONS.map(cert => {
|
|
const selected = existingCerts.some((c: CertificationEntry) => c.certId === cert.id)
|
|
return (
|
|
<button key={cert.id} type="button" onClick={() => toggleExistingCert(cert.id)}
|
|
className={`p-3 rounded-lg border-2 text-left transition-all ${selected ? 'border-purple-500 bg-purple-50 text-purple-700' : 'border-gray-200 hover:border-purple-300 text-gray-700'}`}>
|
|
<div className="font-medium text-sm">{cert.label}</div>
|
|
<div className="text-xs text-gray-500 mt-0.5">{cert.desc}</div>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{existingCerts.length > 0 && (
|
|
<div className="mt-4 space-y-3">
|
|
{existingCerts.map((entry: CertificationEntry) => {
|
|
const cert = CERTIFICATIONS.find(c => c.id === entry.certId)
|
|
const label = cert?.label || entry.certId
|
|
return (
|
|
<div key={entry.certId} className="p-4 bg-purple-50 border border-purple-200 rounded-lg">
|
|
<div className="font-medium text-sm text-purple-800 mb-2">
|
|
{entry.certId === 'other' ? 'Sonstige Zertifizierung' : label}
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
{entry.certId === 'other' && (
|
|
<input type="text" value={entry.customName || ''} onChange={e => updateExistingCert(entry.certId, { customName: e.target.value })} placeholder="Name der Zertifizierung" className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
|
|
)}
|
|
<input type="text" value={entry.certifier || ''} onChange={e => updateExistingCert(entry.certId, { certifier: e.target.value })} placeholder="Zertifizierer (z.B. T\u00DCV, DEKRA)" className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
|
|
<input type="date" value={entry.lastDate || ''} onChange={e => updateExistingCert(entry.certId, { lastDate: e.target.value })} title="Datum der letzten Zertifizierung" className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Angestrebte Zertifizierungen */}
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<h3 className="text-sm font-medium text-gray-700 mb-1">Streben Sie eine Zertifizierung an?</h3>
|
|
<p className="text-sm text-gray-500 mb-3">Welche Zertifizierungen planen Sie? Mehrfachauswahl moeglich.</p>
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
|
{CERTIFICATIONS.map(cert => {
|
|
const selected = targetCerts.includes(cert.id)
|
|
const alreadyHas = existingCerts.some((c: CertificationEntry) => c.certId === cert.id)
|
|
return (
|
|
<button key={cert.id} type="button" onClick={() => !alreadyHas && toggleTargetCert(cert.id)} disabled={alreadyHas}
|
|
className={`p-3 rounded-lg border-2 text-left transition-all ${alreadyHas ? 'border-gray-100 bg-gray-50 text-gray-400 cursor-not-allowed' : selected ? 'border-green-500 bg-green-50 text-green-700' : 'border-gray-200 hover:border-green-300 text-gray-700'}`}>
|
|
<div className="font-medium text-sm">{cert.label}</div>
|
|
{alreadyHas && <div className="text-xs mt-0.5">Bereits vorhanden</div>}
|
|
{!alreadyHas && <div className="text-xs text-gray-500 mt-0.5">{cert.desc}</div>}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
{targetCerts.includes('other') && (
|
|
<div className="mt-3">
|
|
<input type="text" value={targetCertOther} onChange={e => onChange({ targetCertificationOther: e.target.value })} placeholder="Name der angestrebten Zertifizierung" 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>
|
|
|
|
{/* Technical Contacts */}
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h3 className="text-sm font-medium text-gray-700">Technische Ansprechpartner</h3>
|
|
<p className="text-xs text-gray-500">CISO, IT-Manager, DSB etc.</p>
|
|
</div>
|
|
<button type="button" onClick={addContact} className="px-3 py-1.5 text-sm bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200">
|
|
+ Kontakt
|
|
</button>
|
|
</div>
|
|
{contacts.length === 0 && (
|
|
<div className="text-center py-4 text-gray-400 border-2 border-dashed rounded-lg text-sm">Noch keine Kontakte</div>
|
|
)}
|
|
<div className="space-y-3">
|
|
{contacts.map((c: { name: string; role: string; email: string }, i: number) => (
|
|
<div key={i} className="flex gap-3 items-center">
|
|
<input type="text" value={c.name} onChange={e => updateContact(i, { name: e.target.value })} placeholder="Name" className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
|
|
<input type="text" value={c.role} onChange={e => updateContact(i, { role: e.target.value })} placeholder="Rolle (z.B. CISO)" className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
|
|
<input type="email" value={c.email} onChange={e => updateContact(i, { email: e.target.value })} placeholder="E-Mail" className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
|
|
<button type="button" onClick={() => removeContact(i)} className="text-red-400 hover:text-red-600 text-sm">X</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|