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 ): 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 }