diff --git a/admin-compliance/app/sdk/company-profile/_components/ProfileSummary.tsx b/admin-compliance/app/sdk/company-profile/_components/ProfileSummary.tsx new file mode 100644 index 0000000..35e3011 --- /dev/null +++ b/admin-compliance/app/sdk/company-profile/_components/ProfileSummary.tsx @@ -0,0 +1,94 @@ +'use client' + +import { CompanyProfile, BUSINESS_MODEL_LABELS, COMPANY_SIZE_LABELS, TARGET_MARKET_LABELS } from '@/lib/sdk/types' +import { LEGAL_FORM_LABELS } from './constants' + +export function ProfileSummary({ + formData, + onEdit, + onContinue, +}: { + formData: Partial + onEdit: () => void + onContinue: () => void +}) { + const summaryItems = [ + { label: 'Firmenname', value: formData.companyName }, + { label: 'Rechtsform', value: formData.legalForm ? LEGAL_FORM_LABELS[formData.legalForm] : undefined }, + { label: 'Branche', value: formData.industry?.join(', ') }, + { label: 'Geschaeftsmodell', value: formData.businessModel ? BUSINESS_MODEL_LABELS[formData.businessModel]?.short : undefined }, + { label: 'Unternehmensgroesse', value: formData.companySize ? COMPANY_SIZE_LABELS[formData.companySize] : undefined }, + { label: 'Mitarbeiter', value: formData.employeeCount }, + { label: 'Hauptsitz', value: [formData.headquartersZip, formData.headquartersCity, formData.headquartersCountry === 'DE' ? 'Deutschland' : formData.headquartersCountry].filter(Boolean).join(', ') }, + { label: 'Zielmaerkte', value: formData.targetMarkets?.map(m => TARGET_MARKET_LABELS[m] || m).join(', ') }, + { label: 'Verantwortlicher', value: formData.isDataController ? 'Ja' : 'Nein' }, + { label: 'Auftragsverarbeiter', value: formData.isDataProcessor ? 'Ja' : 'Nein' }, + { label: 'DSB', value: formData.dpoName || 'Nicht angegeben' }, + ].filter(item => item.value && item.value.length > 0) + + const missingFields: string[] = [] + if (!formData.companyName) missingFields.push('Firmenname') + if (!formData.legalForm) missingFields.push('Rechtsform') + if (!formData.industry || formData.industry.length === 0) missingFields.push('Branche') + if (!formData.businessModel) missingFields.push('Geschaeftsmodell') + if (!formData.companySize) missingFields.push('Unternehmensgroesse') + + return ( +
+
+
+

Unternehmensprofil

+
+ + {/* Success Banner */} +
+
+
+ {formData.isComplete ? '\u2713' : '!'} +
+
+

+ {formData.isComplete ? 'Profil erfolgreich abgeschlossen' : 'Profil unvollstaendig'} +

+

+ {formData.isComplete + ? 'Alle Angaben wurden gespeichert. Sie koennen jetzt mit der Scope-Analyse fortfahren.' + : `Es fehlen noch Angaben: ${missingFields.join(', ')}.`} +

+
+
+
+ + {/* Profile Summary */} +
+

Zusammenfassung

+
+ {summaryItems.map(item => ( +
+ {item.label} + {item.value} +
+ ))} +
+
+ + {/* Actions */} +
+ + + {formData.isComplete ? ( + + ) : ( + + )} +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/company-profile/_components/StepAISystems.tsx b/admin-compliance/app/sdk/company-profile/_components/StepAISystems.tsx new file mode 100644 index 0000000..22f669c --- /dev/null +++ b/admin-compliance/app/sdk/company-profile/_components/StepAISystems.tsx @@ -0,0 +1,207 @@ +'use client' + +import { useState } from 'react' +import { CompanyProfile } from '@/lib/sdk/types' +import { AISystem, AISystemTemplate } from './types' +import { AI_SYSTEM_TEMPLATES } from './ai-system-data' + +export function StepAISystems({ + data, + onChange, +}: { + data: Partial & { aiSystems?: AISystem[] } + onChange: (updates: Record) => void +}) { + const aiSystems: AISystem[] = (data as any).aiSystems || [] + const [expandedSystem, setExpandedSystem] = useState(null) + const [collapsedCategories, setCollapsedCategories] = useState>(new Set()) + + const activeIds = new Set(aiSystems.map(a => a.id)) + + const toggleTemplateSystem = (template: AISystemTemplate) => { + if (activeIds.has(template.id)) { + onChange({ aiSystems: aiSystems.filter(a => a.id !== template.id) }) + if (expandedSystem === template.id) setExpandedSystem(null) + } else { + const newSystem: AISystem = { + id: template.id, name: template.name, vendor: template.vendor, + purpose: template.typicalPurposes.join(', '), purposes: [], + processes_personal_data: template.processes_personal_data_likely, isCustom: false, + } + onChange({ aiSystems: [...aiSystems, newSystem] }) + setExpandedSystem(template.id) + } + } + + const updateAISystem = (id: string, updates: Partial) => { + onChange({ aiSystems: aiSystems.map(a => a.id === id ? { ...a, ...updates } : a) }) + } + + const togglePurpose = (systemId: string, purpose: string) => { + const system = aiSystems.find(a => a.id === systemId) + if (!system) return + const purposes = system.purposes || [] + const updated = purposes.includes(purpose) ? purposes.filter(p => p !== purpose) : [...purposes, purpose] + updateAISystem(systemId, { purposes: updated, purpose: updated.join(', ') }) + } + + const addCustomSystem = () => { + const id = `custom_ai_${Date.now()}` + onChange({ aiSystems: [...aiSystems, { id, name: '', vendor: '', purpose: '', processes_personal_data: false, isCustom: true }] }) + setExpandedSystem(id) + } + + const removeSystem = (id: string) => { + onChange({ aiSystems: aiSystems.filter(a => a.id !== id) }) + if (expandedSystem === id) setExpandedSystem(null) + } + + const toggleCategoryCollapse = (category: string) => { + setCollapsedCategories(prev => { const next = new Set(prev); if (next.has(category)) next.delete(category); else next.add(category); return next }) + } + + const categoryActiveCount = (systems: AISystemTemplate[]) => systems.filter(s => activeIds.has(s.id)).length + + return ( +
+
+

KI-Systeme im Einsatz

+

+ Waehlen Sie die KI-Systeme aus, die in Ihrem Unternehmen eingesetzt werden. Dies dient der Erfassung fuer den EU AI Act und die DSGVO-Dokumentation. +

+
+ +
+ {AI_SYSTEM_TEMPLATES.map(group => { + const isCollapsed = collapsedCategories.has(group.category) + const activeCount = categoryActiveCount(group.systems) + + return ( +
+ + + {!isCollapsed && ( +
+ {group.systems.map(template => { + const isActive = activeIds.has(template.id) + const system = aiSystems.find(a => a.id === template.id) + const isExpanded = expandedSystem === template.id + + return ( +
+
{ if (!isActive) { toggleTemplateSystem(template) } else { setExpandedSystem(isExpanded ? null : template.id) } }} + > + { e.stopPropagation(); toggleTemplateSystem(template) }} className="w-4 h-4 text-purple-600 rounded focus:ring-purple-500 flex-shrink-0" /> +
+
{template.name}
+

{template.vendor}

+
+ {isActive && ( + + + + )} +
+ + {isActive && isExpanded && system && ( +
+
+ +
+ {template.typicalPurposes.map(purpose => ( + + ))} +
+ updateAISystem(template.id, { notes: e.target.value })} placeholder="Weitere Einsatzzwecke / Anmerkungen..." className="mt-2 w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" /> +
+ + {template.dataWarning && ( +
+ {template.dataWarning.includes('EU') || template.dataWarning.includes('Deutscher Anbieter') ? '\u2139\uFE0F' : '\u26A0\uFE0F'} + {template.dataWarning} +
+ )} + + + + +
+ )} +
+ ) + })} +
+ )} +
+ ) + })} +
+ + {aiSystems.filter(a => a.isCustom).map(system => ( +
+
setExpandedSystem(expandedSystem === system.id ? null : system.id)}> + + +
+ {system.name || 'Neues KI-System'} + {system.vendor && ({system.vendor})} +
+ + + +
+ {expandedSystem === system.id && ( +
+
+ updateAISystem(system.id, { name: e.target.value })} placeholder="Name (z.B. ChatGPT, Copilot)" className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" /> + updateAISystem(system.id, { vendor: e.target.value })} placeholder="Anbieter (z.B. OpenAI, Microsoft)" className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" /> +
+ updateAISystem(system.id, { purpose: e.target.value })} placeholder="Einsatzzweck (z.B. Kundensupport, Code-Assistenz)" className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" /> + + +
+ )} +
+ ))} + + + +
+
+ {'\u2139\uFE0F'} +
+

AI Act Risikoeinstufung

+

+ Die detaillierte Risikoeinstufung Ihrer KI-Systeme nach EU AI Act (verboten / hochriskant / begrenzt / minimal) erfolgt automatisch im AI-Act-Modul. +

+ + Zum AI-Act-Modul + + + + +
+
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/company-profile/_components/StepBasicInfo.tsx b/admin-compliance/app/sdk/company-profile/_components/StepBasicInfo.tsx new file mode 100644 index 0000000..a63c511 --- /dev/null +++ b/admin-compliance/app/sdk/company-profile/_components/StepBasicInfo.tsx @@ -0,0 +1,107 @@ +'use client' + +import { CompanyProfile, LegalForm } from '@/lib/sdk/types' +import { INDUSTRIES, LEGAL_FORM_LABELS } from './constants' + +export function StepBasicInfo({ + data, + onChange, +}: { + data: Partial + onChange: (updates: Partial) => void +}) { + return ( +
+
+ + onChange({ companyName: e.target.value })} + placeholder="Ihre Firma (ohne Rechtsform)" + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> +
+ +
+ + +
+ +
+ +

Mehrfachauswahl moeglich

+
+ {INDUSTRIES.map(ind => { + const selected = (data.industry || []).includes(ind) + return ( + + ) + })} +
+ {(data.industry || []).includes('Sonstige') && ( +
+ onChange({ industryOther: e.target.value })} + placeholder="Ihre Branche eingeben..." + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> +
+ )} +
+ +
+ + { + const val = parseInt(e.target.value) + onChange({ foundedYear: isNaN(val) ? null : val }) + }} + onFocus={e => { + if (!data.foundedYear) onChange({ foundedYear: 2000 }) + }} + placeholder="2020" + min="1900" + 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" + /> +
+
+ ) +} diff --git a/admin-compliance/app/sdk/company-profile/_components/StepBusinessModel.tsx b/admin-compliance/app/sdk/company-profile/_components/StepBusinessModel.tsx new file mode 100644 index 0000000..ad9b61f --- /dev/null +++ b/admin-compliance/app/sdk/company-profile/_components/StepBusinessModel.tsx @@ -0,0 +1,124 @@ +'use client' + +import { + CompanyProfile, + BusinessModel, + OfferingType, + BUSINESS_MODEL_LABELS, + OFFERING_TYPE_LABELS, +} from '@/lib/sdk/types' +import { OFFERING_URL_CONFIG } from './constants' + +export function StepBusinessModel({ + data, + onChange, +}: { + data: Partial + onChange: (updates: Partial) => void +}) { + const toggleOffering = (offering: OfferingType) => { + const current = data.offerings || [] + if (current.includes(offering)) { + const urls = { ...(data.offeringUrls || {}) } + delete urls[offering] + onChange({ offerings: current.filter(o => o !== offering), offeringUrls: urls }) + } else { + onChange({ offerings: [...current, offering] }) + } + } + + const updateOfferingUrl = (offering: string, url: string) => { + onChange({ offeringUrls: { ...(data.offeringUrls || {}), [offering]: url } }) + } + + const selectedWithUrls = (data.offerings || []).filter(o => o in OFFERING_URL_CONFIG) + + return ( +
+
+ +
+ {Object.entries(BUSINESS_MODEL_LABELS).map(([value, { short }]) => ( + + ))} +
+ {data.businessModel && ( +

+ {BUSINESS_MODEL_LABELS[data.businessModel].description} +

+ )} +
+ +
+ +
+ {Object.entries(OFFERING_TYPE_LABELS).map(([value, { label, description }]) => ( + + ))} +
+ + {(data.offerings || []).includes('webshop') && (data.offerings || []).includes('software_saas') && ( +
+ + + +

+ Hinweis: Wenn Sie reine Software verkaufen, genuegt SaaS/CloudOnline-Shop ist nur fuer physische Produkte oder Hardware mit Abo-Modell gedacht. +

+
+ )} +
+ + {selectedWithUrls.length > 0 && ( +
+ + {selectedWithUrls.map(offering => { + const config = OFFERING_URL_CONFIG[offering]! + return ( +
+ + updateOfferingUrl(offering, e.target.value)} + placeholder={config.placeholder} + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> +

{config.hint}

+
+ ) + })} +
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/company-profile/_components/StepCompanySize.tsx b/admin-compliance/app/sdk/company-profile/_components/StepCompanySize.tsx new file mode 100644 index 0000000..567264f --- /dev/null +++ b/admin-compliance/app/sdk/company-profile/_components/StepCompanySize.tsx @@ -0,0 +1,68 @@ +'use client' + +import { CompanyProfile, CompanySize, COMPANY_SIZE_LABELS } from '@/lib/sdk/types' + +export function StepCompanySize({ + data, + onChange, +}: { + data: Partial + onChange: (updates: Partial) => void +}) { + return ( +
+
+ +
+ {Object.entries(COMPANY_SIZE_LABELS).map(([value, label]) => ( + + ))} +
+
+ +
+ +
+ {[ + { value: '< 2 Mio', label: '< 2 Mio. Euro' }, + { value: '2-10 Mio', label: '2-10 Mio. Euro' }, + { value: '10-50 Mio', label: '10-50 Mio. Euro' }, + { value: '> 50 Mio', label: '> 50 Mio. Euro' }, + ].map(opt => ( + + ))} +
+ {(data.companySize === 'medium' || data.companySize === 'large' || data.companySize === 'enterprise') && ( +

+ Geben Sie den konsolidierten Konzernumsatz an, wenn der Compliance-Check für Mutter- und Tochtergesellschaften gelten soll. + Für eine einzelne Einheit eines Konzerns geben Sie nur deren Umsatz an. +

+ )} +
+
+ ) +} diff --git a/admin-compliance/app/sdk/company-profile/_components/StepDataProtection.tsx b/admin-compliance/app/sdk/company-profile/_components/StepDataProtection.tsx new file mode 100644 index 0000000..1f99c76 --- /dev/null +++ b/admin-compliance/app/sdk/company-profile/_components/StepDataProtection.tsx @@ -0,0 +1,77 @@ +'use client' + +import { CompanyProfile } from '@/lib/sdk/types' + +export function StepDataProtection({ + data, + onChange, +}: { + data: Partial + onChange: (updates: Partial) => void +}) { + return ( +
+
+ +
+ + + +
+
+ +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/company-profile/_components/StepLegalFramework.tsx b/admin-compliance/app/sdk/company-profile/_components/StepLegalFramework.tsx new file mode 100644 index 0000000..4ffd085 --- /dev/null +++ b/admin-compliance/app/sdk/company-profile/_components/StepLegalFramework.tsx @@ -0,0 +1,143 @@ +'use client' + +import { CompanyProfile } from '@/lib/sdk/types' +import { CertificationEntry } from './types' +import { CERTIFICATIONS } from './constants' + +export function StepLegalFramework({ + data, + onChange, +}: { + data: Partial + onChange: (updates: Record) => 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) => { + 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 ( +
+ {/* Bestehende Zertifizierungen */} +
+

Bestehende Zertifizierungen

+

Ueber welche Zertifizierungen verfuegt Ihr Unternehmen aktuell? Mehrfachauswahl moeglich.

+
+ {CERTIFICATIONS.map(cert => { + const selected = existingCerts.some((c: CertificationEntry) => c.certId === cert.id) + return ( + + ) + })} +
+ + {existingCerts.length > 0 && ( +
+ {existingCerts.map((entry: CertificationEntry) => { + const cert = CERTIFICATIONS.find(c => c.id === entry.certId) + const label = cert?.label || entry.certId + return ( +
+
+ {entry.certId === 'other' ? 'Sonstige Zertifizierung' : label} +
+
+ {entry.certId === 'other' && ( + 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" /> + )} + 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" /> + 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" /> +
+
+ ) + })} +
+ )} +
+ + {/* Angestrebte Zertifizierungen */} +
+

Streben Sie eine Zertifizierung an?

+

Welche Zertifizierungen planen Sie? Mehrfachauswahl moeglich.

+
+ {CERTIFICATIONS.map(cert => { + const selected = targetCerts.includes(cert.id) + const alreadyHas = existingCerts.some((c: CertificationEntry) => c.certId === cert.id) + return ( + + ) + })} +
+ {targetCerts.includes('other') && ( +
+ 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" /> +
+ )} +
+ + {/* Technical Contacts */} +
+
+
+

Technische Ansprechpartner

+

CISO, IT-Manager, DSB etc.

+
+ +
+ {contacts.length === 0 && ( +
Noch keine Kontakte
+ )} +
+ {contacts.map((c: { name: string; role: string; email: string }, i: number) => ( +
+ 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" /> + 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" /> + 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" /> + +
+ ))} +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/company-profile/_components/StepLocations.tsx b/admin-compliance/app/sdk/company-profile/_components/StepLocations.tsx new file mode 100644 index 0000000..123bdab --- /dev/null +++ b/admin-compliance/app/sdk/company-profile/_components/StepLocations.tsx @@ -0,0 +1,177 @@ +'use client' + +import { CompanyProfile, TargetMarket, TARGET_MARKET_LABELS } from '@/lib/sdk/types' + +const STATES_BY_COUNTRY: Record = { + DE: { + label: 'Bundesland', + options: [ + 'Baden-W\u00FCrttemberg', 'Bayern', 'Berlin', 'Brandenburg', 'Bremen', + 'Hamburg', 'Hessen', 'Mecklenburg-Vorpommern', 'Niedersachsen', + 'Nordrhein-Westfalen', 'Rheinland-Pfalz', 'Saarland', 'Sachsen', + 'Sachsen-Anhalt', 'Schleswig-Holstein', 'Th\u00FCringen', + ], + }, + AT: { + label: 'Bundesland', + options: [ + 'Burgenland', 'K\u00E4rnten', 'Nieder\u00F6sterreich', 'Ober\u00F6sterreich', + 'Salzburg', 'Steiermark', 'Tirol', 'Vorarlberg', 'Wien', + ], + }, + CH: { + label: 'Kanton', + options: [ + 'Aargau', 'Appenzell Ausserrhoden', 'Appenzell Innerrhoden', + 'Basel-Landschaft', 'Basel-Stadt', 'Bern', 'Freiburg', 'Genf', + 'Glarus', 'Graub\u00FCnden', 'Jura', 'Luzern', 'Neuenburg', 'Nidwalden', + 'Obwalden', 'Schaffhausen', 'Schwyz', 'Solothurn', 'St. Gallen', + 'Tessin', 'Thurgau', 'Uri', 'Waadt', 'Wallis', 'Zug', 'Z\u00FCrich', + ], + }, +} + +export function StepLocations({ + data, + onChange, +}: { + data: Partial + onChange: (updates: Partial) => 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] }) + } + } + + const countryStates = data.headquartersCountry ? STATES_BY_COUNTRY[data.headquartersCountry] : null + const stateLabel = countryStates?.label || 'Region / Provinz' + + return ( +
+ {/* Country */} +
+ + +
+ + {data.headquartersCountry === 'other' && ( +
+ + onChange({ headquartersCountryOther: e.target.value })} + placeholder="z.B. Vereinigtes Königreich" + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> +
+ )} + + {/* Street + House Number */} +
+ + onChange({ headquartersStreet: e.target.value })} + placeholder="Musterstraße 42" + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> +
+ + {/* PLZ + City */} +
+
+ + onChange({ headquartersZip: e.target.value })} + placeholder="10115" + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> +
+
+ + onChange({ headquartersCity: e.target.value })} + placeholder="Berlin" + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> +
+
+ + {/* State / Bundesland / Kanton */} +
+ + {countryStates ? ( + + ) : ( + onChange({ headquartersState: e.target.value })} + placeholder="Region / Provinz" + 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(TARGET_MARKET_LABELS).map(([value, { label, description }]) => ( + + ))} +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/company-profile/_components/StepMachineBuilder.tsx b/admin-compliance/app/sdk/company-profile/_components/StepMachineBuilder.tsx new file mode 100644 index 0000000..336929c --- /dev/null +++ b/admin-compliance/app/sdk/company-profile/_components/StepMachineBuilder.tsx @@ -0,0 +1,253 @@ +'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 + onChange: (updates: Partial) => void +}) { + const mb = data.machineBuilder || EMPTY_MACHINE_BUILDER + + const updateMB = (updates: Partial) => { + 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 ( +
+ {/* Block 1: Product description */} +
+

Erzaehlen Sie uns von Ihrer Anlage

+

Je besser wir Ihr Produkt verstehen, desto praeziser koennen wir die relevanten Vorschriften identifizieren.

+ +
+
+ +