From 3c4f7d900db5952f649752f74a6e44244969db40 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:54:29 +0200 Subject: [PATCH] refactor(admin): split compliance-scope-profiling.ts (1171 LOC) into focused modules Split the monolithic file into three content modules plus a barrel re-export: - compliance-scope-profiling-blocks.ts (489 LOC): blocks 1-7, hidden questions, autofill IDs - compliance-scope-profiling-vvt-blocks.ts (274 LOC): blocks 8-9, SCOPE_QUESTION_BLOCKS aggregate - compliance-scope-profiling-helpers.ts (359 LOC): all prefill/export/progress functions - compliance-scope-profiling.ts (41 LOC): barrel re-export preserving existing import paths All files under the 500 LOC hard cap. No consumer changes needed. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../sdk/compliance-scope-profiling-blocks.ts | 489 +++++++ .../sdk/compliance-scope-profiling-helpers.ts | 358 +++++ .../compliance-scope-profiling-vvt-blocks.ts | 274 ++++ .../lib/sdk/compliance-scope-profiling.ts | 1200 +---------------- 4 files changed, 1156 insertions(+), 1165 deletions(-) create mode 100644 admin-compliance/lib/sdk/compliance-scope-profiling-blocks.ts create mode 100644 admin-compliance/lib/sdk/compliance-scope-profiling-helpers.ts create mode 100644 admin-compliance/lib/sdk/compliance-scope-profiling-vvt-blocks.ts diff --git a/admin-compliance/lib/sdk/compliance-scope-profiling-blocks.ts b/admin-compliance/lib/sdk/compliance-scope-profiling-blocks.ts new file mode 100644 index 0000000..c5d8f1a --- /dev/null +++ b/admin-compliance/lib/sdk/compliance-scope-profiling-blocks.ts @@ -0,0 +1,489 @@ +import type { + ScopeQuestionBlock, + ScopeProfilingQuestion, +} from './compliance-scope-types' + +/** + * 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 + +/** + * Block 1: Organisation & Reife + */ +export 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 + */ +export 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 + */ +export 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 + */ +export 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 + */ +export 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 + */ +export 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) + */ +export 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 }, + }, + ], +} diff --git a/admin-compliance/lib/sdk/compliance-scope-profiling-helpers.ts b/admin-compliance/lib/sdk/compliance-scope-profiling-helpers.ts new file mode 100644 index 0000000..79589f5 --- /dev/null +++ b/admin-compliance/lib/sdk/compliance-scope-profiling-helpers.ts @@ -0,0 +1,358 @@ +import type { + ScopeQuestionBlockId, + ScopeProfilingQuestion, + ScopeProfilingAnswer, +} from './compliance-scope-types' +import type { CompanyProfile } from './types' +import { + HIDDEN_SCORING_QUESTIONS, +} from './compliance-scope-profiling-blocks' +import { + SCOPE_QUESTION_BLOCKS, +} from './compliance-scope-profiling-vvt-blocks' + +/** + * Prefill scope answers from CompanyProfile. + */ +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. + */ +export function getAutoFilledScoringAnswers( + profile: CompanyProfile +): ScopeProfilingAnswer[] { + const answers: ScopeProfilingAnswer[] = [] + + if (profile.employeeCount != null) { + answers.push({ questionId: 'org_employee_count', value: profile.employeeCount }) + } + if (profile.annualRevenue) { + answers.push({ questionId: 'org_annual_revenue', value: profile.annualRevenue }) + } + if (profile.industry && profile.industry.length > 0) { + answers.push({ questionId: 'org_industry', value: profile.industry.join(', ') }) + } + if (profile.businessModel) { + answers.push({ questionId: 'org_business_model', value: profile.businessModel }) + } + 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 +): ScopeProfilingAnswer[] { + const answers: ScopeProfilingAnswer[] = [] + const reverseMap: Record = {} + for (const block of SCOPE_QUESTION_BLOCKS) { + for (const q of block.questions) { + if (q.mapsToVVTQuestion) { + reverseMap[q.mapsToVVTQuestion] = q.id + } + } + } + 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[] = [] + const reverseMap: Record = {} + for (const block of SCOPE_QUESTION_BLOCKS) { + for (const q of block.questions) { + if (q.mapsToLFQuestion) { + reverseMap[q.mapsToLFQuestion] = q.id + } + } + } + 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 { + const vvtAnswers: Record = {} + for (const answer of scopeAnswers) { + 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) { + 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 { + const tomProfile: Record = {} + const getVal = (qId: string) => getAnswerValue(scopeAnswers, qId) + + 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. + */ +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 +} diff --git a/admin-compliance/lib/sdk/compliance-scope-profiling-vvt-blocks.ts b/admin-compliance/lib/sdk/compliance-scope-profiling-vvt-blocks.ts new file mode 100644 index 0000000..a0ebd7f --- /dev/null +++ b/admin-compliance/lib/sdk/compliance-scope-profiling-vvt-blocks.ts @@ -0,0 +1,274 @@ +import type { + ScopeQuestionBlock, +} from './compliance-scope-types' +import { DEPARTMENT_DATA_CATEGORIES } from './vvt-profiling' +import { + BLOCK_1_ORGANISATION, + BLOCK_2_DATA, + BLOCK_3_PROCESSING, + BLOCK_4_TECH, + BLOCK_5_PROCESSES, + BLOCK_6_PRODUCT, + BLOCK_7_AI_SYSTEMS, +} from './compliance-scope-profiling-blocks' + +/** + * 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, +] diff --git a/admin-compliance/lib/sdk/compliance-scope-profiling.ts b/admin-compliance/lib/sdk/compliance-scope-profiling.ts index 0a80501..caee4ea 100644 --- a/admin-compliance/lib/sdk/compliance-scope-profiling.ts +++ b/admin-compliance/lib/sdk/compliance-scope-profiling.ts @@ -1,1171 +1,41 @@ -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. + * Compliance Scope Profiling -- barrel re-export. * - * 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. + * Block constants 1-7 and hidden questions live in + * ./compliance-scope-profiling-blocks + * + * Blocks 8-9 and the aggregated SCOPE_QUESTION_BLOCKS array live in + * ./compliance-scope-profiling-vvt-blocks + * + * All helper/export functions live in + * ./compliance-scope-profiling-helpers */ -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, - }) - } +// --- data / constants --- +export { + PROFILE_AUTOFILL_QUESTION_IDS, + HIDDEN_SCORING_QUESTIONS, +} from './compliance-scope-profiling-blocks' - // 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()) +// --- blocks aggregate --- +export { + SCOPE_QUESTION_BLOCKS, +} from './compliance-scope-profiling-vvt-blocks' - 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 -): ScopeProfilingAnswer[] { - const answers: ScopeProfilingAnswer[] = [] - - // Build reverse mapping: VVT question -> Scope question - const reverseMap: Record = {} - 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 = {} - 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 { - const vvtAnswers: Record = {} - - 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 { - const tomProfile: Record = {} - - // 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 -} +// --- all functions --- +export { + prefillFromCompanyProfile, + getAutoFilledScoringAnswers, + getProfileInfoForBlock, + prefillFromVVTAnswers, + prefillFromLoeschfristenAnswers, + exportToVVTAnswers, + exportToLoeschfristenAnswers, + exportToTOMProfile, + isBlockComplete, + getBlockProgress, + getTotalProgress, + getAnswerValue, + getAllQuestions, + getUnansweredRequiredQuestions, +} from './compliance-scope-profiling-helpers'