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>
1172 lines
38 KiB
TypeScript
1172 lines
38 KiB
TypeScript
import type {
|
||
ScopeQuestionBlock,
|
||
ScopeQuestionBlockId,
|
||
ScopeProfilingQuestion,
|
||
ScopeProfilingAnswer,
|
||
ComplianceScopeState,
|
||
} from './compliance-scope-types'
|
||
import type { CompanyProfile } from './types'
|
||
import { DEPARTMENT_DATA_CATEGORIES } from './vvt-profiling'
|
||
|
||
/**
|
||
* Block 1: Organisation & Reife
|
||
*/
|
||
/**
|
||
* IDs of questions that are auto-filled from company profile.
|
||
* These are no longer shown as interactive questions but still contribute to scoring.
|
||
*/
|
||
export const PROFILE_AUTOFILL_QUESTION_IDS = [
|
||
'org_employee_count',
|
||
'org_annual_revenue',
|
||
'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: 'Kunden & Nutzer',
|
||
description: 'Informationen zu Ihren Kunden und Nutzern',
|
||
order: 1,
|
||
questions: [
|
||
{
|
||
id: 'org_customer_count',
|
||
type: 'single',
|
||
question: 'Wie viele Kunden/Nutzer betreuen Sie?',
|
||
helpText: 'Schätzen Sie die Anzahl aktiver Kunden oder Nutzer',
|
||
required: true,
|
||
options: [
|
||
{ value: '<100', label: 'Weniger als 100' },
|
||
{ value: '100-1000', label: '100 bis 1.000' },
|
||
{ value: '1000-10000', label: '1.000 bis 10.000' },
|
||
{ value: '10000-100000', label: '10.000 bis 100.000' },
|
||
{ value: '100000+', label: 'Mehr als 100.000' },
|
||
],
|
||
scoreWeights: { risk: 6, complexity: 7, assurance: 6 },
|
||
},
|
||
],
|
||
}
|
||
|
||
/**
|
||
* Block 2: Daten & Betroffene
|
||
*/
|
||
const BLOCK_2_DATA: ScopeQuestionBlock = {
|
||
id: 'data',
|
||
title: 'Datenverarbeitung',
|
||
description: 'Art und Umfang der verarbeiteten personenbezogenen Daten',
|
||
order: 2,
|
||
questions: [
|
||
{
|
||
id: 'data_minors',
|
||
type: 'boolean',
|
||
question: 'Verarbeiten Sie Daten von Minderjährigen?',
|
||
helpText: 'Besondere Schutzpflichten für unter 16-Jährige (bzw. 13-Jährige bei Online-Diensten)',
|
||
required: true,
|
||
scoreWeights: { risk: 10, complexity: 5, assurance: 7 },
|
||
mapsToVVTQuestion: 'data_minors',
|
||
},
|
||
{
|
||
id: 'data_art9',
|
||
type: 'multi',
|
||
question: 'Verarbeiten Sie besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)?',
|
||
helpText: 'Diese Daten unterliegen erhöhten Schutzanforderungen',
|
||
required: true,
|
||
options: [
|
||
{ value: 'gesundheit', label: 'Gesundheitsdaten' },
|
||
{ value: 'biometrie', label: 'Biometrische Daten (z.B. Fingerabdruck, Gesichtserkennung)' },
|
||
{ value: 'genetik', label: 'Genetische Daten' },
|
||
{ value: 'politisch', label: 'Politische Meinungen' },
|
||
{ value: 'religion', label: 'Religiöse/weltanschauliche Überzeugungen' },
|
||
{ value: 'gewerkschaft', label: 'Gewerkschaftszugehörigkeit' },
|
||
{ value: 'sexualleben', label: 'Sexualleben/sexuelle Orientierung' },
|
||
{ value: 'strafrechtlich', label: 'Strafrechtliche Verurteilungen/Straftaten' },
|
||
{ value: 'ethnisch', label: 'Ethnische Herkunft' },
|
||
],
|
||
scoreWeights: { risk: 10, complexity: 8, assurance: 9 },
|
||
mapsToVVTQuestion: 'data_health',
|
||
},
|
||
{
|
||
id: 'data_hr',
|
||
type: 'boolean',
|
||
question: 'Verarbeiten Sie Personaldaten (HR)?',
|
||
helpText: 'Bewerberdaten, Gehälter, Leistungsbeurteilungen etc.',
|
||
required: true,
|
||
scoreWeights: { risk: 6, complexity: 4, assurance: 5 },
|
||
mapsToVVTQuestion: 'dept_hr',
|
||
mapsToLFQuestion: 'data-hr',
|
||
},
|
||
{
|
||
id: 'data_communication',
|
||
type: 'boolean',
|
||
question: 'Verarbeiten Sie Kommunikationsdaten (E-Mail, Chat, Telefonie)?',
|
||
helpText: 'Inhalte oder Metadaten von Kommunikationsvorgängen',
|
||
required: true,
|
||
scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
|
||
},
|
||
{
|
||
id: 'data_financial',
|
||
type: 'boolean',
|
||
question: 'Verarbeiten Sie Finanzdaten (Konten, Zahlungen)?',
|
||
helpText: 'Bankdaten, Kreditkartendaten, Buchhaltungsdaten',
|
||
required: true,
|
||
scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
|
||
mapsToVVTQuestion: 'dept_finance',
|
||
mapsToLFQuestion: 'data-buchhaltung',
|
||
},
|
||
],
|
||
}
|
||
|
||
/**
|
||
* Block 3: Verarbeitung & Zweck
|
||
*/
|
||
const BLOCK_3_PROCESSING: ScopeQuestionBlock = {
|
||
id: 'processing',
|
||
title: 'Verarbeitung & Zweck',
|
||
description: 'Wie und wofür werden personenbezogene Daten verarbeitet?',
|
||
order: 3,
|
||
questions: [
|
||
{
|
||
id: 'proc_tracking',
|
||
type: 'boolean',
|
||
question: 'Setzen Sie Tracking oder Profiling ein?',
|
||
helpText: 'Web-Analytics, Werbe-Tracking, Nutzungsprofile etc.',
|
||
required: true,
|
||
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
|
||
},
|
||
{
|
||
id: 'proc_adm_scoring',
|
||
type: 'boolean',
|
||
question: 'Treffen Sie automatisierte Entscheidungen (Art. 22 DSGVO)?',
|
||
helpText: 'Scoring, Bonitätsprüfung, automatische Ablehnung ohne menschliche Beteiligung',
|
||
required: true,
|
||
scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
|
||
},
|
||
{
|
||
id: 'proc_ai_usage',
|
||
type: 'multi',
|
||
question: 'Setzen Sie KI-Systeme ein?',
|
||
helpText: 'KI-Einsatz kann zusätzliche Anforderungen (EU AI Act) auslösen',
|
||
required: true,
|
||
options: [
|
||
{ value: 'keine', label: 'Keine KI im Einsatz' },
|
||
{ value: 'chatbot', label: 'Chatbots/Virtuelle Assistenten' },
|
||
{ value: 'scoring', label: 'Scoring/Risikobewertung' },
|
||
{ value: 'profiling', label: 'Profiling/Verhaltensvorhersage' },
|
||
{ value: 'generativ', label: 'Generative KI (Text, Bild, Code)' },
|
||
{ value: 'autonom', label: 'Autonome Systeme/Entscheidungen' },
|
||
],
|
||
scoreWeights: { risk: 8, complexity: 9, assurance: 7 },
|
||
},
|
||
{
|
||
id: 'proc_data_combination',
|
||
type: 'boolean',
|
||
question: 'Führen Sie Daten aus verschiedenen Quellen zusammen?',
|
||
helpText: 'Data Matching, Anreicherung aus externen Quellen',
|
||
required: true,
|
||
scoreWeights: { risk: 7, complexity: 7, assurance: 6 },
|
||
},
|
||
{
|
||
id: 'proc_employee_monitoring',
|
||
type: 'boolean',
|
||
question: 'Überwachen Sie Mitarbeiter (Zeiterfassung, Standort, IT-Nutzung)?',
|
||
helpText: 'Beschäftigtendatenschutz nach § 26 BDSG',
|
||
required: true,
|
||
scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
|
||
},
|
||
{
|
||
id: 'proc_video_surveillance',
|
||
type: 'boolean',
|
||
question: 'Setzen Sie Videoüberwachung ein?',
|
||
helpText: 'Kameras in Büros, Produktionsstätten, Verkaufsräumen etc.',
|
||
required: true,
|
||
scoreWeights: { risk: 8, complexity: 5, assurance: 7 },
|
||
mapsToVVTQuestion: 'special_video_surveillance',
|
||
mapsToLFQuestion: 'data-video',
|
||
},
|
||
],
|
||
}
|
||
|
||
/**
|
||
* Block 4: Technik/Hosting/Transfers
|
||
*/
|
||
const BLOCK_4_TECH: ScopeQuestionBlock = {
|
||
id: 'tech',
|
||
title: 'Hosting & Verarbeitung',
|
||
description: 'Technische Infrastruktur und Datenübermittlung',
|
||
order: 4,
|
||
questions: [
|
||
{
|
||
id: 'tech_hosting_location',
|
||
type: 'single',
|
||
question: 'Wo werden Ihre Daten primär gehostet?',
|
||
helpText: 'Standort bestimmt anwendbares Datenschutzrecht',
|
||
required: true,
|
||
options: [
|
||
{ value: 'de', label: 'Deutschland' },
|
||
{ value: 'eu', label: 'EU (ohne Deutschland)' },
|
||
{ value: 'ewr', label: 'EWR (z.B. Norwegen, Island)' },
|
||
{ value: 'us_adequacy', label: 'USA (mit Angemessenheitsbeschluss/DPF)' },
|
||
{ value: 'drittland', label: 'Drittland ohne Angemessenheitsbeschluss' },
|
||
],
|
||
scoreWeights: { risk: 7, complexity: 6, assurance: 7 },
|
||
},
|
||
{
|
||
id: 'tech_subprocessors',
|
||
type: 'boolean',
|
||
question: 'Nutzen Sie Auftragsverarbeiter (externe Dienstleister)?',
|
||
helpText: 'Cloud-Anbieter, Hosting, E-Mail-Service, CRM etc. – erfordert AVV nach Art. 28 DSGVO',
|
||
required: true,
|
||
scoreWeights: { risk: 6, complexity: 7, assurance: 7 },
|
||
},
|
||
{
|
||
id: 'tech_third_country',
|
||
type: 'boolean',
|
||
question: 'Übermitteln Sie Daten in Drittländer?',
|
||
helpText: 'Transfer außerhalb EU/EWR erfordert Schutzmaßnahmen (SCC, BCR etc.)',
|
||
required: true,
|
||
scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
|
||
mapsToVVTQuestion: 'transfer_cloud_us',
|
||
},
|
||
{
|
||
id: 'tech_encryption_rest',
|
||
type: 'boolean',
|
||
question: 'Sind Daten im Ruhezustand verschlüsselt (at rest)?',
|
||
helpText: 'Datenbank-, Dateisystem- oder Volume-Verschlüsselung',
|
||
required: true,
|
||
scoreWeights: { risk: -5, complexity: 3, assurance: 7 },
|
||
},
|
||
{
|
||
id: 'tech_encryption_transit',
|
||
type: 'boolean',
|
||
question: 'Sind Daten bei Übertragung verschlüsselt (in transit)?',
|
||
helpText: 'TLS/SSL für alle Verbindungen',
|
||
required: true,
|
||
scoreWeights: { risk: -5, complexity: 2, assurance: 7 },
|
||
},
|
||
{
|
||
id: 'tech_cloud_providers',
|
||
type: 'multi',
|
||
question: 'Welche Cloud-Anbieter nutzen Sie?',
|
||
helpText: 'Mehrfachauswahl möglich',
|
||
required: false,
|
||
options: [
|
||
{ value: 'aws', label: 'Amazon Web Services (AWS)' },
|
||
{ value: 'azure', label: 'Microsoft Azure' },
|
||
{ value: 'gcp', label: 'Google Cloud Platform (GCP)' },
|
||
{ value: 'hetzner', label: 'Hetzner' },
|
||
{ value: 'ionos', label: 'IONOS' },
|
||
{ value: 'ovh', label: 'OVH' },
|
||
{ value: 'andere', label: 'Andere Anbieter' },
|
||
{ value: 'keine', label: 'Keine Cloud-Nutzung (On-Premise)' },
|
||
],
|
||
scoreWeights: { risk: 5, complexity: 6, assurance: 6 },
|
||
},
|
||
],
|
||
}
|
||
|
||
/**
|
||
* Block 5: Rechte & Prozesse
|
||
*/
|
||
const BLOCK_5_PROCESSES: ScopeQuestionBlock = {
|
||
id: 'processes',
|
||
title: 'Rechte & Prozesse',
|
||
description: 'Etablierte Datenschutz- und Sicherheitsprozesse',
|
||
order: 5,
|
||
questions: [
|
||
{
|
||
id: 'proc_dsar_process',
|
||
type: 'boolean',
|
||
question: 'Haben Sie einen Prozess für Betroffenenrechte (DSAR)?',
|
||
helpText: 'Auskunft, Löschung, Berichtigung, Widerspruch etc. – Art. 15-22 DSGVO',
|
||
required: true,
|
||
scoreWeights: { risk: 6, complexity: 5, assurance: 8 },
|
||
},
|
||
{
|
||
id: 'proc_deletion_concept',
|
||
type: 'boolean',
|
||
question: 'Haben Sie ein Löschkonzept?',
|
||
helpText: 'Definierte Löschfristen und automatisierte Löschroutinen',
|
||
required: true,
|
||
scoreWeights: { risk: 7, complexity: 6, assurance: 8 },
|
||
},
|
||
{
|
||
id: 'proc_incident_response',
|
||
type: 'boolean',
|
||
question: 'Haben Sie einen Notfallplan für Datenschutzvorfälle?',
|
||
helpText: 'Incident Response Plan, 72h-Meldepflicht an Aufsichtsbehörde (Art. 33 DSGVO)',
|
||
required: true,
|
||
scoreWeights: { risk: 8, complexity: 6, assurance: 9 },
|
||
},
|
||
{
|
||
id: 'proc_regular_audits',
|
||
type: 'boolean',
|
||
question: 'Führen Sie regelmäßige Datenschutz-Audits durch?',
|
||
helpText: 'Interne oder externe Prüfungen mindestens jährlich',
|
||
required: true,
|
||
scoreWeights: { risk: 5, complexity: 4, assurance: 9 },
|
||
},
|
||
{
|
||
id: 'proc_training',
|
||
type: 'boolean',
|
||
question: 'Schulen Sie Ihre Mitarbeiter im Datenschutz?',
|
||
helpText: 'Awareness-Trainings, Onboarding, jährliche Auffrischung',
|
||
required: true,
|
||
scoreWeights: { risk: 6, complexity: 3, assurance: 7 },
|
||
},
|
||
],
|
||
}
|
||
|
||
/**
|
||
* Block 6: Produktkontext
|
||
*/
|
||
const BLOCK_6_PRODUCT: ScopeQuestionBlock = {
|
||
id: 'product',
|
||
title: 'Website und Services',
|
||
description: 'Spezifische Merkmale Ihrer Produkte und Services',
|
||
order: 6,
|
||
questions: [
|
||
{
|
||
id: 'prod_cookies_consent',
|
||
type: 'boolean',
|
||
question: 'Benötigen Sie Cookie-Consent (Tracking-Cookies)?',
|
||
helpText: 'Nicht-essenzielle Cookies erfordern opt-in Einwilligung',
|
||
required: true,
|
||
scoreWeights: { risk: 5, complexity: 4, assurance: 6 },
|
||
},
|
||
{
|
||
id: 'prod_api_external',
|
||
type: 'boolean',
|
||
question: 'Bieten Sie externe APIs an (Daten-Weitergabe an Dritte)?',
|
||
helpText: 'Programmierschnittstellen für Partner, Entwickler etc.',
|
||
required: true,
|
||
scoreWeights: { risk: 7, complexity: 7, assurance: 7 },
|
||
},
|
||
{
|
||
id: 'prod_data_broker',
|
||
type: 'boolean',
|
||
question: 'Handeln Sie mit Daten (Data Brokerage, Adresshandel)?',
|
||
helpText: 'Verkauf oder Vermittlung personenbezogener Daten',
|
||
required: true,
|
||
scoreWeights: { risk: 10, complexity: 8, assurance: 9 },
|
||
},
|
||
],
|
||
}
|
||
|
||
/**
|
||
* Hidden questions — removed from UI but still contribute to scoring.
|
||
* These are auto-filled from the Company Profile.
|
||
*/
|
||
export const HIDDEN_SCORING_QUESTIONS: ScopeProfilingQuestion[] = [
|
||
{
|
||
id: 'org_employee_count',
|
||
type: 'number',
|
||
question: 'Mitarbeiterzahl (aus Profil)',
|
||
required: false,
|
||
scoreWeights: { risk: 5, complexity: 8, assurance: 6 },
|
||
mapsToCompanyProfile: 'employeeCount',
|
||
},
|
||
{
|
||
id: 'org_annual_revenue',
|
||
type: 'single',
|
||
question: 'Jahresumsatz (aus Profil)',
|
||
required: false,
|
||
scoreWeights: { risk: 4, complexity: 6, assurance: 7 },
|
||
mapsToCompanyProfile: 'annualRevenue',
|
||
},
|
||
{
|
||
id: 'org_industry',
|
||
type: 'single',
|
||
question: 'Branche (aus Profil)',
|
||
required: false,
|
||
scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
|
||
mapsToCompanyProfile: 'industry',
|
||
mapsToVVTQuestion: 'org_industry',
|
||
mapsToLFQuestion: 'org-branche',
|
||
},
|
||
{
|
||
id: 'org_business_model',
|
||
type: 'single',
|
||
question: 'Geschäftsmodell (aus Profil)',
|
||
required: false,
|
||
scoreWeights: { risk: 6, complexity: 5, assurance: 5 },
|
||
mapsToCompanyProfile: 'businessModel',
|
||
mapsToVVTQuestion: 'org_b2b_b2c',
|
||
mapsToLFQuestion: 'org-geschaeftsmodell',
|
||
},
|
||
{
|
||
id: 'org_has_dsb',
|
||
type: 'boolean',
|
||
question: 'DSB vorhanden (aus Profil)',
|
||
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',
|
||
question: 'Angebotstypen (aus Profil)',
|
||
required: false,
|
||
scoreWeights: { risk: 5, complexity: 6, assurance: 5 },
|
||
},
|
||
{
|
||
id: 'prod_webshop',
|
||
type: 'boolean',
|
||
question: 'Webshop (aus Profil)',
|
||
required: false,
|
||
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
|
||
},
|
||
]
|
||
|
||
/**
|
||
* Block 7: KI-Systeme (portiert aus Company Profile Step 7)
|
||
*/
|
||
const BLOCK_7_AI_SYSTEMS: ScopeQuestionBlock = {
|
||
id: 'ai_systems',
|
||
title: 'KI-Systeme',
|
||
description: 'Erfassung eingesetzter KI-Systeme für EU AI Act und DSGVO-Dokumentation',
|
||
order: 7,
|
||
questions: [
|
||
{
|
||
id: 'ai_uses_ai',
|
||
type: 'boolean',
|
||
question: 'Setzt Ihr Unternehmen KI-Systeme ein?',
|
||
helpText: 'Chatbots, Empfehlungssysteme, automatisierte Entscheidungen, Copilot, etc.',
|
||
required: true,
|
||
scoreWeights: { risk: 8, complexity: 7, assurance: 6 },
|
||
},
|
||
{
|
||
id: 'ai_categories',
|
||
type: 'multi',
|
||
question: 'Welche Kategorien von KI-Systemen setzen Sie ein?',
|
||
helpText: 'Mehrfachauswahl möglich. Wird nur angezeigt, wenn KI im Einsatz ist.',
|
||
required: false,
|
||
options: [
|
||
{ value: 'chatbot', label: 'Text-KI / Chatbots (ChatGPT, Claude, Gemini)' },
|
||
{ value: 'office', label: 'Office / Produktivität (Copilot, Workspace AI)' },
|
||
{ value: 'code', label: 'Code-Assistenz (GitHub Copilot, Cursor)' },
|
||
{ value: 'image', label: 'Bildgenerierung (DALL-E, Midjourney, Firefly)' },
|
||
{ value: 'translation', label: 'Übersetzung / Sprache (DeepL)' },
|
||
{ value: 'crm', label: 'CRM / Sales KI (Salesforce Einstein, HubSpot AI)' },
|
||
{ value: 'internal', label: 'Eigene / interne KI-Systeme' },
|
||
{ value: 'other', label: 'Sonstige KI-Systeme' },
|
||
],
|
||
scoreWeights: { risk: 5, complexity: 5, assurance: 5 },
|
||
},
|
||
{
|
||
id: 'ai_personal_data',
|
||
type: 'boolean',
|
||
question: 'Werden personenbezogene Daten an KI-Systeme übermittelt?',
|
||
helpText: 'Z.B. Kundendaten in ChatGPT eingeben, E-Mails mit Copilot verarbeiten',
|
||
required: false,
|
||
scoreWeights: { risk: 10, complexity: 5, assurance: 7 },
|
||
},
|
||
{
|
||
id: 'ai_risk_assessment',
|
||
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 },
|
||
},
|
||
],
|
||
}
|
||
|
||
/**
|
||
* Block 8: Verarbeitungstätigkeiten (portiert aus Company Profile Step 6)
|
||
*/
|
||
const BLOCK_8_VVT: ScopeQuestionBlock = {
|
||
id: 'vvt',
|
||
title: 'Verarbeitungstätigkeiten',
|
||
description: 'Übersicht der Datenverarbeitungen nach Art. 30 DSGVO',
|
||
order: 8,
|
||
questions: [
|
||
{
|
||
id: 'vvt_departments',
|
||
type: 'multi',
|
||
question: 'In welchen Abteilungen werden personenbezogene Daten verarbeitet?',
|
||
helpText: 'Wählen Sie alle Abteilungen, in denen Verarbeitungstätigkeiten stattfinden',
|
||
required: true,
|
||
options: [
|
||
{ value: 'personal', label: 'Personal / HR' },
|
||
{ value: 'finanzen', label: 'Finanzen / Buchhaltung' },
|
||
{ value: 'vertrieb', label: 'Vertrieb / Sales' },
|
||
{ value: 'marketing', label: 'Marketing' },
|
||
{ value: 'it', label: 'IT / Administration' },
|
||
{ value: 'recht', label: 'Recht / Compliance' },
|
||
{ value: 'kundenservice', label: 'Kundenservice / Support' },
|
||
{ value: 'produktion', label: 'Produktion / Fertigung' },
|
||
{ value: 'logistik', label: 'Logistik / Versand' },
|
||
{ value: 'einkauf', label: 'Einkauf / Beschaffung' },
|
||
{ value: 'facility', label: 'Facility Management' },
|
||
],
|
||
scoreWeights: { risk: 10, complexity: 10, assurance: 8 },
|
||
},
|
||
{
|
||
id: 'vvt_data_categories',
|
||
type: 'multi',
|
||
question: 'Welche Datenkategorien werden verarbeitet?',
|
||
helpText: 'Wählen Sie alle zutreffenden Kategorien personenbezogener Daten',
|
||
required: true,
|
||
options: [
|
||
{ value: 'stammdaten', label: 'Stammdaten (Name, Geburtsdatum)' },
|
||
{ value: 'kontaktdaten', label: 'Kontaktdaten (E-Mail, Telefon, Adresse)' },
|
||
{ value: 'vertragsdaten', label: 'Vertragsdaten' },
|
||
{ value: 'zahlungsdaten', label: 'Zahlungs-/Bankdaten' },
|
||
{ value: 'beschaeftigtendaten', label: 'Beschäftigtendaten (Gehalt, Arbeitszeiten)' },
|
||
{ value: 'kommunikation', label: 'Kommunikationsdaten (E-Mail, Chat)' },
|
||
{ value: 'nutzungsdaten', label: 'Nutzungs-/Logdaten (IP, Klicks)' },
|
||
{ value: 'standortdaten', label: 'Standortdaten' },
|
||
{ value: 'bilddaten', label: 'Bild-/Videodaten' },
|
||
{ value: 'bewerberdaten', label: 'Bewerberdaten' },
|
||
],
|
||
scoreWeights: { risk: 8, complexity: 7, assurance: 7 },
|
||
},
|
||
{
|
||
id: 'vvt_special_categories',
|
||
type: 'boolean',
|
||
question: 'Verarbeiten Sie besondere Kategorien (Art. 9 DSGVO) in Ihren Tätigkeiten?',
|
||
helpText: 'Gesundheit, Biometrie, Religion, Gewerkschaft — über die bereits in Block 2 erfassten hinaus',
|
||
required: true,
|
||
scoreWeights: { risk: 10, complexity: 5, assurance: 8 },
|
||
},
|
||
{
|
||
id: 'vvt_has_vvt',
|
||
type: 'boolean',
|
||
question: 'Haben Sie bereits ein Verarbeitungsverzeichnis (VVT)?',
|
||
helpText: 'Dokumentation aller Verarbeitungstätigkeiten nach Art. 30 DSGVO',
|
||
required: true,
|
||
scoreWeights: { risk: -5, complexity: 3, assurance: 8 },
|
||
},
|
||
{
|
||
id: 'vvt_external_processors',
|
||
type: 'boolean',
|
||
question: 'Setzen Sie externe Dienstleister als Auftragsverarbeiter ein?',
|
||
helpText: 'Lohnbüro, Hosting-Provider, Cloud-Dienste, externe IT etc.',
|
||
required: true,
|
||
scoreWeights: { risk: 7, complexity: 6, assurance: 7 },
|
||
},
|
||
],
|
||
}
|
||
|
||
/**
|
||
* Block 9: Datenkategorien pro Abteilung
|
||
* Generiert Fragen dynamisch aus DEPARTMENT_DATA_CATEGORIES
|
||
*/
|
||
const BLOCK_9_DATENKATEGORIEN: ScopeQuestionBlock = {
|
||
id: 'datenkategorien_detail',
|
||
title: 'Datenkategorien pro Abteilung',
|
||
description: 'Detaillierte Erfassung der Datenkategorien je Abteilung — basierend auf Ihrer Abteilungswahl in Block 8',
|
||
order: 9,
|
||
questions: [
|
||
{
|
||
id: 'dk_dept_hr',
|
||
type: 'multi',
|
||
question: 'Welche Datenkategorien verarbeitet Ihre Personalabteilung?',
|
||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer den HR-Bereich',
|
||
required: false,
|
||
options: DEPARTMENT_DATA_CATEGORIES.dept_hr.categories.map(c => ({
|
||
value: c.id,
|
||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||
})),
|
||
scoreWeights: { risk: 6, complexity: 4, assurance: 5 },
|
||
mapsToVVTQuestion: 'dept_hr_categories',
|
||
},
|
||
{
|
||
id: 'dk_dept_recruiting',
|
||
type: 'multi',
|
||
question: 'Welche Datenkategorien verarbeitet Ihr Recruiting?',
|
||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer das Bewerbermanagement',
|
||
required: false,
|
||
options: DEPARTMENT_DATA_CATEGORIES.dept_recruiting.categories.map(c => ({
|
||
value: c.id,
|
||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||
})),
|
||
scoreWeights: { risk: 5, complexity: 3, assurance: 4 },
|
||
mapsToVVTQuestion: 'dept_recruiting_categories',
|
||
},
|
||
{
|
||
id: 'dk_dept_finance',
|
||
type: 'multi',
|
||
question: 'Welche Datenkategorien verarbeitet Ihre Finanzabteilung?',
|
||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Finanzen & Buchhaltung',
|
||
required: false,
|
||
options: DEPARTMENT_DATA_CATEGORIES.dept_finance.categories.map(c => ({
|
||
value: c.id,
|
||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||
})),
|
||
scoreWeights: { risk: 6, complexity: 4, assurance: 5 },
|
||
mapsToVVTQuestion: 'dept_finance_categories',
|
||
},
|
||
{
|
||
id: 'dk_dept_sales',
|
||
type: 'multi',
|
||
question: 'Welche Datenkategorien verarbeitet Ihr Vertrieb?',
|
||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Vertrieb & CRM',
|
||
required: false,
|
||
options: DEPARTMENT_DATA_CATEGORIES.dept_sales.categories.map(c => ({
|
||
value: c.id,
|
||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||
})),
|
||
scoreWeights: { risk: 5, complexity: 4, assurance: 4 },
|
||
mapsToVVTQuestion: 'dept_sales_categories',
|
||
},
|
||
{
|
||
id: 'dk_dept_marketing',
|
||
type: 'multi',
|
||
question: 'Welche Datenkategorien verarbeitet Ihr Marketing?',
|
||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Marketing',
|
||
required: false,
|
||
options: DEPARTMENT_DATA_CATEGORIES.dept_marketing.categories.map(c => ({
|
||
value: c.id,
|
||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||
})),
|
||
scoreWeights: { risk: 6, complexity: 5, assurance: 5 },
|
||
mapsToVVTQuestion: 'dept_marketing_categories',
|
||
},
|
||
{
|
||
id: 'dk_dept_support',
|
||
type: 'multi',
|
||
question: 'Welche Datenkategorien verarbeitet Ihr Kundenservice?',
|
||
helpText: 'Waehlen Sie alle zutreffenden Datenkategorien fuer Support',
|
||
required: false,
|
||
options: DEPARTMENT_DATA_CATEGORIES.dept_support.categories.map(c => ({
|
||
value: c.id,
|
||
label: `${c.label}${c.isArt9 ? ' (Art. 9)' : ''}`,
|
||
})),
|
||
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',
|
||
},
|
||
],
|
||
}
|
||
|
||
/**
|
||
* All question blocks in order
|
||
*/
|
||
export const SCOPE_QUESTION_BLOCKS: ScopeQuestionBlock[] = [
|
||
BLOCK_1_ORGANISATION,
|
||
BLOCK_2_DATA,
|
||
BLOCK_3_PROCESSING,
|
||
BLOCK_4_TECH,
|
||
BLOCK_5_PROCESSES,
|
||
BLOCK_6_PRODUCT,
|
||
BLOCK_7_AI_SYSTEMS,
|
||
BLOCK_8_VVT,
|
||
BLOCK_9_DATENKATEGORIEN,
|
||
]
|
||
|
||
/**
|
||
* Prefill scope answers from CompanyProfile.
|
||
*
|
||
* Questions that were removed from the UI (org_employee_count, org_annual_revenue,
|
||
* org_industry, org_business_model, org_has_dsb, prod_type, prod_webshop) are
|
||
* still auto-filled here so their scoreWeights continue to affect the scoring.
|
||
*/
|
||
export function prefillFromCompanyProfile(
|
||
profile: CompanyProfile
|
||
): ScopeProfilingAnswer[] {
|
||
const answers: ScopeProfilingAnswer[] = []
|
||
|
||
// dpoName -> org_has_dsb (auto-filled, not shown in UI)
|
||
if (profile.dpoName && profile.dpoName.trim() !== '') {
|
||
answers.push({
|
||
questionId: 'org_has_dsb',
|
||
value: true,
|
||
})
|
||
}
|
||
|
||
// offerings -> prod_type mapping (auto-filled, not shown in UI)
|
||
if (profile.offerings && profile.offerings.length > 0) {
|
||
const prodTypes: string[] = []
|
||
const offeringsLower = profile.offerings.map((o) => o.toLowerCase())
|
||
|
||
if (offeringsLower.some((o) => o.includes('webapp') || o.includes('web'))) {
|
||
prodTypes.push('webapp')
|
||
}
|
||
if (
|
||
offeringsLower.some((o) => o.includes('mobile') || o.includes('app'))
|
||
) {
|
||
prodTypes.push('mobile')
|
||
}
|
||
if (offeringsLower.some((o) => o.includes('saas') || o.includes('cloud'))) {
|
||
prodTypes.push('saas')
|
||
}
|
||
if (
|
||
offeringsLower.some(
|
||
(o) => o.includes('onpremise') || o.includes('on-premise')
|
||
)
|
||
) {
|
||
prodTypes.push('onpremise')
|
||
}
|
||
if (offeringsLower.some((o) => o.includes('api'))) {
|
||
prodTypes.push('api')
|
||
}
|
||
if (offeringsLower.some((o) => o.includes('iot') || o.includes('hardware'))) {
|
||
prodTypes.push('iot')
|
||
}
|
||
if (
|
||
offeringsLower.some(
|
||
(o) => o.includes('beratung') || o.includes('consulting')
|
||
)
|
||
) {
|
||
prodTypes.push('beratung')
|
||
}
|
||
if (
|
||
offeringsLower.some(
|
||
(o) => o.includes('handel') || o.includes('shop') || o.includes('commerce')
|
||
)
|
||
) {
|
||
prodTypes.push('handel')
|
||
}
|
||
|
||
if (prodTypes.length > 0) {
|
||
answers.push({
|
||
questionId: 'prod_type',
|
||
value: prodTypes,
|
||
})
|
||
}
|
||
|
||
// webshop auto-fill
|
||
if (offeringsLower.some((o) => o.includes('webshop') || o.includes('shop'))) {
|
||
answers.push({
|
||
questionId: 'prod_webshop',
|
||
value: true,
|
||
})
|
||
}
|
||
}
|
||
|
||
return answers
|
||
}
|
||
|
||
/**
|
||
* Get auto-filled scoring values for questions removed from UI.
|
||
* These contribute to scoring even though the user doesn't answer them interactively.
|
||
*/
|
||
export function getAutoFilledScoringAnswers(
|
||
profile: CompanyProfile
|
||
): ScopeProfilingAnswer[] {
|
||
const answers: ScopeProfilingAnswer[] = []
|
||
|
||
// employeeCount -> org_employee_count
|
||
if (profile.employeeCount != null) {
|
||
answers.push({
|
||
questionId: 'org_employee_count',
|
||
value: profile.employeeCount,
|
||
})
|
||
}
|
||
|
||
// annualRevenue -> org_annual_revenue
|
||
if (profile.annualRevenue) {
|
||
answers.push({
|
||
questionId: 'org_annual_revenue',
|
||
value: profile.annualRevenue,
|
||
})
|
||
}
|
||
|
||
// industry -> org_industry
|
||
if (profile.industry && profile.industry.length > 0) {
|
||
answers.push({
|
||
questionId: 'org_industry',
|
||
value: profile.industry.join(', '),
|
||
})
|
||
}
|
||
|
||
// businessModel -> org_business_model
|
||
if (profile.businessModel) {
|
||
answers.push({
|
||
questionId: 'org_business_model',
|
||
value: profile.businessModel,
|
||
})
|
||
}
|
||
|
||
// dpoName -> org_has_dsb
|
||
if (profile.dpoName && profile.dpoName.trim() !== '') {
|
||
answers.push({
|
||
questionId: 'org_has_dsb',
|
||
value: true,
|
||
})
|
||
}
|
||
|
||
return answers
|
||
}
|
||
|
||
/**
|
||
* Get profile info summary for display in "Aus Profil" info boxes.
|
||
*/
|
||
export function getProfileInfoForBlock(
|
||
profile: CompanyProfile,
|
||
blockId: ScopeQuestionBlockId
|
||
): { label: string; value: string }[] {
|
||
const items: { label: string; value: string }[] = []
|
||
|
||
if (blockId === 'organisation') {
|
||
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.annualRevenue) items.push({ label: 'Umsatz', value: profile.annualRevenue })
|
||
if (profile.businessModel) items.push({ label: 'Geschäftsmodell', value: profile.businessModel })
|
||
if (profile.dpoName) items.push({ label: 'DSB', value: profile.dpoName })
|
||
}
|
||
|
||
if (blockId === 'product') {
|
||
if (profile.offerings && profile.offerings.length > 0) {
|
||
items.push({ label: 'Angebote', value: profile.offerings.join(', ') })
|
||
}
|
||
const hasWebshop = profile.offerings?.some(o => o.toLowerCase().includes('webshop') || o.toLowerCase().includes('shop'))
|
||
if (hasWebshop) items.push({ label: 'Webshop', value: 'Ja' })
|
||
}
|
||
|
||
return items
|
||
}
|
||
|
||
/**
|
||
* Prefill scope answers from VVT profiling answers
|
||
*/
|
||
export function prefillFromVVTAnswers(
|
||
vvtAnswers: Record<string, unknown>
|
||
): ScopeProfilingAnswer[] {
|
||
const answers: ScopeProfilingAnswer[] = []
|
||
|
||
// Build reverse mapping: VVT question -> Scope question
|
||
const reverseMap: Record<string, string> = {}
|
||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||
for (const q of block.questions) {
|
||
if (q.mapsToVVTQuestion) {
|
||
reverseMap[q.mapsToVVTQuestion] = q.id
|
||
}
|
||
}
|
||
}
|
||
|
||
// Map VVT answers to scope answers
|
||
for (const [vvtQuestionId, vvtValue] of Object.entries(vvtAnswers)) {
|
||
const scopeQuestionId = reverseMap[vvtQuestionId]
|
||
if (scopeQuestionId) {
|
||
answers.push({
|
||
questionId: scopeQuestionId,
|
||
value: vvtValue,
|
||
})
|
||
}
|
||
}
|
||
|
||
return answers
|
||
}
|
||
|
||
/**
|
||
* Prefill scope answers from Loeschfristen profiling answers
|
||
*/
|
||
export function prefillFromLoeschfristenAnswers(
|
||
lfAnswers: Array<{ questionId: string; value: unknown }>
|
||
): ScopeProfilingAnswer[] {
|
||
const answers: ScopeProfilingAnswer[] = []
|
||
|
||
// Build reverse mapping: LF question -> Scope question
|
||
const reverseMap: Record<string, string> = {}
|
||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||
for (const q of block.questions) {
|
||
if (q.mapsToLFQuestion) {
|
||
reverseMap[q.mapsToLFQuestion] = q.id
|
||
}
|
||
}
|
||
}
|
||
|
||
// Map LF answers to scope answers
|
||
for (const lfAnswer of lfAnswers) {
|
||
const scopeQuestionId = reverseMap[lfAnswer.questionId]
|
||
if (scopeQuestionId) {
|
||
answers.push({
|
||
questionId: scopeQuestionId,
|
||
value: lfAnswer.value,
|
||
})
|
||
}
|
||
}
|
||
|
||
return answers
|
||
}
|
||
|
||
/**
|
||
* Export scope answers in VVT format
|
||
*/
|
||
export function exportToVVTAnswers(
|
||
scopeAnswers: ScopeProfilingAnswer[]
|
||
): Record<string, unknown> {
|
||
const vvtAnswers: Record<string, unknown> = {}
|
||
|
||
for (const answer of scopeAnswers) {
|
||
// Find the question
|
||
let question: ScopeProfilingQuestion | undefined
|
||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||
question = block.questions.find((q) => q.id === answer.questionId)
|
||
if (question) break
|
||
}
|
||
|
||
if (question?.mapsToVVTQuestion) {
|
||
vvtAnswers[question.mapsToVVTQuestion] = answer.value
|
||
}
|
||
}
|
||
|
||
return vvtAnswers
|
||
}
|
||
|
||
/**
|
||
* Export scope answers in Loeschfristen format
|
||
*/
|
||
export function exportToLoeschfristenAnswers(
|
||
scopeAnswers: ScopeProfilingAnswer[]
|
||
): Array<{ questionId: string; value: unknown }> {
|
||
const lfAnswers: Array<{ questionId: string; value: unknown }> = []
|
||
|
||
for (const answer of scopeAnswers) {
|
||
// Find the question
|
||
let question: ScopeProfilingQuestion | undefined
|
||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||
question = block.questions.find((q) => q.id === answer.questionId)
|
||
if (question) break
|
||
}
|
||
|
||
if (question?.mapsToLFQuestion) {
|
||
lfAnswers.push({
|
||
questionId: question.mapsToLFQuestion,
|
||
value: answer.value,
|
||
})
|
||
}
|
||
}
|
||
|
||
return lfAnswers
|
||
}
|
||
|
||
/**
|
||
* Export scope answers for TOM generator
|
||
*/
|
||
export function exportToTOMProfile(
|
||
scopeAnswers: ScopeProfilingAnswer[]
|
||
): Record<string, unknown> {
|
||
const tomProfile: Record<string, unknown> = {}
|
||
|
||
// Get answer values
|
||
const getVal = (qId: string) => getAnswerValue(scopeAnswers, qId)
|
||
|
||
// Map relevant scope answers to TOM profile fields
|
||
tomProfile.industry = getVal('org_industry')
|
||
tomProfile.employeeCount = getVal('org_employee_count')
|
||
tomProfile.hasDataMinors = getVal('data_minors')
|
||
tomProfile.hasSpecialCategories = Array.isArray(getVal('data_art9'))
|
||
? (getVal('data_art9') as string[]).length > 0
|
||
: false
|
||
tomProfile.hasAutomatedDecisions = getVal('proc_adm_scoring')
|
||
tomProfile.usesAI = Array.isArray(getVal('proc_ai_usage'))
|
||
? !(getVal('proc_ai_usage') as string[]).includes('keine')
|
||
: false
|
||
tomProfile.hasThirdCountryTransfer = getVal('tech_third_country')
|
||
tomProfile.hasEncryptionRest = getVal('tech_encryption_rest')
|
||
tomProfile.hasEncryptionTransit = getVal('tech_encryption_transit')
|
||
tomProfile.hasIncidentResponse = getVal('proc_incident_response')
|
||
tomProfile.hasDeletionConcept = getVal('proc_deletion_concept')
|
||
tomProfile.hasRegularAudits = getVal('proc_regular_audits')
|
||
tomProfile.hasTraining = getVal('proc_training')
|
||
|
||
return tomProfile
|
||
}
|
||
|
||
/**
|
||
* Check if a block is complete (all required questions answered)
|
||
*/
|
||
export function isBlockComplete(
|
||
answers: ScopeProfilingAnswer[],
|
||
blockId: ScopeQuestionBlockId
|
||
): boolean {
|
||
const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
|
||
if (!block) return false
|
||
|
||
const requiredQuestions = block.questions.filter((q) => q.required)
|
||
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
|
||
|
||
return requiredQuestions.every((q) => answeredQuestionIds.has(q.id))
|
||
}
|
||
|
||
/**
|
||
* Get progress for a specific block (0-100)
|
||
*/
|
||
export function getBlockProgress(
|
||
answers: ScopeProfilingAnswer[],
|
||
blockId: ScopeQuestionBlockId
|
||
): number {
|
||
const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
|
||
if (!block) return 0
|
||
|
||
const requiredQuestions = block.questions.filter((q) => q.required)
|
||
if (requiredQuestions.length === 0) return 100
|
||
|
||
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
|
||
const answeredCount = requiredQuestions.filter((q) =>
|
||
answeredQuestionIds.has(q.id)
|
||
).length
|
||
|
||
return Math.round((answeredCount / requiredQuestions.length) * 100)
|
||
}
|
||
|
||
/**
|
||
* Get total progress across all blocks (0-100)
|
||
*/
|
||
export function getTotalProgress(answers: ScopeProfilingAnswer[]): number {
|
||
let totalRequired = 0
|
||
let totalAnswered = 0
|
||
|
||
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
|
||
|
||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
||
const requiredQuestions = block.questions.filter((q) => q.required)
|
||
totalRequired += requiredQuestions.length
|
||
totalAnswered += requiredQuestions.filter((q) =>
|
||
answeredQuestionIds.has(q.id)
|
||
).length
|
||
}
|
||
|
||
if (totalRequired === 0) return 100
|
||
return Math.round((totalAnswered / totalRequired) * 100)
|
||
}
|
||
|
||
/**
|
||
* Get answer value for a specific question
|
||
*/
|
||
export function getAnswerValue(
|
||
answers: ScopeProfilingAnswer[],
|
||
questionId: string
|
||
): unknown {
|
||
const answer = answers.find((a) => a.questionId === questionId)
|
||
return answer?.value
|
||
}
|
||
|
||
/**
|
||
* Get all questions as a flat array (including hidden auto-filled questions)
|
||
*/
|
||
export function getAllQuestions(): ScopeProfilingQuestion[] {
|
||
return [
|
||
...SCOPE_QUESTION_BLOCKS.flatMap((block) => block.questions),
|
||
...HIDDEN_SCORING_QUESTIONS,
|
||
]
|
||
}
|
||
|
||
/**
|
||
* Get unanswered required questions, optionally filtered by block.
|
||
* Returns block metadata along with each question for navigation.
|
||
*/
|
||
export function getUnansweredRequiredQuestions(
|
||
answers: ScopeProfilingAnswer[],
|
||
blockId?: ScopeQuestionBlockId
|
||
): { blockId: ScopeQuestionBlockId; blockTitle: string; question: ScopeProfilingQuestion }[] {
|
||
const answeredIds = new Set(answers.map((a) => a.questionId))
|
||
const blocks = blockId
|
||
? SCOPE_QUESTION_BLOCKS.filter((b) => b.id === blockId)
|
||
: SCOPE_QUESTION_BLOCKS
|
||
|
||
const result: { blockId: ScopeQuestionBlockId; blockTitle: string; question: ScopeProfilingQuestion }[] = []
|
||
|
||
for (const block of blocks) {
|
||
for (const q of block.questions) {
|
||
if (q.required && !answeredIds.has(q.id)) {
|
||
result.push({ blockId: block.id, blockTitle: block.title, question: q })
|
||
}
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|