Compare commits
2 Commits
5da93c5d10
...
1f91e05600
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f91e05600 | ||
|
|
3c0c1e49da |
@@ -34,12 +34,12 @@ const BASE_WIZARD_STEPS = [
|
|||||||
{ id: 3, name: 'Firmengroesse', description: 'Mitarbeiter und Umsatz' },
|
{ id: 3, name: 'Firmengroesse', description: 'Mitarbeiter und Umsatz' },
|
||||||
{ id: 4, name: 'Standorte', description: 'Hauptsitz und Zielmaerkte' },
|
{ id: 4, name: 'Standorte', description: 'Hauptsitz und Zielmaerkte' },
|
||||||
{ id: 5, name: 'Datenschutz', description: 'Rollen und DSB' },
|
{ id: 5, name: 'Datenschutz', description: 'Rollen und DSB' },
|
||||||
{ id: 6, name: 'Rechtlicher Rahmen', description: 'Regulierungen und Prüfzyklen' },
|
{ id: 6, name: 'Zertifizierungen & Kontakte', description: 'Bestehende und angestrebte Zertifizierungen' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const MACHINE_BUILDER_STEP = { id: 7, name: 'Produkt & Maschine', description: 'Software, KI und CE in Ihrem Produkt' }
|
const MACHINE_BUILDER_STEP = { id: 7, name: 'Produkt & Maschine', description: 'Software, KI und CE in Ihrem Produkt' }
|
||||||
|
|
||||||
function getWizardSteps(industry: string) {
|
function getWizardSteps(industry: string | string[]) {
|
||||||
if (isMachineBuilderIndustry(industry)) {
|
if (isMachineBuilderIndustry(industry)) {
|
||||||
return [...BASE_WIZARD_STEPS, MACHINE_BUILDER_STEP]
|
return [...BASE_WIZARD_STEPS, MACHINE_BUILDER_STEP]
|
||||||
}
|
}
|
||||||
@@ -73,20 +73,35 @@ const LEGAL_FORM_LABELS: Record<LegalForm, string> = {
|
|||||||
|
|
||||||
const INDUSTRIES = [
|
const INDUSTRIES = [
|
||||||
'Technologie / IT',
|
'Technologie / IT',
|
||||||
|
'IT Dienstleistungen',
|
||||||
'E-Commerce / Handel',
|
'E-Commerce / Handel',
|
||||||
'Finanzdienstleistungen',
|
'Finanzdienstleistungen',
|
||||||
|
'Versicherungen',
|
||||||
'Gesundheitswesen',
|
'Gesundheitswesen',
|
||||||
|
'Pharma',
|
||||||
'Bildung',
|
'Bildung',
|
||||||
'Beratung / Consulting',
|
'Beratung / Consulting',
|
||||||
'Marketing / Agentur',
|
'Marketing / Agentur',
|
||||||
'Produktion / Industrie',
|
'Produktion / Industrie',
|
||||||
'Logistik / Transport',
|
'Logistik / Transport',
|
||||||
'Immobilien',
|
'Immobilien',
|
||||||
|
'Bau',
|
||||||
|
'Energie',
|
||||||
|
'Automobil',
|
||||||
|
'Luft- und Raumfahrt',
|
||||||
'Maschinenbau',
|
'Maschinenbau',
|
||||||
'Anlagenbau',
|
'Anlagenbau',
|
||||||
'Automatisierung',
|
'Automatisierung',
|
||||||
'Robotik',
|
'Robotik',
|
||||||
'Messtechnik',
|
'Messtechnik',
|
||||||
|
'Agrar',
|
||||||
|
'Chemie',
|
||||||
|
'Minen / Bergbau',
|
||||||
|
'Telekommunikation',
|
||||||
|
'Medien / Verlage',
|
||||||
|
'Gastronomie / Hotellerie',
|
||||||
|
'Recht / Kanzlei',
|
||||||
|
'Oeffentlicher Dienst',
|
||||||
'Sonstige',
|
'Sonstige',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -98,8 +113,10 @@ const MACHINE_BUILDER_INDUSTRIES = [
|
|||||||
'Messtechnik',
|
'Messtechnik',
|
||||||
]
|
]
|
||||||
|
|
||||||
const isMachineBuilderIndustry = (industry: string) =>
|
const isMachineBuilderIndustry = (industry: string | string[]) => {
|
||||||
MACHINE_BUILDER_INDUSTRIES.includes(industry)
|
const industries = Array.isArray(industry) ? industry : [industry]
|
||||||
|
return industries.some(i => MACHINE_BUILDER_INDUSTRIES.includes(i))
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// STEP COMPONENTS
|
// STEP COMPONENTS
|
||||||
@@ -146,23 +163,44 @@ function StepBasicInfo({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-3">Branche</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Branche(n)</label>
|
||||||
|
<p className="text-sm text-gray-500 mb-3">Mehrfachauswahl moeglich</p>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
{INDUSTRIES.map(industry => (
|
{INDUSTRIES.map(ind => {
|
||||||
<button
|
const selected = (data.industry || []).includes(ind)
|
||||||
key={industry}
|
return (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => onChange({ industry })}
|
key={ind}
|
||||||
className={`p-3 rounded-lg border-2 text-sm text-left transition-all ${
|
type="button"
|
||||||
data.industry === industry
|
onClick={() => {
|
||||||
? 'border-purple-500 bg-purple-50 text-purple-700 font-medium'
|
const current = data.industry || []
|
||||||
: 'border-gray-200 hover:border-purple-300 text-gray-700'
|
const updated = selected
|
||||||
}`}
|
? current.filter(i => i !== ind)
|
||||||
>
|
: [...current, ind]
|
||||||
{industry}
|
onChange({ industry: updated })
|
||||||
</button>
|
}}
|
||||||
))}
|
className={`p-3 rounded-lg border-2 text-sm text-left transition-all ${
|
||||||
|
selected
|
||||||
|
? 'border-purple-500 bg-purple-50 text-purple-700 font-medium'
|
||||||
|
: 'border-gray-200 hover:border-purple-300 text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{ind}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{(data.industry || []).includes('Sonstige') && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={data.industryOther || ''}
|
||||||
|
onChange={e => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -843,16 +881,25 @@ const INDUSTRY_DEPARTMENTS: Record<string, ActivityDepartment[]> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compute which departments to show based on company context
|
// Compute which departments to show based on company context
|
||||||
function getRelevantDepartments(industry: string, businessModel: string | undefined, companySize: string | undefined): ActivityDepartment[] {
|
function getRelevantDepartments(industry: string | string[], businessModel: string | undefined, companySize: string | undefined): ActivityDepartment[] {
|
||||||
const departments: ActivityDepartment[] = [...UNIVERSAL_DEPARTMENTS]
|
const departments: ActivityDepartment[] = [...UNIVERSAL_DEPARTMENTS]
|
||||||
|
|
||||||
// Always show optional departments — user can choose
|
// Always show optional departments — user can choose
|
||||||
departments.push(...OPTIONAL_DEPARTMENTS)
|
departments.push(...OPTIONAL_DEPARTMENTS)
|
||||||
|
|
||||||
// Add industry-specific departments
|
// Add industry-specific departments (support multi-select)
|
||||||
const industryDepts = INDUSTRY_DEPARTMENTS[industry]
|
const industries = Array.isArray(industry) ? industry : [industry]
|
||||||
if (industryDepts) {
|
const addedIds = new Set<string>()
|
||||||
departments.push(...industryDepts)
|
for (const ind of industries) {
|
||||||
|
const industryDepts = INDUSTRY_DEPARTMENTS[ind]
|
||||||
|
if (industryDepts) {
|
||||||
|
for (const dept of industryDepts) {
|
||||||
|
if (!addedIds.has(dept.id)) {
|
||||||
|
addedIds.add(dept.id)
|
||||||
|
departments.push(dept)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return departments
|
return departments
|
||||||
@@ -898,7 +945,7 @@ function StepProcessing({
|
|||||||
onChange: (updates: Record<string, unknown>) => void
|
onChange: (updates: Record<string, unknown>) => void
|
||||||
}) {
|
}) {
|
||||||
const activities: ProcessingActivity[] = (data as any).processingSystems || []
|
const activities: ProcessingActivity[] = (data as any).processingSystems || []
|
||||||
const industry = data.industry || ''
|
const industry = data.industry || []
|
||||||
const [expandedActivity, setExpandedActivity] = useState<string | null>(null)
|
const [expandedActivity, setExpandedActivity] = useState<string | null>(null)
|
||||||
const [collapsedDepts, setCollapsedDepts] = useState<Set<string>>(new Set())
|
const [collapsedDepts, setCollapsedDepts] = useState<Set<string>>(new Set())
|
||||||
const [showExtraCategories, setShowExtraCategories] = useState<Set<string>>(new Set())
|
const [showExtraCategories, setShowExtraCategories] = useState<Set<string>>(new Set())
|
||||||
@@ -1645,14 +1692,68 @@ function StepAISystems({
|
|||||||
// STEP 6: RECHTLICHER RAHMEN (was Step 8, renumbered)
|
// STEP 6: RECHTLICHER RAHMEN (was Step 8, renumbered)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
const CERTIFICATIONS = [
|
||||||
|
{ id: 'iso27001', label: 'ISO 27001', desc: 'Informationssicherheits-Managementsystem' },
|
||||||
|
{ id: 'iso27701', label: 'ISO 27701', desc: 'Datenschutz-Managementsystem' },
|
||||||
|
{ id: 'iso9001', label: 'ISO 9001', desc: 'Qualitaetsmanagement' },
|
||||||
|
{ id: 'iso14001', label: 'ISO 14001', desc: 'Umweltmanagement' },
|
||||||
|
{ id: 'iso22301', label: 'ISO 22301', desc: 'Business Continuity Management' },
|
||||||
|
{ id: 'iso42001', label: 'ISO 42001', desc: 'KI-Managementsystem' },
|
||||||
|
{ id: 'tisax', label: 'TISAX', desc: 'Trusted Information Security Assessment Exchange (Automotive)' },
|
||||||
|
{ id: 'soc2', label: 'SOC 2', desc: 'Service Organization Controls (Typ I/II)' },
|
||||||
|
{ id: 'c5', label: 'C5', desc: 'Cloud Computing Compliance Criteria Catalogue (BSI)' },
|
||||||
|
{ id: 'bsi_grundschutz', label: 'BSI IT-Grundschutz', desc: 'IT-Grundschutz-Zertifikat oder Testat' },
|
||||||
|
{ id: 'pci_dss', label: 'PCI DSS', desc: 'Payment Card Industry Data Security Standard' },
|
||||||
|
{ id: 'hipaa', label: 'HIPAA', desc: 'Health Insurance Portability and Accountability Act' },
|
||||||
|
{ id: 'ce_marking', label: 'CE-Kennzeichnung', desc: 'EU-Konformitaetskennzeichnung fuer Produkte' },
|
||||||
|
{ id: 'other', label: 'Sonstige', desc: 'Andere Zertifizierungen' },
|
||||||
|
]
|
||||||
|
|
||||||
|
interface CertificationEntry {
|
||||||
|
certId: string
|
||||||
|
certifier?: string
|
||||||
|
lastDate?: string
|
||||||
|
customName?: string
|
||||||
|
}
|
||||||
|
|
||||||
function StepLegalFramework({
|
function StepLegalFramework({
|
||||||
data,
|
data,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
data: Partial<CompanyProfile> & { subjectToNis2?: boolean; subjectToAiAct?: boolean; subjectToIso27001?: boolean; supervisoryAuthority?: string; reviewCycleMonths?: number; technicalContacts?: { name: string; role: string; email: string }[] }
|
data: Partial<CompanyProfile>
|
||||||
onChange: (updates: Record<string, unknown>) => void
|
onChange: (updates: Record<string, unknown>) => void
|
||||||
}) {
|
}) {
|
||||||
const contacts = (data as any).technicalContacts || []
|
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 || ''
|
||||||
|
|
||||||
|
// Toggle existing certification
|
||||||
|
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
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle target certification
|
||||||
|
const toggleTargetCert = (certId: string) => {
|
||||||
|
if (targetCerts.includes(certId)) {
|
||||||
|
onChange({ targetCertifications: targetCerts.filter((c: string) => c !== certId) })
|
||||||
|
} else {
|
||||||
|
onChange({ targetCertifications: [...targetCerts, certId] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const addContact = () => {
|
const addContact = () => {
|
||||||
onChange({ technicalContacts: [...contacts, { name: '', role: '', email: '' }] })
|
onChange({ technicalContacts: [...contacts, { name: '', role: '', email: '' }] })
|
||||||
@@ -1668,77 +1769,144 @@ function StepLegalFramework({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Regulatory Flags */}
|
{/* Bestehende Zertifizierungen */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-gray-700 mb-4">Regulatorischer Rahmen</h3>
|
<h3 className="text-sm font-medium text-gray-700 mb-1">Bestehende Zertifizierungen</h3>
|
||||||
<div className="space-y-3">
|
<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">
|
||||||
{ key: 'subjectToNis2', label: 'NIS2-Richtlinie', desc: 'Ihr Unternehmen fällt unter die NIS2-Richtlinie (Netzwerk- und Informationssicherheit)' },
|
{CERTIFICATIONS.map(cert => {
|
||||||
{ key: 'subjectToAiAct', label: 'EU AI Act', desc: 'Ihr Unternehmen setzt KI-Systeme ein, die unter den AI Act fallen' },
|
const selected = existingCerts.some((c: CertificationEntry) => c.certId === cert.id)
|
||||||
{ key: 'subjectToIso27001', label: 'ISO 27001', desc: 'Ihr Unternehmen strebt ISO 27001 Zertifizierung an oder ist bereits zertifiziert' },
|
return (
|
||||||
].map(item => (
|
<button
|
||||||
<label
|
key={cert.id}
|
||||||
key={item.key}
|
type="button"
|
||||||
className={`flex items-start gap-4 p-4 rounded-xl border-2 cursor-pointer transition-all ${
|
onClick={() => toggleExistingCert(cert.id)}
|
||||||
(data as any)[item.key] ? 'border-purple-500 bg-purple-50' : 'border-gray-200 hover:border-purple-300'
|
className={`p-3 rounded-lg border-2 text-left transition-all ${
|
||||||
}`}
|
selected
|
||||||
>
|
? 'border-purple-500 bg-purple-50 text-purple-700'
|
||||||
<input
|
: 'border-gray-200 hover:border-purple-300 text-gray-700'
|
||||||
type="checkbox"
|
}`}
|
||||||
checked={(data as any)[item.key] ?? false}
|
>
|
||||||
onChange={e => onChange({ [item.key]: e.target.checked })}
|
<div className="font-medium text-sm">{cert.label}</div>
|
||||||
className="mt-1 w-5 h-5 text-purple-600 rounded focus:ring-purple-500"
|
<div className="text-xs text-gray-500 mt-0.5">{cert.desc}</div>
|
||||||
/>
|
</button>
|
||||||
<div>
|
)
|
||||||
<div className="font-medium text-gray-900">{item.label}</div>
|
})}
|
||||||
<div className="text-sm text-gray-500">{item.desc}</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Details fuer ausgewaehlte Zertifizierungen */}
|
||||||
|
{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ÜV, 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>
|
</div>
|
||||||
|
|
||||||
{/* Supervisory Authority & Review Cycle */}
|
{/* Angestrebte Zertifizierungen */}
|
||||||
<div className="grid grid-cols-2 gap-6">
|
<div className="border-t border-gray-200 pt-6">
|
||||||
<div>
|
<h3 className="text-sm font-medium text-gray-700 mb-1">Streben Sie eine Zertifizierung an?</h3>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Aufsichtsbehörde</label>
|
<p className="text-sm text-gray-500 mb-3">Welche Zertifizierungen planen Sie? Mehrfachauswahl moeglich.</p>
|
||||||
<select
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
value={(data as any).supervisoryAuthority || ''}
|
{CERTIFICATIONS.map(cert => {
|
||||||
onChange={e => onChange({ supervisoryAuthority: e.target.value })}
|
const selected = targetCerts.includes(cert.id)
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
// Bereits bestehende Zertifizierungen ausgrauen
|
||||||
>
|
const alreadyHas = existingCerts.some((c: CertificationEntry) => c.certId === cert.id)
|
||||||
<option value="">Bitte wählen...</option>
|
return (
|
||||||
<option value="LfDI BW">LfDI Baden-Württemberg</option>
|
<button
|
||||||
<option value="BayLDA">BayLDA Bayern</option>
|
key={cert.id}
|
||||||
<option value="BlnBDI">BlnBDI Berlin</option>
|
type="button"
|
||||||
<option value="LDA BB">LDA Brandenburg</option>
|
onClick={() => !alreadyHas && toggleTargetCert(cert.id)}
|
||||||
<option value="LfDI HB">LfDI Bremen</option>
|
disabled={alreadyHas}
|
||||||
<option value="HmbBfDI">HmbBfDI Hamburg</option>
|
className={`p-3 rounded-lg border-2 text-left transition-all ${
|
||||||
<option value="HBDI">HBDI Hessen</option>
|
alreadyHas
|
||||||
<option value="LfDI MV">LfDI Mecklenburg-Vorpommern</option>
|
? 'border-gray-100 bg-gray-50 text-gray-400 cursor-not-allowed'
|
||||||
<option value="LfD NI">LfD Niedersachsen</option>
|
: selected
|
||||||
<option value="LDI NRW">LDI NRW</option>
|
? 'border-green-500 bg-green-50 text-green-700'
|
||||||
<option value="LfDI RP">LfDI Rheinland-Pfalz</option>
|
: 'border-gray-200 hover:border-green-300 text-gray-700'
|
||||||
<option value="UDZ SL">UDZ Saarland</option>
|
}`}
|
||||||
<option value="SächsDSB">Sächsischer DSB</option>
|
>
|
||||||
<option value="LfD LSA">LfD Sachsen-Anhalt</option>
|
<div className="font-medium text-sm">{cert.label}</div>
|
||||||
<option value="ULD SH">ULD Schleswig-Holstein</option>
|
{alreadyHas && <div className="text-xs mt-0.5">Bereits vorhanden</div>}
|
||||||
<option value="TLfDI">TLfDI Thüringen</option>
|
{!alreadyHas && <div className="text-xs text-gray-500 mt-0.5">{cert.desc}</div>}
|
||||||
<option value="BfDI">BfDI (Bund)</option>
|
</button>
|
||||||
</select>
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{targetCerts.includes('other') && (
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Prüfzyklus (Monate)</label>
|
<div className="mt-3">
|
||||||
<select
|
<input
|
||||||
value={(data as any).reviewCycleMonths || 12}
|
type="text"
|
||||||
onChange={e => onChange({ reviewCycleMonths: parseInt(e.target.value) })}
|
value={targetCertOther}
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
onChange={e => onChange({ targetCertificationOther: e.target.value })}
|
||||||
>
|
placeholder="Name der angestrebten Zertifizierung"
|
||||||
<option value={3}>Vierteljährlich (3 Monate)</option>
|
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={6}>Halbjährlich (6 Monate)</option>
|
/>
|
||||||
<option value={12}>Jährlich (12 Monate)</option>
|
</div>
|
||||||
<option value={24}>Zweijährlich (24 Monate)</option>
|
)}
|
||||||
</select>
|
</div>
|
||||||
|
|
||||||
|
{/* Pruefzyklus */}
|
||||||
|
<div className="border-t border-gray-200 pt-6">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Gewuenschter Pruefzyklus</label>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
|
{[
|
||||||
|
{ value: 3, label: 'Vierteljaehrlich', desc: '3 Monate' },
|
||||||
|
{ value: 6, label: 'Halbjaehrlich', desc: '6 Monate' },
|
||||||
|
{ value: 12, label: 'Jaehrlich', desc: '12 Monate' },
|
||||||
|
{ value: 24, label: 'Zweijaehrlich', desc: '24 Monate' },
|
||||||
|
].map(opt => {
|
||||||
|
const selected = ((data as any).reviewCycleMonths || 12) === opt.value
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange({ reviewCycleMonths: opt.value })}
|
||||||
|
className={`p-3 rounded-lg border-2 text-center transition-all ${
|
||||||
|
selected
|
||||||
|
? 'border-purple-500 bg-purple-50 text-purple-700 font-medium'
|
||||||
|
: 'border-gray-200 hover:border-purple-300 text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-sm font-medium">{opt.label}</div>
|
||||||
|
<div className="text-xs text-gray-500">{opt.desc}</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2269,7 +2437,8 @@ export default function CompanyProfilePage() {
|
|||||||
const [formData, setFormData] = useState<Partial<CompanyProfile>>({
|
const [formData, setFormData] = useState<Partial<CompanyProfile>>({
|
||||||
companyName: '',
|
companyName: '',
|
||||||
legalForm: undefined,
|
legalForm: undefined,
|
||||||
industry: '',
|
industry: [],
|
||||||
|
industryOther: '',
|
||||||
foundedYear: null,
|
foundedYear: null,
|
||||||
businessModel: undefined,
|
businessModel: undefined,
|
||||||
offerings: [],
|
offerings: [],
|
||||||
@@ -2297,8 +2466,8 @@ export default function CompanyProfilePage() {
|
|||||||
completedAt: null,
|
completedAt: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const showMachineBuilderStep = isMachineBuilderIndustry(formData.industry || '')
|
const showMachineBuilderStep = isMachineBuilderIndustry(formData.industry || [])
|
||||||
const wizardSteps = getWizardSteps(formData.industry || '')
|
const wizardSteps = getWizardSteps(formData.industry || [])
|
||||||
const totalSteps = wizardSteps.length
|
const totalSteps = wizardSteps.length
|
||||||
const lastStep = wizardSteps[wizardSteps.length - 1].id
|
const lastStep = wizardSteps[wizardSteps.length - 1].id
|
||||||
|
|
||||||
@@ -2325,7 +2494,8 @@ export default function CompanyProfilePage() {
|
|||||||
const backendProfile: Partial<CompanyProfile> = {
|
const backendProfile: Partial<CompanyProfile> = {
|
||||||
companyName: data.company_name || '',
|
companyName: data.company_name || '',
|
||||||
legalForm: data.legal_form || undefined,
|
legalForm: data.legal_form || undefined,
|
||||||
industry: data.industry || '',
|
industry: Array.isArray(data.industry) ? data.industry : (data.industry ? [data.industry] : []),
|
||||||
|
industryOther: data.industry_other || '',
|
||||||
foundedYear: data.founded_year || undefined,
|
foundedYear: data.founded_year || undefined,
|
||||||
businessModel: data.business_model || undefined,
|
businessModel: data.business_model || undefined,
|
||||||
offerings: data.offerings || [],
|
offerings: data.offerings || [],
|
||||||
@@ -2352,10 +2522,9 @@ export default function CompanyProfilePage() {
|
|||||||
processingSystems: data.processing_systems || [],
|
processingSystems: data.processing_systems || [],
|
||||||
aiSystems: data.ai_systems || [],
|
aiSystems: data.ai_systems || [],
|
||||||
technicalContacts: data.technical_contacts || [],
|
technicalContacts: data.technical_contacts || [],
|
||||||
subjectToNis2: data.subject_to_nis2 || false,
|
existingCertifications: data.existing_certifications || [],
|
||||||
subjectToAiAct: data.subject_to_ai_act || false,
|
targetCertifications: data.target_certifications || [],
|
||||||
subjectToIso27001: data.subject_to_iso27001 || false,
|
targetCertificationOther: data.target_certification_other || '',
|
||||||
supervisoryAuthority: data.supervisory_authority || '',
|
|
||||||
reviewCycleMonths: data.review_cycle_months || 12,
|
reviewCycleMonths: data.review_cycle_months || 12,
|
||||||
repos: data.repos || [],
|
repos: data.repos || [],
|
||||||
documentSources: data.document_sources || [],
|
documentSources: data.document_sources || [],
|
||||||
@@ -2395,7 +2564,8 @@ export default function CompanyProfilePage() {
|
|||||||
project_id: projectId || null,
|
project_id: projectId || null,
|
||||||
company_name: formData.companyName || '',
|
company_name: formData.companyName || '',
|
||||||
legal_form: formData.legalForm || 'GmbH',
|
legal_form: formData.legalForm || 'GmbH',
|
||||||
industry: formData.industry || '',
|
industry: formData.industry || [],
|
||||||
|
industry_other: formData.industryOther || '',
|
||||||
founded_year: formData.foundedYear || null,
|
founded_year: formData.foundedYear || null,
|
||||||
business_model: formData.businessModel || 'B2B',
|
business_model: formData.businessModel || 'B2B',
|
||||||
offerings: formData.offerings || [],
|
offerings: formData.offerings || [],
|
||||||
@@ -2422,10 +2592,9 @@ export default function CompanyProfilePage() {
|
|||||||
processing_systems: (formData as any).processingSystems || [],
|
processing_systems: (formData as any).processingSystems || [],
|
||||||
ai_systems: (formData as any).aiSystems || [],
|
ai_systems: (formData as any).aiSystems || [],
|
||||||
technical_contacts: (formData as any).technicalContacts || [],
|
technical_contacts: (formData as any).technicalContacts || [],
|
||||||
subject_to_nis2: (formData as any).subjectToNis2 || false,
|
existing_certifications: (formData as any).existingCertifications || [],
|
||||||
subject_to_ai_act: (formData as any).subjectToAiAct || false,
|
target_certifications: (formData as any).targetCertifications || [],
|
||||||
subject_to_iso27001: (formData as any).subjectToIso27001 || false,
|
target_certification_other: (formData as any).targetCertificationOther || '',
|
||||||
supervisory_authority: (formData as any).supervisoryAuthority || '',
|
|
||||||
review_cycle_months: (formData as any).reviewCycleMonths || 12,
|
review_cycle_months: (formData as any).reviewCycleMonths || 12,
|
||||||
repos: (formData as any).repos || [],
|
repos: (formData as any).repos || [],
|
||||||
document_sources: (formData as any).documentSources || [],
|
document_sources: (formData as any).documentSources || [],
|
||||||
@@ -2542,7 +2711,8 @@ export default function CompanyProfilePage() {
|
|||||||
setFormData({
|
setFormData({
|
||||||
companyName: '',
|
companyName: '',
|
||||||
legalForm: undefined,
|
legalForm: undefined,
|
||||||
industry: '',
|
industry: [],
|
||||||
|
industryOther: '',
|
||||||
foundedYear: null,
|
foundedYear: null,
|
||||||
businessModel: undefined,
|
businessModel: undefined,
|
||||||
offerings: [],
|
offerings: [],
|
||||||
|
|||||||
@@ -123,9 +123,9 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [
|
|||||||
checkpointType: 'REQUIRED',
|
checkpointType: 'REQUIRED',
|
||||||
checkpointReviewer: 'NONE',
|
checkpointReviewer: 'NONE',
|
||||||
description: 'Bestimmung der Compliance-Tiefe und des Umfangs basierend auf Unternehmensprofil.',
|
description: 'Bestimmung der Compliance-Tiefe und des Umfangs basierend auf Unternehmensprofil.',
|
||||||
descriptionLong: 'Basierend auf dem Unternehmensprofil wird automatisch ermittelt, wie tiefgehend die Compliance-Analyse sein muss. Kleine Unternehmen mit wenig Datenverarbeitung erhalten eine "BASIS"-Tiefe, waehrend grosse Unternehmen mit sensiblen Daten oder KI-Systemen eine "ERWEITERT" oder "VOLLSTAENDIG"-Tiefe erhalten. Der Compliance-Scope bestimmt, welche Module aktiviert werden und wie detailliert die Dokumentation sein muss.',
|
descriptionLong: 'Basierend auf dem Unternehmensprofil wird automatisch ermittelt, wie tiefgehend die Compliance-Analyse sein muss. Kleine Unternehmen mit wenig Datenverarbeitung erhalten eine "BASIS"-Tiefe, waehrend grosse Unternehmen mit sensiblen Daten oder KI-Systemen eine "ERWEITERT" oder "VOLLSTAENDIG"-Tiefe erhalten. Der Compliance-Scope bestimmt, welche Module aktiviert werden und wie detailliert die Dokumentation sein muss. Zusaetzlich werden anwendbare Regulierungen (DSGVO, AI Act, NIS2 etc.) und zustaendige Aufsichtsbehoerden automatisch abgeleitet.',
|
||||||
inputs: ['companyProfile'],
|
inputs: ['companyProfile'],
|
||||||
outputs: ['complianceDepthLevel'],
|
outputs: ['complianceDepthLevel', 'applicableRegulations', 'supervisoryAuthorities'],
|
||||||
prerequisiteSteps: ['company-profile'],
|
prerequisiteSteps: ['company-profile'],
|
||||||
dbTables: ['sdk_states'],
|
dbTables: ['sdk_states'],
|
||||||
dbMode: 'read/write',
|
dbMode: 'read/write',
|
||||||
@@ -399,9 +399,9 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [
|
|||||||
checkpointType: 'REQUIRED',
|
checkpointType: 'REQUIRED',
|
||||||
checkpointReviewer: 'NONE',
|
checkpointReviewer: 'NONE',
|
||||||
description: 'Zusammenfassung aller gesetzlichen Pflichten aus DSGVO, AI Act, NIS2.',
|
description: 'Zusammenfassung aller gesetzlichen Pflichten aus DSGVO, AI Act, NIS2.',
|
||||||
descriptionLong: 'Die Pflichtenuebersicht konsolidiert alle gesetzlichen Pflichten, die sich aus den Requirements, der AI-Act-Klassifizierung und den aktivierten Modulen ergeben. Fuer jede Pflicht wird angegeben: Welches Gesetz (DSGVO, AI Act, NIS2), welcher Artikel, welche Frist, wer verantwortlich ist und welche Massnahmen erforderlich sind. Die RAG-Collection bp_compliance_recht liefert aktuelle Pflichtentexte und Auslegungshinweise.',
|
descriptionLong: 'Die Pflichtenuebersicht konsolidiert alle gesetzlichen Pflichten, die sich aus den Requirements, der AI-Act-Klassifizierung und den aktivierten Modulen ergeben. Wenn applicableRegulations aus dem Scope-Profiling vorliegen, werden diese direkt als Vorfilter verwendet. Fuer jede Pflicht wird angegeben: Welches Gesetz (DSGVO, AI Act, NIS2), welcher Artikel, welche Frist, wer verantwortlich ist und welche Massnahmen erforderlich sind. Die RAG-Collection bp_compliance_recht liefert aktuelle Pflichtentexte und Auslegungshinweise.',
|
||||||
legalBasis: 'Art. 5 Abs. 2 DSGVO, Art. 9 AI Act',
|
legalBasis: 'Art. 5 Abs. 2 DSGVO, Art. 9 AI Act',
|
||||||
inputs: ['requirements', 'aiActClassification', 'modules'],
|
inputs: ['requirements', 'aiActClassification', 'modules', 'applicableRegulations'],
|
||||||
outputs: ['obligationsOverview'],
|
outputs: ['obligationsOverview'],
|
||||||
prerequisiteSteps: ['audit-report'],
|
prerequisiteSteps: ['audit-report'],
|
||||||
dbTables: ['compliance_obligations'],
|
dbTables: ['compliance_obligations'],
|
||||||
|
|||||||
@@ -482,7 +482,6 @@ export function ProjectSelector() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleProjectClick = (project: ProjectInfo) => {
|
const handleProjectClick = (project: ProjectInfo) => {
|
||||||
if (project.status === 'archived') return // archived projects are read-only in list
|
|
||||||
router.push(`/sdk?project=${project.id}`)
|
router.push(`/sdk?project=${project.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,26 +597,6 @@ export function ProjectSelector() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* No active but has archived */}
|
|
||||||
{!loading && projects.length === 0 && archivedProjects.length > 0 && (
|
|
||||||
<div className="text-center py-12 mb-6">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Keine aktiven Projekte</h2>
|
|
||||||
<p className="mt-2 text-gray-500">
|
|
||||||
Sie haben {archivedProjects.length} archivierte{archivedProjects.length === 1 ? 's' : ''} Projekt{archivedProjects.length === 1 ? '' : 'e'}.
|
|
||||||
Stellen Sie ein Projekt wieder her oder erstellen Sie ein neues.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateDialog(true)}
|
|
||||||
className="mt-4 inline-flex items-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
||||||
</svg>
|
|
||||||
Neues Projekt erstellen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Archived Projects Section */}
|
{/* Archived Projects Section */}
|
||||||
{!loading && archivedProjects.length > 0 && (
|
{!loading && archivedProjects.length > 0 && (
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
|
|||||||
193
admin-compliance/lib/sdk/__tests__/scope-to-facts.test.ts
Normal file
193
admin-compliance/lib/sdk/__tests__/scope-to-facts.test.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import {
|
||||||
|
parseEmployeeRange,
|
||||||
|
parseRevenueRange,
|
||||||
|
buildAssessmentPayload,
|
||||||
|
} from '../scope-to-facts'
|
||||||
|
import type { CompanyProfile } from '../types'
|
||||||
|
import type { ScopeProfilingAnswer, ScopeDecision } from '../compliance-scope-types'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// parseEmployeeRange
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe('parseEmployeeRange', () => {
|
||||||
|
it('returns 5 for "1-9"', () => {
|
||||||
|
expect(parseEmployeeRange('1-9')).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 30 for "10-49"', () => {
|
||||||
|
expect(parseEmployeeRange('10-49')).toBe(30)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 150 for "50-249"', () => {
|
||||||
|
expect(parseEmployeeRange('50-249')).toBe(150)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 625 for "250-999"', () => {
|
||||||
|
expect(parseEmployeeRange('250-999')).toBe(625)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 1500 for "1000+"', () => {
|
||||||
|
expect(parseEmployeeRange('1000+')).toBe(1500)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 10 for null', () => {
|
||||||
|
expect(parseEmployeeRange(null)).toBe(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 10 for undefined', () => {
|
||||||
|
expect(parseEmployeeRange(undefined)).toBe(10)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// parseRevenueRange
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe('parseRevenueRange', () => {
|
||||||
|
it('returns 1000000 for "< 2 Mio"', () => {
|
||||||
|
expect(parseRevenueRange('< 2 Mio')).toBe(1000000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 6000000 for "2-10 Mio"', () => {
|
||||||
|
expect(parseRevenueRange('2-10 Mio')).toBe(6000000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 30000000 for "10-50 Mio"', () => {
|
||||||
|
expect(parseRevenueRange('10-50 Mio')).toBe(30000000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 75000000 for "> 50 Mio"', () => {
|
||||||
|
expect(parseRevenueRange('> 50 Mio')).toBe(75000000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 1000000 for null', () => {
|
||||||
|
expect(parseRevenueRange(null)).toBe(1000000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 1000000 for undefined', () => {
|
||||||
|
expect(parseRevenueRange(undefined)).toBe(1000000)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// buildAssessmentPayload
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe('buildAssessmentPayload', () => {
|
||||||
|
const baseProfile: CompanyProfile = {
|
||||||
|
companyName: 'Test GmbH',
|
||||||
|
legalForm: 'GmbH',
|
||||||
|
industry: ['IT', 'Software'],
|
||||||
|
employeeCount: '50-249',
|
||||||
|
annualRevenue: '10-50 Mio',
|
||||||
|
headquartersCountry: 'DE',
|
||||||
|
headquartersState: 'BW',
|
||||||
|
isDataController: true,
|
||||||
|
isDataProcessor: false,
|
||||||
|
offerings: ['software_saas'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseAnswers: ScopeProfilingAnswer[] = [
|
||||||
|
{ questionId: 'data_art9', value: false, blockId: 'data' },
|
||||||
|
{ questionId: 'data_minors', value: false, blockId: 'data' },
|
||||||
|
{ questionId: 'data_hr', value: true, blockId: 'data' },
|
||||||
|
{ questionId: 'data_financial', value: false, blockId: 'data' },
|
||||||
|
{ questionId: 'tech_third_country', value: true, blockId: 'tech' },
|
||||||
|
{ questionId: 'tech_subprocessors', value: true, blockId: 'tech' },
|
||||||
|
{ questionId: 'proc_adm_scoring', value: false, blockId: 'processing' },
|
||||||
|
{ questionId: 'proc_employee_monitoring', value: false, blockId: 'processing' },
|
||||||
|
{ questionId: 'proc_video_surveillance', value: false, blockId: 'processing' },
|
||||||
|
{ questionId: 'proc_tracking', value: false, blockId: 'processing' },
|
||||||
|
{ questionId: 'prod_cookies_consent', value: true, blockId: 'product' },
|
||||||
|
{ questionId: 'data_volume', value: false, blockId: 'data' },
|
||||||
|
{ questionId: 'ai_uses_ai', value: true, blockId: 'ai' },
|
||||||
|
{ questionId: 'ai_categories', value: ['ai_provider'], blockId: 'ai' },
|
||||||
|
{ questionId: 'ai_risk_assessment', value: 'limited', blockId: 'ai' },
|
||||||
|
{ questionId: 'org_cert_target', value: 'iso27001', blockId: 'organisation' },
|
||||||
|
]
|
||||||
|
|
||||||
|
it('maps a full profile correctly', () => {
|
||||||
|
const payload = buildAssessmentPayload(baseProfile, baseAnswers, null)
|
||||||
|
|
||||||
|
expect(payload.employee_count).toBe(150)
|
||||||
|
expect(payload.annual_revenue).toBe(30000000)
|
||||||
|
expect(payload.country).toBe('DE')
|
||||||
|
expect(payload.industry).toBe('IT, Software')
|
||||||
|
expect(payload.legal_form).toBe('GmbH')
|
||||||
|
expect(payload.is_controller).toBe(true)
|
||||||
|
expect(payload.is_processor).toBe(false)
|
||||||
|
expect(payload.cross_border_transfer).toBe(true)
|
||||||
|
expect(payload.uses_processors).toBe(true)
|
||||||
|
expect(payload.uses_cookies).toBe(true)
|
||||||
|
expect(payload.processes_employee_data).toBe(true)
|
||||||
|
expect(payload.operates_platform).toBe(true)
|
||||||
|
expect(payload.proc_ai_usage).toBe(true)
|
||||||
|
expect(payload.cert_target).toBe('iso27001')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses defaults for null/undefined profile fields', () => {
|
||||||
|
const emptyProfile: CompanyProfile = {
|
||||||
|
companyName: 'Minimal',
|
||||||
|
}
|
||||||
|
const payload = buildAssessmentPayload(emptyProfile, [], null)
|
||||||
|
|
||||||
|
expect(payload.employee_count).toBe(10) // parseEmployeeRange(undefined)
|
||||||
|
expect(payload.annual_revenue).toBe(1000000)
|
||||||
|
expect(payload.country).toBe('DE') // default
|
||||||
|
expect(payload.industry).toBe('')
|
||||||
|
expect(payload.legal_form).toBe('')
|
||||||
|
expect(payload.is_controller).toBe(true) // default
|
||||||
|
expect(payload.is_processor).toBe(false) // default
|
||||||
|
expect(payload.determined_level).toBe('L2') // default
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects AI provider from ai_categories', () => {
|
||||||
|
const payload = buildAssessmentPayload(baseProfile, baseAnswers, null)
|
||||||
|
|
||||||
|
expect(payload.is_ai_provider).toBe(true)
|
||||||
|
expect(payload.is_ai_deployer).toBe(false)
|
||||||
|
expect(payload.limited_risk_ai).toBe(true)
|
||||||
|
expect(payload.high_risk_ai).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects AI deployer from ai_categories', () => {
|
||||||
|
const deployerAnswers = baseAnswers.map(a =>
|
||||||
|
a.questionId === 'ai_categories'
|
||||||
|
? { ...a, value: ['ai_deployer'] }
|
||||||
|
: a
|
||||||
|
)
|
||||||
|
const payload = buildAssessmentPayload(baseProfile, deployerAnswers, null)
|
||||||
|
|
||||||
|
expect(payload.is_ai_provider).toBe(false)
|
||||||
|
expect(payload.is_ai_deployer).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects financial institution from industry', () => {
|
||||||
|
const finProfile: CompanyProfile = {
|
||||||
|
...baseProfile,
|
||||||
|
industry: ['Finanzdienstleistungen', 'Banking'],
|
||||||
|
}
|
||||||
|
const payload = buildAssessmentPayload(finProfile, baseAnswers, null)
|
||||||
|
|
||||||
|
expect(payload.is_financial_institution).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('includes decision data when provided', () => {
|
||||||
|
const decision: ScopeDecision = {
|
||||||
|
determinedLevel: 'L3',
|
||||||
|
triggeredHardTriggers: [
|
||||||
|
{ rule: { id: 'rule-1', name: 'Test Rule', description: '', targetLevel: 'L3', trigger: { field: '', op: 'eq', value: true } }, factValue: true },
|
||||||
|
],
|
||||||
|
requiredDocuments: [
|
||||||
|
{ documentType: 'dsfa', reason: 'test', regulation: 'dsgvo' },
|
||||||
|
],
|
||||||
|
} as any
|
||||||
|
const payload = buildAssessmentPayload(baseProfile, baseAnswers, decision)
|
||||||
|
|
||||||
|
expect(payload.determined_level).toBe('L3')
|
||||||
|
expect(payload.triggered_rules).toEqual(['rule-1'])
|
||||||
|
expect(payload.required_documents).toEqual(['dsfa'])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { resolveAuthorities } from '../supervisory-authority-resolver'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Datenschutz-Aufsichtsbehoerde (DSGVO)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe('resolveAuthorities — Datenschutz', () => {
|
||||||
|
it('resolves DE + BW to LfDI BW', () => {
|
||||||
|
const results = resolveAuthorities('BW', 'DE', ['dsgvo'])
|
||||||
|
const dp = results.find(r => r.domain === 'Datenschutz')
|
||||||
|
expect(dp).toBeDefined()
|
||||||
|
expect(dp!.authority.abbreviation).toBe('LfDI BW')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves DE + BY to BayLDA', () => {
|
||||||
|
const results = resolveAuthorities('BY', 'DE', ['dsgvo'])
|
||||||
|
const dp = results.find(r => r.domain === 'Datenschutz')
|
||||||
|
expect(dp).toBeDefined()
|
||||||
|
expect(dp!.authority.abbreviation).toBe('BayLDA')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves DE without state to BfDI', () => {
|
||||||
|
const results = resolveAuthorities(undefined, 'DE', ['dsgvo'])
|
||||||
|
const dp = results.find(r => r.domain === 'Datenschutz')
|
||||||
|
expect(dp).toBeDefined()
|
||||||
|
expect(dp!.authority.abbreviation).toBe('BfDI')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves AT to DSB AT', () => {
|
||||||
|
const results = resolveAuthorities(undefined, 'AT', ['dsgvo'])
|
||||||
|
const dp = results.find(r => r.domain === 'Datenschutz')
|
||||||
|
expect(dp).toBeDefined()
|
||||||
|
expect(dp!.authority.abbreviation).toBe('DSB AT')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves CH to EDOEB', () => {
|
||||||
|
const results = resolveAuthorities(undefined, 'CH', ['dsgvo'])
|
||||||
|
const dp = results.find(r => r.domain === 'Datenschutz')
|
||||||
|
expect(dp).toBeDefined()
|
||||||
|
expect(dp!.authority.abbreviation).toBe('EDOEB')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Regulierungs-spezifische Behoerden
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe('resolveAuthorities — regulation-specific', () => {
|
||||||
|
it('includes BSI for NIS2 in DE', () => {
|
||||||
|
const results = resolveAuthorities('BW', 'DE', ['dsgvo', 'nis2'])
|
||||||
|
const nis2 = results.find(r => r.domain.includes('NIS2'))
|
||||||
|
expect(nis2).toBeDefined()
|
||||||
|
expect(nis2!.authority.abbreviation).toBe('BSI')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('includes BaFin for financial_policy in DE', () => {
|
||||||
|
const results = resolveAuthorities('BW', 'DE', ['dsgvo', 'financial_policy'])
|
||||||
|
const fin = results.find(r => r.domain.includes('Finanzaufsicht'))
|
||||||
|
expect(fin).toBeDefined()
|
||||||
|
expect(fin!.authority.abbreviation).toBe('BaFin')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('includes BNetzA for ai_act in DE', () => {
|
||||||
|
const results = resolveAuthorities('BW', 'DE', ['dsgvo', 'ai_act'])
|
||||||
|
const ai = results.find(r => r.domain.includes('KI-Aufsicht'))
|
||||||
|
expect(ai).toBeDefined()
|
||||||
|
expect(ai!.authority.abbreviation).toBe('BNetzA')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('includes NCSA for NIS2 outside DE', () => {
|
||||||
|
const results = resolveAuthorities(undefined, 'AT', ['dsgvo', 'nis2'])
|
||||||
|
const nis2 = results.find(r => r.domain.includes('NIS2'))
|
||||||
|
expect(nis2).toBeDefined()
|
||||||
|
expect(nis2!.authority.abbreviation).toBe('NCSA')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Edge Cases
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe('resolveAuthorities — edge cases', () => {
|
||||||
|
it('returns empty array when no regulations', () => {
|
||||||
|
const results = resolveAuthorities('BW', 'DE', [])
|
||||||
|
expect(results).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns multiple authorities for multiple regulations', () => {
|
||||||
|
const results = resolveAuthorities('BW', 'DE', ['dsgvo', 'nis2', 'ai_act', 'financial_policy'])
|
||||||
|
expect(results.length).toBe(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not include BaFin for non-DE financial_policy', () => {
|
||||||
|
const results = resolveAuthorities(undefined, 'AT', ['financial_policy'])
|
||||||
|
const fin = results.find(r => r.domain.includes('Finanzaufsicht'))
|
||||||
|
expect(fin).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not include BNetzA for non-DE ai_act', () => {
|
||||||
|
const results = resolveAuthorities(undefined, 'AT', ['ai_act'])
|
||||||
|
const ai = results.find(r => r.domain.includes('KI-Aufsicht'))
|
||||||
|
expect(ai).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -702,10 +702,10 @@ export function getAutoFilledScoringAnswers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// industry -> org_industry
|
// industry -> org_industry
|
||||||
if (profile.industry) {
|
if (profile.industry && profile.industry.length > 0) {
|
||||||
answers.push({
|
answers.push({
|
||||||
questionId: 'org_industry',
|
questionId: 'org_industry',
|
||||||
value: profile.industry,
|
value: profile.industry.join(', '),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,7 +738,7 @@ export function getProfileInfoForBlock(
|
|||||||
const items: { label: string; value: string }[] = []
|
const items: { label: string; value: string }[] = []
|
||||||
|
|
||||||
if (blockId === 'organisation') {
|
if (blockId === 'organisation') {
|
||||||
if (profile.industry) items.push({ label: 'Branche', value: profile.industry })
|
if (profile.industry && profile.industry.length > 0) items.push({ label: 'Branche', value: profile.industry.join(', ') })
|
||||||
if (profile.employeeCount) items.push({ label: 'Mitarbeiter', value: profile.employeeCount })
|
if (profile.employeeCount) items.push({ label: 'Mitarbeiter', value: profile.employeeCount })
|
||||||
if (profile.annualRevenue) items.push({ label: 'Umsatz', value: profile.annualRevenue })
|
if (profile.annualRevenue) items.push({ label: 'Umsatz', value: profile.annualRevenue })
|
||||||
if (profile.businessModel) items.push({ label: 'Geschäftsmodell', value: profile.businessModel })
|
if (profile.businessModel) items.push({ label: 'Geschäftsmodell', value: profile.businessModel })
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ export function generateDemoState(tenantId: string, userId: string): Partial<SDK
|
|||||||
companyProfile: {
|
companyProfile: {
|
||||||
companyName: 'TechStart GmbH',
|
companyName: 'TechStart GmbH',
|
||||||
legalForm: 'gmbh',
|
legalForm: 'gmbh',
|
||||||
industry: 'Technologie / IT',
|
industry: ['Technologie / IT'],
|
||||||
|
industryOther: '',
|
||||||
foundedYear: 2022,
|
foundedYear: 2022,
|
||||||
businessModel: 'B2B_B2C',
|
businessModel: 'B2B_B2C',
|
||||||
offerings: ['app_web', 'software_saas', 'services_consulting'],
|
offerings: ['app_web', 'software_saas', 'services_consulting'],
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function buildAllowedFactsFromDraftContext(
|
|||||||
return {
|
return {
|
||||||
companyName: profile.name || 'Unbekannt',
|
companyName: profile.name || 'Unbekannt',
|
||||||
legalForm: '', // Nicht im DraftContext enthalten
|
legalForm: '', // Nicht im DraftContext enthalten
|
||||||
industry: profile.industry || '',
|
industry: Array.isArray(profile.industry) ? profile.industry.join(', ') : (profile.industry || ''),
|
||||||
location: '', // Nicht im DraftContext enthalten
|
location: '', // Nicht im DraftContext enthalten
|
||||||
employeeCount: profile.employeeCount || 0,
|
employeeCount: profile.employeeCount || 0,
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export function buildAllowedFacts(
|
|||||||
return {
|
return {
|
||||||
companyName: profile?.companyName ?? 'Unbekannt',
|
companyName: profile?.companyName ?? 'Unbekannt',
|
||||||
legalForm: profile?.legalForm ?? '',
|
legalForm: profile?.legalForm ?? '',
|
||||||
industry: profile?.industry ?? '',
|
industry: Array.isArray(profile?.industry) ? profile.industry.join(', ') : (profile?.industry ?? ''),
|
||||||
location: profile?.headquartersCity ?? '',
|
location: profile?.headquartersCity ?? '',
|
||||||
employeeCount: parseEmployeeCount(profile?.employeeCount),
|
employeeCount: parseEmployeeCount(profile?.employeeCount),
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ KONTROLLE:
|
|||||||
- Typ: ${control.type}
|
- Typ: ${control.type}
|
||||||
|
|
||||||
UNTERNEHMENSPROFIL:
|
UNTERNEHMENSPROFIL:
|
||||||
- Branche: ${companyProfile.industry}
|
- Branche: ${Array.isArray(companyProfile.industry) ? companyProfile.industry.join(', ') : companyProfile.industry}
|
||||||
- Größe: ${companyProfile.size}
|
- Größe: ${companyProfile.size}
|
||||||
- Rolle: ${companyProfile.role}
|
- Rolle: ${companyProfile.role}
|
||||||
- Produkte/Services: ${companyProfile.products.join(', ')}
|
- Produkte/Services: ${companyProfile.products.join(', ')}
|
||||||
@@ -177,7 +177,7 @@ CONTROL:
|
|||||||
- Type: ${control.type}
|
- Type: ${control.type}
|
||||||
|
|
||||||
COMPANY PROFILE:
|
COMPANY PROFILE:
|
||||||
- Industry: ${companyProfile.industry}
|
- Industry: ${Array.isArray(companyProfile.industry) ? companyProfile.industry.join(', ') : companyProfile.industry}
|
||||||
- Size: ${companyProfile.size}
|
- Size: ${companyProfile.size}
|
||||||
- Role: ${companyProfile.role}
|
- Role: ${companyProfile.role}
|
||||||
- Products/Services: ${companyProfile.products.join(', ')}
|
- Products/Services: ${companyProfile.products.join(', ')}
|
||||||
@@ -240,7 +240,7 @@ export function getGapRecommendationsPrompt(
|
|||||||
return `Du bist ein Experte für Datenschutz-Compliance und erstellst Handlungsempfehlungen für TOM-Lücken.
|
return `Du bist ein Experte für Datenschutz-Compliance und erstellst Handlungsempfehlungen für TOM-Lücken.
|
||||||
|
|
||||||
UNTERNEHMEN:
|
UNTERNEHMEN:
|
||||||
- Branche: ${companyProfile.industry}
|
- Branche: ${Array.isArray(companyProfile.industry) ? companyProfile.industry.join(', ') : companyProfile.industry}
|
||||||
- Größe: ${companyProfile.size}
|
- Größe: ${companyProfile.size}
|
||||||
- Rolle: ${companyProfile.role}
|
- Rolle: ${companyProfile.role}
|
||||||
|
|
||||||
@@ -279,7 +279,7 @@ Antworte im JSON-Format:
|
|||||||
return `You are a data protection compliance expert creating recommendations for TOM gaps.
|
return `You are a data protection compliance expert creating recommendations for TOM gaps.
|
||||||
|
|
||||||
COMPANY:
|
COMPANY:
|
||||||
- Industry: ${companyProfile.industry}
|
- Industry: ${Array.isArray(companyProfile.industry) ? companyProfile.industry.join(', ') : companyProfile.industry}
|
||||||
- Size: ${companyProfile.size}
|
- Size: ${companyProfile.size}
|
||||||
- Role: ${companyProfile.role}
|
- Role: ${companyProfile.role}
|
||||||
|
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ export function generateDOCXContent(
|
|||||||
elements.push({
|
elements.push({
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: opts.language === 'de'
|
content: opts.language === 'de'
|
||||||
? `Branche: ${state.companyProfile.industry}`
|
? `Branche: ${Array.isArray(state.companyProfile.industry) ? state.companyProfile.industry.join(', ') : state.companyProfile.industry}`
|
||||||
: `Industry: ${state.companyProfile.industry}`,
|
: `Industry: ${Array.isArray(state.companyProfile.industry) ? state.companyProfile.industry.join(', ') : state.companyProfile.industry}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
elements.push({
|
elements.push({
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export function generatePDFContent(
|
|||||||
|
|
||||||
sections.push({
|
sections.push({
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: `${opts.language === 'de' ? 'Branche' : 'Industry'}: ${state.companyProfile.industry}`,
|
content: `${opts.language === 'de' ? 'Branche' : 'Industry'}: ${Array.isArray(state.companyProfile.industry) ? state.companyProfile.industry.join(', ') : state.companyProfile.industry}`,
|
||||||
style: { align: 'center' },
|
style: { align: 'center' },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,8 @@ export interface CompanyProfile {
|
|||||||
// Basic Info
|
// Basic Info
|
||||||
companyName: string
|
companyName: string
|
||||||
legalForm: LegalForm
|
legalForm: LegalForm
|
||||||
industry: string // Free text or NACE code
|
industry: string[] // Multi-select industries
|
||||||
|
industryOther: string // Custom text when "Sonstige" selected
|
||||||
foundedYear: number | null
|
foundedYear: number | null
|
||||||
|
|
||||||
// Business Model
|
// Business Model
|
||||||
|
|||||||
@@ -228,6 +228,12 @@ function ComplianceDashboard() {
|
|||||||
PDF, JSON, ZIP-Export fuer Audits und Dokumentation
|
PDF, JSON, ZIP-Export fuer Audits und Dokumentation
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="p-4 border border-gray-200 rounded-lg">
|
||||||
|
<h4 className="font-medium text-gray-900 mb-2">Automatische Regulierungs-Ableitung</h4>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Aus CompanyProfile und Scope-Profiling werden anwendbare Gesetze (DSGVO, AI Act, NIS2, DORA etc.) und zustaendige Aufsichtsbehoerden automatisch ermittelt
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Next Steps */}
|
{/* Next Steps */}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ Obligation DSGVO-ART-32 → Controls [TOM-001, TOM-042, TOM-097]
|
|||||||
| `GET` | `/sdk/v1/ucca/obligations/regulations` | Alle verfügbaren Regulierungen auflisten |
|
| `GET` | `/sdk/v1/ucca/obligations/regulations` | Alle verfügbaren Regulierungen auflisten |
|
||||||
| `GET` | `/sdk/v1/ucca/obligations/regulations/:id/decision-tree` | Entscheidungsbaum für eine Regulierung |
|
| `GET` | `/sdk/v1/ucca/obligations/regulations/:id/decision-tree` | Entscheidungsbaum für eine Regulierung |
|
||||||
|
|
||||||
### Schnellprüfung
|
### Schnellprüfung & Scope-Assessment
|
||||||
|
|
||||||
| Methode | Pfad | Beschreibung |
|
| Methode | Pfad | Beschreibung |
|
||||||
|---------|------|--------------|
|
|---------|------|--------------|
|
||||||
@@ -205,6 +205,60 @@ Obligation DSGVO-ART-32 → Controls [TOM-001, TOM-042, TOM-097]
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Automatische Regulierungs-Ableitung
|
||||||
|
|
||||||
|
Seit v2 kann das Obligations-Framework anwendbare Regulierungen und Aufsichtsbehörden direkt aus dem Scope-Profiling ableiten.
|
||||||
|
|
||||||
|
### Datenfluss
|
||||||
|
|
||||||
|
```
|
||||||
|
CompanyProfile + ScopeProfilingAnswers
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
scope-to-facts.ts ← Konvertiert Profil + Scope-Antworten in ScopeDecisionPayload
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
POST /assess-from-scope ← Go AI SDK bewertet Payload gegen Condition Engine
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
ApplicableRegulations ← Liste anwendbarer Gesetze (DSGVO, AI Act, NIS2, etc.)
|
||||||
|
+
|
||||||
|
supervisory-authority- ← Zuständige Aufsichtsbehörden (LfDI, BSI, BaFin, BNetzA)
|
||||||
|
resolver.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neue Dateien
|
||||||
|
|
||||||
|
| Datei | Beschreibung |
|
||||||
|
|-------|--------------|
|
||||||
|
| `admin-compliance/lib/sdk/scope-to-facts.ts` | Mapper: CompanyProfile + ScopeAnswers → `ScopeDecisionPayload` für Go SDK |
|
||||||
|
| `admin-compliance/lib/sdk/supervisory-authority-resolver.ts` | Ermittelt Aufsichtsbehörden aus Bundesland/Land + Regulierungen |
|
||||||
|
|
||||||
|
### scope-to-facts.ts
|
||||||
|
|
||||||
|
Exportierte Funktionen:
|
||||||
|
|
||||||
|
- `buildAssessmentPayload(profile, scopeAnswers, decision)` → `ScopeDecisionPayload`
|
||||||
|
- `parseEmployeeRange(range)` → Mittelwert als Zahl (z.B. "50-249" → 150)
|
||||||
|
- `parseRevenueRange(range)` → Umsatz als Zahl (z.B. "10-50 Mio" → 30.000.000)
|
||||||
|
|
||||||
|
### supervisory-authority-resolver.ts
|
||||||
|
|
||||||
|
Exportierte Funktion:
|
||||||
|
|
||||||
|
- `resolveAuthorities(state, country, regulationIds)` → `SupervisoryAuthorityResult[]`
|
||||||
|
|
||||||
|
Abgedeckte Regulierungen → Behörden:
|
||||||
|
|
||||||
|
| Regulierung | Behörde (DE) | Behörde (Andere) |
|
||||||
|
|-------------|-------------|-----------------|
|
||||||
|
| `dsgvo` | Landes-Datenschutzbehörde (16 Bundesländer) | Nationale DSB (AT, CH, FR, NL, etc.) |
|
||||||
|
| `nis2` | BSI | NCSA |
|
||||||
|
| `financial_policy` | BaFin | — |
|
||||||
|
| `ai_act` | BNetzA | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Gap-Analyse
|
## Gap-Analyse
|
||||||
|
|
||||||
Die Gap-Analyse vergleicht die **geforderten TOM-Controls** (aus Obligations) mit den **implementierten Controls** (aus `compliance_controls`):
|
Die Gap-Analyse vergleicht die **geforderten TOM-Controls** (aus Obligations) mit den **implementierten Controls** (aus `compliance_controls`):
|
||||||
|
|||||||
Reference in New Issue
Block a user