fix(scope): Evaluierung crasht (answerValue→value), Profil-Persistenz, Block-Umbenennungen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 36s
CI / test-python-backend-compliance (push) Successful in 42s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 25s

- compliance-scope-engine: answerValue→value (Property existierte nicht, Crash bei Evaluierung)
- company-profile: saveProfileDraft synct jetzt Redux-State (Daten bleiben bei Navigation)
- Scope-Bloecke umbenannt: Kunden & Nutzer, Datenverarbeitung, Hosting & Verarbeitung, Website und Services
- org_cert_target + data_volume als Hidden Scoring Questions (Duplikate entfernt)
- ai_risk_assessment: boolean→single mit Ja/Nein/Noch nicht
- 6 neue Abteilungs-Datenkategorien: IT, Recht, Produktion, Logistik, Einkauf, Facility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-10 16:33:59 +01:00
parent ee6743c7c6
commit 579fe1b5e1
5 changed files with 216 additions and 64 deletions

View File

@@ -2608,6 +2608,8 @@ export default function CompanyProfilePage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(buildProfilePayload(false)),
})
// Sync draft to Redux so it persists across navigation
setCompanyProfile({ ...formData, isComplete: false, completedAt: null } as CompanyProfile)
setDraftSaveStatus('saved')
// Reset status after 3 seconds
if (draftSaveTimerRef.current) clearTimeout(draftSaveTimerRef.current)

View File

@@ -580,7 +580,13 @@ const DEPT_VALUE_TO_KEY: Record<string, string[]> = {
finanzen: ['dept_finance'],
vertrieb: ['dept_sales'],
marketing: ['dept_marketing'],
it: ['dept_it'],
recht: ['dept_recht'],
kundenservice: ['dept_support'],
produktion: ['dept_produktion'],
logistik: ['dept_logistik'],
einkauf: ['dept_einkauf'],
facility: ['dept_facility'],
}
/** Mapping department key → scope question ID for Block 9 */
@@ -591,6 +597,12 @@ const DEPT_KEY_TO_QUESTION: Record<string, string> = {
dept_sales: 'dk_dept_sales',
dept_marketing: 'dk_dept_marketing',
dept_support: 'dk_dept_support',
dept_it: 'dk_dept_it',
dept_recht: 'dk_dept_recht',
dept_produktion: 'dk_dept_produktion',
dept_logistik: 'dk_dept_logistik',
dept_einkauf: 'dk_dept_einkauf',
dept_facility: 'dk_dept_facility',
}
function DatenkategorienBlock9({

View File

@@ -1062,32 +1062,32 @@ export class ComplianceScopeEngine {
* Bestimmt den Multiplikator für eine Antwort (0.0 - 1.0)
*/
private getAnswerMultiplier(answer: ScopeProfilingAnswer): number {
const { questionId, answerValue } = answer
const { questionId, value } = answer
// Boolean
if (typeof answerValue === 'boolean') {
return answerValue ? 1.0 : 0.0
if (typeof value === 'boolean') {
return value ? 1.0 : 0.0
}
// Number
if (typeof answerValue === 'number') {
return this.normalizeNumericAnswer(questionId, answerValue)
if (typeof value === 'number') {
return this.normalizeNumericAnswer(questionId, value)
}
// Single choice
if (typeof answerValue === 'string') {
if (typeof value === 'string') {
const multipliers = ANSWER_MULTIPLIERS[questionId]
if (multipliers && multipliers[answerValue] !== undefined) {
return multipliers[answerValue]
if (multipliers && multipliers[value] !== undefined) {
return multipliers[value]
}
return 0.5 // Fallback
}
// Multi choice
if (Array.isArray(answerValue)) {
if (answerValue.length === 0) return 0.0
if (Array.isArray(value)) {
if (value.length === 0) return 0.0
// Simplified: count selected items
return Math.min(answerValue.length / 5, 1.0)
return Math.min(value.length / 5, 1.0)
}
return 0.0
@@ -1110,7 +1110,7 @@ export class ComplianceScopeEngine {
*/
evaluateHardTriggers(answers: ScopeProfilingAnswer[], companyProfile?: CompanyProfile | null): TriggeredHardTrigger[] {
const triggered: TriggeredHardTrigger[] = []
const answerMap = new Map(answers.map((a) => [a.questionId, a.answerValue]))
const answerMap = new Map(answers.map((a) => [a.questionId, a.value]))
for (const rule of HARD_TRIGGER_RULES) {
const isTriggered = this.checkTriggerCondition(rule, answerMap, answers, companyProfile)
@@ -1186,39 +1186,39 @@ export class ComplianceScopeEngine {
}
// Standard answer-based triggers
const answerValue = answerMap.get(rule.questionId)
if (answerValue === undefined) return false
const value = answerMap.get(rule.questionId)
if (value === undefined) return false
// Basis-Check
let baseCondition = false
switch (rule.condition) {
case 'EQUALS':
baseCondition = answerValue === rule.conditionValue
baseCondition = value === rule.conditionValue
break
case 'CONTAINS':
if (Array.isArray(answerValue)) {
baseCondition = answerValue.includes(rule.conditionValue)
} else if (typeof answerValue === 'string') {
baseCondition = answerValue.includes(rule.conditionValue)
if (Array.isArray(value)) {
baseCondition = value.includes(rule.conditionValue)
} else if (typeof value === 'string') {
baseCondition = value.includes(rule.conditionValue)
}
break
case 'IN':
if (Array.isArray(rule.conditionValue)) {
baseCondition = rule.conditionValue.includes(answerValue)
baseCondition = rule.conditionValue.includes(value)
}
break
case 'GREATER_THAN':
if (typeof answerValue === 'number' && typeof rule.conditionValue === 'number') {
baseCondition = answerValue > rule.conditionValue
} else if (typeof answerValue === 'string') {
if (typeof value === 'number' && typeof rule.conditionValue === 'number') {
baseCondition = value > rule.conditionValue
} else if (typeof value === 'string') {
// Parse employee count from string like "1000+"
const parsed = this.parseEmployeeCount(answerValue)
const parsed = this.parseEmployeeCount(value)
baseCondition = parsed > (rule.conditionValue as number)
}
break
case 'NOT_EQUALS':
baseCondition = answerValue !== rule.conditionValue
baseCondition = value !== rule.conditionValue
break
}
@@ -1404,7 +1404,7 @@ export class ComplianceScopeEngine {
level: ComplianceDepthLevel
): RiskFlag[] {
const flags: RiskFlag[] = []
const answerMap = new Map(answers.map((a) => [a.questionId, a.answerValue]))
const answerMap = new Map(answers.map((a) => [a.questionId, a.value]))
// Process Maturity Gaps (Kategorie I Trigger)
const maturityRules = HARD_TRIGGER_RULES.filter((r) => r.category === 'process_maturity')
@@ -1503,7 +1503,7 @@ export class ComplianceScopeEngine {
level: ComplianceDepthLevel
): ScopeGap[] {
const gaps: ScopeGap[] = []
const answerMap = new Map(answers.map((a) => [a.questionId, a.answerValue]))
const answerMap = new Map(answers.map((a) => [a.questionId, a.value]))
// DSFA Gap (bei L3+)
if (getDepthLevelNumeric(level) >= 3) {

View File

@@ -21,14 +21,16 @@ export const PROFILE_AUTOFILL_QUESTION_IDS = [
'org_industry',
'org_business_model',
'org_has_dsb',
'org_cert_target',
'data_volume',
'prod_type',
'prod_webshop',
] as const
const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
id: 'organisation',
title: 'Organisation & Reife',
description: 'Grundlegende Informationen zu Ihrer Organisation und Compliance-Zielen',
title: 'Kunden & Nutzer',
description: 'Informationen zu Ihren Kunden und Nutzern',
order: 1,
questions: [
{
@@ -46,22 +48,6 @@ const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
],
scoreWeights: { risk: 6, complexity: 7, assurance: 6 },
},
{
id: 'org_cert_target',
type: 'multi',
question: 'Welche Zertifizierungen streben Sie an oder besitzen Sie bereits?',
helpText: 'Mehrfachauswahl möglich. Zertifizierungen erhöhen den Assurance-Bedarf',
required: false,
options: [
{ value: 'ISO27001', label: 'ISO 27001 (Informationssicherheit)' },
{ value: 'ISO27701', label: 'ISO 27701 (Datenschutz-Erweiterung)' },
{ value: 'TISAX', label: 'TISAX (Automotive)' },
{ value: 'SOC2', label: 'SOC 2 (US-Standard)' },
{ value: 'BSI-Grundschutz', label: 'BSI IT-Grundschutz' },
{ value: 'Keine', label: 'Keine Zertifizierung geplant' },
],
scoreWeights: { risk: 3, complexity: 5, assurance: 10 },
},
],
}
@@ -70,7 +56,7 @@ const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
*/
const BLOCK_2_DATA: ScopeQuestionBlock = {
id: 'data',
title: 'Daten & Betroffene',
title: 'Datenverarbeitung',
description: 'Art und Umfang der verarbeiteten personenbezogenen Daten',
order: 2,
questions: [
@@ -131,21 +117,6 @@ const BLOCK_2_DATA: ScopeQuestionBlock = {
mapsToVVTQuestion: 'dept_finance',
mapsToLFQuestion: 'data-buchhaltung',
},
{
id: 'data_volume',
type: 'single',
question: 'Wie viele Personendatensätze verarbeiten Sie insgesamt?',
helpText: 'Schätzen Sie die Gesamtzahl betroffener Personen',
required: true,
options: [
{ value: '<1000', label: 'Unter 1.000' },
{ value: '1000-10000', label: '1.000 bis 10.000' },
{ value: '10000-100000', label: '10.000 bis 100.000' },
{ value: '100000-1000000', label: '100.000 bis 1 Mio.' },
{ value: '>1000000', label: 'Über 1 Mio.' },
],
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
},
],
}
@@ -224,7 +195,7 @@ const BLOCK_3_PROCESSING: ScopeQuestionBlock = {
*/
const BLOCK_4_TECH: ScopeQuestionBlock = {
id: 'tech',
title: 'Technik, Hosting & Transfers',
title: 'Hosting & Verarbeitung',
description: 'Technische Infrastruktur und Datenübermittlung',
order: 4,
questions: [
@@ -354,7 +325,7 @@ const BLOCK_5_PROCESSES: ScopeQuestionBlock = {
*/
const BLOCK_6_PRODUCT: ScopeQuestionBlock = {
id: 'product',
title: 'Produktkontext',
title: 'Website und Services',
description: 'Spezifische Merkmale Ihrer Produkte und Services',
order: 6,
questions: [
@@ -433,6 +404,20 @@ export const HIDDEN_SCORING_QUESTIONS: ScopeProfilingQuestion[] = [
required: false,
scoreWeights: { risk: 5, complexity: 3, assurance: 6 },
},
{
id: 'org_cert_target',
type: 'multi',
question: 'Zertifizierungen (aus Profil)',
required: false,
scoreWeights: { risk: 3, complexity: 5, assurance: 10 },
},
{
id: 'data_volume',
type: 'single',
question: 'Personendatensaetze (aus Profil)',
required: false,
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
},
{
id: 'prod_type',
type: 'multi',
@@ -494,10 +479,15 @@ const BLOCK_7_AI_SYSTEMS: ScopeQuestionBlock = {
},
{
id: 'ai_risk_assessment',
type: 'boolean',
type: 'single',
question: 'Haben Sie eine KI-Risikobewertung nach EU AI Act durchgeführt?',
helpText: 'Risikoeinstufung der KI-Systeme (verboten / hochriskant / begrenzt / minimal)',
required: false,
options: [
{ value: 'yes', label: 'Ja' },
{ value: 'no', label: 'Nein' },
{ value: 'not_yet', label: 'Noch nicht' },
],
scoreWeights: { risk: -5, complexity: 3, assurance: 8 },
},
],
@@ -668,6 +658,84 @@ const BLOCK_9_DATENKATEGORIEN: ScopeQuestionBlock = {
scoreWeights: { risk: 5, complexity: 3, assurance: 4 },
mapsToVVTQuestion: 'dept_support_categories',
},
{
id: 'dk_dept_it',
type: 'multi',
question: 'Welche Datenkategorien verarbeitet Ihre IT-Abteilung?',
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer IT / Administration',
required: false,
options: DEPARTMENT_DATA_CATEGORIES.dept_it.categories.map(c => ({
value: c.id,
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
})),
scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
mapsToVVTQuestion: 'dept_it_categories',
},
{
id: 'dk_dept_recht',
type: 'multi',
question: 'Welche Datenkategorien verarbeitet Ihre Rechtsabteilung?',
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Recht / Compliance',
required: false,
options: DEPARTMENT_DATA_CATEGORIES.dept_recht.categories.map(c => ({
value: c.id,
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
})),
scoreWeights: { risk: 6, complexity: 4, assurance: 6 },
mapsToVVTQuestion: 'dept_recht_categories',
},
{
id: 'dk_dept_produktion',
type: 'multi',
question: 'Welche Datenkategorien verarbeitet Ihre Produktion?',
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Produktion / Fertigung',
required: false,
options: DEPARTMENT_DATA_CATEGORIES.dept_produktion.categories.map(c => ({
value: c.id,
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
})),
scoreWeights: { risk: 6, complexity: 4, assurance: 5 },
mapsToVVTQuestion: 'dept_produktion_categories',
},
{
id: 'dk_dept_logistik',
type: 'multi',
question: 'Welche Datenkategorien verarbeitet Ihre Logistik?',
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Logistik / Versand',
required: false,
options: DEPARTMENT_DATA_CATEGORIES.dept_logistik.categories.map(c => ({
value: c.id,
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
})),
scoreWeights: { risk: 5, complexity: 3, assurance: 4 },
mapsToVVTQuestion: 'dept_logistik_categories',
},
{
id: 'dk_dept_einkauf',
type: 'multi',
question: 'Welche Datenkategorien verarbeitet Ihr Einkauf?',
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Einkauf / Beschaffung',
required: false,
options: DEPARTMENT_DATA_CATEGORIES.dept_einkauf.categories.map(c => ({
value: c.id,
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
})),
scoreWeights: { risk: 4, complexity: 3, assurance: 4 },
mapsToVVTQuestion: 'dept_einkauf_categories',
},
{
id: 'dk_dept_facility',
type: 'multi',
question: 'Welche Datenkategorien verarbeitet Ihr Facility Management?',
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Facility Management',
required: false,
options: DEPARTMENT_DATA_CATEGORIES.dept_facility.categories.map(c => ({
value: c.id,
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
})),
scoreWeights: { risk: 5, complexity: 3, assurance: 4 },
mapsToVVTQuestion: 'dept_facility_categories',
},
],
}

View File

@@ -380,6 +380,76 @@ export const DEPARTMENT_DATA_CATEGORIES: Record<string, DepartmentDataConfig> =
{ id: 'TECHNICAL_DATA', label: 'Technische Daten', info: 'Systeminfos, Logdateien, Screenshots bei Fehlermeldungen' },
]
},
dept_it: {
label: 'IT / Administration',
icon: '💻',
categories: [
{ id: 'USER_ACCOUNTS', label: 'Benutzerkonten', info: 'Benutzernamen, Passwort-Hashes, Rollen, Berechtigungen', isTypical: true },
{ id: 'LOG_DATA', label: 'Log-/Protokolldaten', info: 'System-Logs, Zugriffsprotokolle, Fehlerprotokolle, IP-Adressen', isTypical: true },
{ id: 'DEVICE_DATA', label: 'Geraetedaten', info: 'Inventar, Seriennummern, MAC-Adressen, zugewiesene Geraete', isTypical: true },
{ id: 'NETWORK_DATA', label: 'Netzwerkdaten', info: 'IP-Adressen, VPN-Verbindungen, Firewall-Logs', isTypical: true },
{ id: 'EMAIL_DATA', label: 'E-Mail-/Kommunikation', info: 'E-Mail-Konten, Verteiler, Archivierung', isTypical: true },
{ id: 'BACKUP_DATA', label: 'Backup-Daten', info: 'Sicherungskopien mit personenbezogenen Inhalten' },
{ id: 'MONITORING_DATA', label: 'Monitoring-Daten', info: 'Systemueberwachung, Performance-Metriken mit Nutzerbezug' },
]
},
dept_recht: {
label: 'Recht / Compliance',
icon: '⚖️',
categories: [
{ id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Vertraege, NDAs, AVVs, Rahmenvereinbarungen', isTypical: true },
{ id: 'NAME', label: 'Ansprechpartner', info: 'Namen, Kontaktdaten von Vertragspartnern und Anwaelten', isTypical: true },
{ id: 'COMPLIANCE_DATA', label: 'Compliance-Daten', info: 'Datenschutzanfragen, Meldungen, Audit-Ergebnisse', isTypical: true },
{ id: 'INCIDENT_DATA', label: 'Vorfallsdaten', info: 'Datenschutzvorfaelle, Beschwerden, Meldungen an Aufsichtsbehoerden' },
{ id: 'CONSENT_DATA', label: 'Einwilligungsdaten', info: 'Consent-Nachweise, Widerrufe, Opt-in/Opt-out-Protokolle' },
{ id: 'CRIMINAL_DATA', label: 'Strafrechtliche Daten', info: 'Fuehrungszeugnisse, Compliance-Pruefungen (Art. 10 DSGVO)', isArt9: true },
]
},
dept_produktion: {
label: 'Produktion / Fertigung',
icon: '🏭',
categories: [
{ id: 'EMPLOYMENT_DATA', label: 'Schichtplaene', info: 'Schichtzuordnung, Arbeitszeiten, Anwesenheitslisten', isTypical: true },
{ id: 'NAME', label: 'Mitarbeiterstammdaten', info: 'Name, Personalnummer, Qualifikation, Maschinenberechtigungen', isTypical: true },
{ id: 'HEALTH_DATA', label: 'Arbeitsschutzdaten', info: 'Arbeitsmedizinische Vorsorge, Unfallmeldungen, Gefahrstoff-Expositionen', isArt9: true },
{ id: 'ACCESS_DATA', label: 'Zugangsdaten', info: 'Zutrittskontrolle, Badge-Protokolle, Bereichsberechtigungen', isTypical: true },
{ id: 'QUALITY_DATA', label: 'Qualitaetsdaten', info: 'Pruefprotokolle mit Pruefernamen, Fehlerberichte' },
{ id: 'PHOTO_VIDEO', label: 'Bild-/Videodaten', info: 'Kameraueberwachung in Produktionsbereichen' },
]
},
dept_logistik: {
label: 'Logistik / Versand',
icon: '🚚',
categories: [
{ id: 'NAME', label: 'Empfaengerdaten', info: 'Name, Lieferadresse, Telefon fuer Zustellung', isTypical: true },
{ id: 'ADDRESS', label: 'Versandadressen', info: 'Liefer-/Abholadressen, Paketshop-Zuordnung', isTypical: true },
{ id: 'TRACKING_DATA', label: 'Sendungsverfolgung', info: 'Tracking-Nummern, Zustellstatus, Lieferzeitfenster', isTypical: true },
{ id: 'DRIVER_DATA', label: 'Fahrerdaten', info: 'Fahrerlaubnis, Touren, GPS-Standortdaten', isTypical: true },
{ id: 'CUSTOMS_DATA', label: 'Zolldaten', info: 'Zollerklaerungen, EORI-Nummern bei internationalem Versand' },
]
},
dept_einkauf: {
label: 'Einkauf / Beschaffung',
icon: '🛒',
categories: [
{ id: 'NAME', label: 'Lieferantenkontakte', info: 'Ansprechpartner, E-Mail, Telefon der Lieferanten', isTypical: true },
{ id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Rahmenvertraege, Bestellungen, Konditionen, Laufzeiten', isTypical: true },
{ id: 'BANK_ACCOUNT', label: 'Bankverbindungen', info: 'IBAN, BIC der Lieferanten fuer Zahlungsabwicklung', isTypical: true },
{ id: 'TAX_ID', label: 'Steuer-IDs', info: 'USt-IdNr., Steuernummer der Lieferanten', isTypical: true },
{ id: 'COMPLIANCE_DATA', label: 'Lieferantenbewertung', info: 'Qualitaetsbewertungen, Audit-Ergebnisse, Zertifizierungen' },
]
},
dept_facility: {
label: 'Facility Management',
icon: '🏢',
categories: [
{ id: 'ACCESS_DATA', label: 'Zutrittsdaten', info: 'Schluesselausgaben, Badge-Protokolle, Zutrittslisten', isTypical: true },
{ id: 'NAME', label: 'Dienstleisterkontakte', info: 'Reinigung, Wartung, Sicherheitsdienst — Namen und Kontaktdaten', isTypical: true },
{ id: 'PHOTO_VIDEO', label: 'Videoueberwachung', info: 'Kameraaufnahmen in/an Gebaeuden, Parkplaetzen', isTypical: true },
{ id: 'VISITOR_DATA', label: 'Besucherdaten', info: 'Name, Firma, Besuchsgrund, Ein-/Austrittszeiten', isTypical: true },
{ id: 'HEALTH_DATA', label: 'Gesundheits-/Sicherheitsdaten', info: 'Unfallmeldungen, Evakuierungslisten, Ersthelfer-Register', isArt9: true },
]
},
}
// =============================================================================