import type { ScopeQuestionBlock, ScopeQuestionBlockId, ScopeProfilingQuestion, ScopeProfilingAnswer, ComplianceScopeState, } from './compliance-scope-types' import type { CompanyProfile } from './types' /** * Block 1: Organisation & Reife */ const BLOCK_1_ORGANISATION: ScopeQuestionBlock = { id: 'organisation', title: 'Organisation & Reife', description: 'Grundlegende Informationen zu Ihrer Organisation und Compliance-Zielen', order: 1, questions: [ { id: 'org_employee_count', type: 'number', question: 'Wie viele Mitarbeiter hat Ihre Organisation?', helpText: 'Geben Sie die Gesamtzahl aller Beschäftigten an (inkl. Teilzeit, Minijobs)', required: true, scoreWeights: { risk: 5, complexity: 8, assurance: 6 }, mapsToCompanyProfile: 'employeeCount', }, { 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 }, }, { id: 'org_annual_revenue', type: 'single', question: 'Wie hoch ist Ihr jährlicher Umsatz?', helpText: 'Wählen Sie die zutreffende Umsatzklasse', required: true, options: [ { value: '<2Mio', label: 'Unter 2 Mio. EUR' }, { value: '2-10Mio', label: '2 bis 10 Mio. EUR' }, { value: '10-50Mio', label: '10 bis 50 Mio. EUR' }, { value: '>50Mio', label: 'Über 50 Mio. EUR' }, ], scoreWeights: { risk: 4, complexity: 6, assurance: 7 }, mapsToCompanyProfile: 'annualRevenue', }, { id: 'org_cert_target', type: 'multi', question: 'Welche Zertifizierungen streben Sie an oder besitzen Sie bereits?', helpText: 'Mehrfachauswahl möglich. Zertifizierungen erhöhen den Assurance-Bedarf', required: false, options: [ { value: 'ISO27001', label: 'ISO 27001 (Informationssicherheit)' }, { value: 'ISO27701', label: 'ISO 27701 (Datenschutz-Erweiterung)' }, { value: 'TISAX', label: 'TISAX (Automotive)' }, { value: 'SOC2', label: 'SOC 2 (US-Standard)' }, { value: 'BSI-Grundschutz', label: 'BSI IT-Grundschutz' }, { value: 'Keine', label: 'Keine Zertifizierung geplant' }, ], scoreWeights: { risk: 3, complexity: 5, assurance: 10 }, }, { id: 'org_industry', type: 'single', question: 'In welcher Branche sind Sie tätig?', helpText: 'Ihre Branche beeinflusst Risikobewertung und regulatorische Anforderungen', required: true, options: [ { value: 'it_software', label: 'IT & Software' }, { value: 'healthcare', label: 'Gesundheitswesen' }, { value: 'education', label: 'Bildung & Forschung' }, { value: 'finance', label: 'Finanzdienstleistungen' }, { value: 'retail', label: 'Einzelhandel & E-Commerce' }, { value: 'manufacturing', label: 'Produktion & Fertigung' }, { value: 'consulting', label: 'Beratung & Dienstleistungen' }, { value: 'public', label: 'Öffentliche Verwaltung' }, { value: 'other', label: 'Sonstige' }, ], scoreWeights: { risk: 7, complexity: 5, assurance: 6 }, mapsToCompanyProfile: 'industry', mapsToVVTQuestion: 'org_industry', mapsToLFQuestion: 'org-branche', }, { id: 'org_business_model', type: 'single', question: 'Was ist Ihr primäres Geschäftsmodell?', helpText: 'B2C-Modelle haben höhere Datenschutzanforderungen', required: true, options: [ { value: 'b2b', label: 'B2B (Business-to-Business)' }, { value: 'b2c', label: 'B2C (Business-to-Consumer)' }, { value: 'both', label: 'B2B und B2C gemischt' }, { value: 'b2g', label: 'B2G (Business-to-Government)' }, ], scoreWeights: { risk: 6, complexity: 5, assurance: 5 }, mapsToCompanyProfile: 'businessModel', mapsToVVTQuestion: 'org_b2b_b2c', mapsToLFQuestion: 'org-geschaeftsmodell', }, { id: 'org_has_dsb', type: 'boolean', question: 'Haben Sie einen Datenschutzbeauftragten bestellt?', helpText: 'Ein DSB ist bei mehr als 20 Personen mit regelmäßiger Datenverarbeitung Pflicht', required: true, scoreWeights: { risk: 5, complexity: 3, assurance: 6 }, }, ], } /** * Block 2: Daten & Betroffene */ const BLOCK_2_DATA: ScopeQuestionBlock = { id: 'data', title: 'Daten & Betroffene', 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', }, { id: 'data_volume', type: 'single', question: 'Wie viele Personendatensätze verarbeiten Sie insgesamt?', helpText: 'Schätzen Sie die Gesamtzahl betroffener Personen', required: true, options: [ { value: '<1000', label: 'Unter 1.000' }, { value: '1000-10000', label: '1.000 bis 10.000' }, { value: '10000-100000', label: '10.000 bis 100.000' }, { value: '100000-1000000', label: '100.000 bis 1 Mio.' }, { value: '>1000000', label: 'Über 1 Mio.' }, ], scoreWeights: { risk: 7, complexity: 6, assurance: 6 }, }, ], } /** * 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: 'Technik, Hosting & Transfers', 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: 'Produktkontext', description: 'Spezifische Merkmale Ihrer Produkte und Services', order: 6, questions: [ { id: 'prod_type', type: 'multi', question: 'Welche Art von Produkten/Services bieten Sie an?', helpText: 'Mehrfachauswahl möglich', required: true, options: [ { value: 'webapp', label: 'Web-Anwendung' }, { value: 'mobile', label: 'Mobile App (iOS/Android)' }, { value: 'saas', label: 'SaaS-Plattform' }, { value: 'onpremise', label: 'On-Premise Software' }, { value: 'api', label: 'API/Schnittstellen' }, { value: 'iot', label: 'IoT/Hardware' }, { value: 'beratung', label: 'Beratungsleistungen' }, { value: 'handel', label: 'Handel/Vertrieb' }, ], scoreWeights: { risk: 5, complexity: 6, assurance: 5 }, }, { 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_webshop', type: 'boolean', question: 'Betreiben Sie einen Online-Shop?', helpText: 'E-Commerce mit Zahlungsabwicklung, Bestellverwaltung', required: true, scoreWeights: { risk: 7, complexity: 6, 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 }, }, ], } /** * 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, ] /** * Prefill scope answers from CompanyProfile */ export function prefillFromCompanyProfile( profile: CompanyProfile ): ScopeProfilingAnswer[] { const answers: ScopeProfilingAnswer[] = [] // employeeCount if (profile.employeeCount != null) { answers.push({ questionId: 'org_employee_count', value: profile.employeeCount, }) } // annualRevenue if (profile.annualRevenue) { answers.push({ questionId: 'org_annual_revenue', value: profile.annualRevenue, }) } // industry if (profile.industry) { answers.push({ questionId: 'org_industry', value: profile.industry, }) } // businessModel 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, }) } // usesAI -> proc_ai_usage if (profile.usesAI === true) { // We don't know which specific AI type, so just mark as "generativ" as a default answers.push({ questionId: 'proc_ai_usage', value: ['generativ'], }) } else if (profile.usesAI === false) { answers.push({ questionId: 'proc_ai_usage', value: ['keine'], }) } // offerings -> prod_type mapping 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, }) } } return answers } /** * 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 */ export function getAllQuestions(): ScopeProfilingQuestion[] { return SCOPE_QUESTION_BLOCKS.flatMap((block) => block.questions) }