From 84b21cad084b956ba46ec3b38648458706073ed0 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Mon, 4 May 2026 19:36:13 +0200 Subject: [PATCH] feat: DSFA pre-fill from Company Profile + Scope answers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New prefill-from-scope.ts utility: - headquartersState → federal_state (Bundesland for authority lookup) - data_art9 → special data categories (Gesundheit, Biometrie, etc.) - data_minors → adds "Minderjährige" to data subjects + raises risk - proc_adm_scoring → Art. 22 affected rights + measures - proc_ai_usage → involves_ai flag + AI measures - proc_video_surveillance → video data categories - industry/businessModel → processing purpose + legal basis - isDSFARequired() check: shows red banner when Art. 35 triggers detected - GeneratorWizard accepts prefill prop, initializes all fields - Passes federal_state, involves_ai, legal_basis to backend POST Co-Authored-By: Claude Opus 4.6 (1M context) --- .../sdk/dsfa/_components/GeneratorWizard.tsx | 27 ++- admin-compliance/app/sdk/dsfa/page.tsx | 28 ++- .../lib/sdk/dsfa/prefill-from-scope.ts | 202 ++++++++++++++++++ 3 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 admin-compliance/lib/sdk/dsfa/prefill-from-scope.ts diff --git a/admin-compliance/app/sdk/dsfa/_components/GeneratorWizard.tsx b/admin-compliance/app/sdk/dsfa/_components/GeneratorWizard.tsx index 1132340..79748ed 100644 --- a/admin-compliance/app/sdk/dsfa/_components/GeneratorWizard.tsx +++ b/admin-compliance/app/sdk/dsfa/_components/GeneratorWizard.tsx @@ -2,16 +2,24 @@ import React, { useState } from 'react' import type { DSFA } from './DSFACard' +import type { DSFAPrefillResult } from '@/lib/sdk/dsfa/prefill-from-scope' -export function GeneratorWizard({ onClose, onSubmit }: { onClose: () => void; onSubmit: (data: Partial) => Promise }) { +interface GeneratorWizardProps { + onClose: () => void + onSubmit: (data: Partial) => Promise + prefill?: DSFAPrefillResult | null +} + +export function GeneratorWizard({ onClose, onSubmit, prefill }: GeneratorWizardProps) { const [step, setStep] = useState(1) const [saving, setSaving] = useState(false) - const [title, setTitle] = useState('') - const [description, setDescription] = useState('') - const [processingActivity, setProcessingActivity] = useState('') - const [selectedCategories, setSelectedCategories] = useState([]) - const [riskLevel, setRiskLevel] = useState<'low' | 'medium' | 'high' | 'critical'>('low') - const [selectedMeasures, setSelectedMeasures] = useState([]) + const [title, setTitle] = useState(prefill?.title || '') + const [description, setDescription] = useState(prefill?.description || '') + const [processingActivity, setProcessingActivity] = useState(prefill?.processingActivity || '') + const [selectedCategories, setSelectedCategories] = useState(prefill?.dataCategories || []) + const riskMap2: Record = { niedrig: 'low', mittel: 'medium', hoch: 'high', kritisch: 'critical' } + const [riskLevel, setRiskLevel] = useState<'low' | 'medium' | 'high' | 'critical'>(riskMap2[prefill?.riskLevel || ''] || 'low') + const [selectedMeasures, setSelectedMeasures] = useState(prefill?.measures || []) const riskMap: Record = { Niedrig: 'low', Mittel: 'medium', Hoch: 'high', Kritisch: 'critical', @@ -28,7 +36,10 @@ export function GeneratorWizard({ onClose, onSubmit }: { onClose: () => void; on riskLevel, measures: selectedMeasures, status: 'draft', - }) + ...(prefill?.federalState ? { federal_state: prefill.federalState } : {}), + ...(prefill?.involvesAi ? { involves_ai: true } : {}), + ...(prefill?.legalBasis ? { legal_basis: prefill.legalBasis } : {}), + } as Partial) onClose() } finally { setSaving(false) diff --git a/admin-compliance/app/sdk/dsfa/page.tsx b/admin-compliance/app/sdk/dsfa/page.tsx index 4b991b3..f31607e 100644 --- a/admin-compliance/app/sdk/dsfa/page.tsx +++ b/admin-compliance/app/sdk/dsfa/page.tsx @@ -1,12 +1,13 @@ 'use client' -import { useState, useCallback, useEffect } from 'react' +import { useState, useCallback, useEffect, useMemo } from 'react' import { useRouter } from 'next/navigation' import { useSDK } from '@/lib/sdk' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' import { DocumentUploadSection, type UploadedDocument } from '@/components/sdk' import { DSFACard, type DSFA } from './_components/DSFACard' import { GeneratorWizard } from './_components/GeneratorWizard' +import { prefillDSFAFromScope, isDSFARequired } from '@/lib/sdk/dsfa/prefill-from-scope' export default function DSFAPage() { const router = useRouter() @@ -17,6 +18,14 @@ export default function DSFAPage() { const [showGenerator, setShowGenerator] = useState(false) const [filter, setFilter] = useState('all') + // Pre-fill from Company Profile + Scope answers + const scopeAnswers = state.complianceScope?.answers || [] + const prefill = useMemo( + () => prefillDSFAFromScope(state.companyProfile || null, scopeAnswers), + [state.companyProfile, scopeAnswers] + ) + const dsfaCheck = useMemo(() => isDSFARequired(scopeAnswers), [scopeAnswers]) + const loadDSFAs = useCallback(async () => { setIsLoading(true) setError(null) @@ -120,10 +129,27 @@ export default function DSFAPage() { )} + {/* DSFA Requirement Check */} + {dsfaCheck.required && dsfas.length === 0 && ( +
+

DSFA erforderlich (Art. 35 DSGVO)

+

Basierend auf Ihrem Scope-Profiling wurde festgestellt:

+
    + {dsfaCheck.triggers.map(t => ( +
  • + + {t} +
  • + ))} +
+
+ )} + {showGenerator && ( setShowGenerator(false)} onSubmit={handleCreateDSFA} + prefill={prefill} /> )} diff --git a/admin-compliance/lib/sdk/dsfa/prefill-from-scope.ts b/admin-compliance/lib/sdk/dsfa/prefill-from-scope.ts new file mode 100644 index 0000000..03ae31d --- /dev/null +++ b/admin-compliance/lib/sdk/dsfa/prefill-from-scope.ts @@ -0,0 +1,202 @@ +/** + * DSFA Pre-Fill — derives initial DSFA data from Company Profile + Scope answers. + * + * Maps: Firmensitz → Bundesland, Scope-Antworten → Datenkategorien/Risiken, + * Use Cases → Verarbeitungstaetigkeiten, DPO → Beratungsinformationen. + */ + +import type { ScopeProfilingAnswer } from '@/lib/sdk/compliance-scope-types' + +interface CompanyProfileMinimal { + headquartersState?: string + industry?: string[] + businessModel?: string + dpoName?: string | null + dpoEmail?: string | null + companyName?: string +} + +export interface DSFAPrefillResult { + title: string + description: string + processingActivity: string + dataCategories: string[] + dataSubjects: string[] + riskLevel: string + measures: string[] + federalState: string + involvesAi: boolean + legalBasis: string + processingPurpose: string + affectedRights: string[] +} + +const ART9_CATEGORY_MAP: Record = { + health: 'Gesundheitsdaten', + biometric: 'Biometrische Daten', + genetic: 'Genetische Daten', + ethnic: 'Ethnische Herkunft', + political: 'Politische Meinungen', + religious: 'Religioese Ueberzeugungen', + union: 'Gewerkschaftszugehoerigkeit', + sexual: 'Sexualleben/Orientierung', +} + +const BUNDESLAND_LABELS: Record = { + BW: 'Baden-Wuerttemberg', BY: 'Bayern', BE: 'Berlin', BB: 'Brandenburg', + HB: 'Bremen', HH: 'Hamburg', HE: 'Hessen', MV: 'Mecklenburg-Vorpommern', + NI: 'Niedersachsen', NW: 'Nordrhein-Westfalen', RP: 'Rheinland-Pfalz', + SL: 'Saarland', SN: 'Sachsen', ST: 'Sachsen-Anhalt', + SH: 'Schleswig-Holstein', TH: 'Thueringen', +} + +function getAnswer(answers: ScopeProfilingAnswer[], questionId: string): string | string[] | boolean | undefined { + const a = answers.find(a => a.questionId === questionId) + return a?.value as string | string[] | boolean | undefined +} + +export function prefillDSFAFromScope( + profile: CompanyProfileMinimal | null, + scopeAnswers: ScopeProfilingAnswer[], +): DSFAPrefillResult { + const result: DSFAPrefillResult = { + title: '', + description: '', + processingActivity: '', + dataCategories: [], + dataSubjects: [], + riskLevel: 'mittel', + measures: ['Zugriffskontrolle', 'Verschluesselung'], + federalState: '', + involvesAi: false, + legalBasis: '', + processingPurpose: '', + affectedRights: [], + } + + // 1. Firmensitz → Bundesland + if (profile?.headquartersState) { + result.federalState = profile.headquartersState + } + + // 2. Art. 9 Daten → Datenkategorien + Risikostufe + const art9 = getAnswer(scopeAnswers, 'data_art9') + if (art9 === true || art9 === 'yes') { + result.dataCategories.push('Besondere Kategorien (Art. 9)') + result.riskLevel = 'hoch' + result.title = 'DSFA — Verarbeitung besonderer Datenkategorien' + } + if (Array.isArray(art9)) { + for (const cat of art9) { + const label = ART9_CATEGORY_MAP[cat] + if (label) result.dataCategories.push(label) + } + if (art9.length > 0) result.riskLevel = 'hoch' + } + + // 3. Minderjährige → Betroffene + Risiko + const minors = getAnswer(scopeAnswers, 'data_minors') + if (minors === true || minors === 'yes') { + result.dataSubjects.push('Minderjaehrige (unter 16 Jahre)') + result.riskLevel = 'hoch' + result.affectedRights.push('Besonderer Schutz Minderjaehriger (Art. 8 DSGVO)') + if (!result.title) result.title = 'DSFA — Verarbeitung von Daten Minderjaehriger' + } + + // 4. Automatisierte Entscheidungen (Scoring) + const scoring = getAnswer(scopeAnswers, 'proc_adm_scoring') + if (scoring === true || scoring === 'yes') { + result.affectedRights.push('Recht auf nicht-automatisierte Entscheidung (Art. 22 DSGVO)') + result.riskLevel = 'hoch' + if (!result.title) result.title = 'DSFA — Automatisierte Einzelentscheidungen' + result.measures.push('Menschliche Pruefung') + } + + // 5. KI-Einsatz + const aiUsage = getAnswer(scopeAnswers, 'proc_ai_usage') + if (aiUsage === true || aiUsage === 'yes' || (Array.isArray(aiUsage) && aiUsage.length > 0)) { + result.involvesAi = true + result.measures.push('KI-Transparenz', 'Human Oversight') + if (!result.title) result.title = 'DSFA — KI-gestuetzte Datenverarbeitung' + } + + // 6. Videoueberwachung + const video = getAnswer(scopeAnswers, 'proc_video_surveillance') + if (video === true || video === 'yes') { + result.dataCategories.push('Videoaufnahmen / Bilddaten') + result.dataSubjects.push('Besucher', 'Mitarbeiter') + if (!result.title) result.title = 'DSFA — Videoueberwachung' + } + + // 7. Datenvolumen + const volume = getAnswer(scopeAnswers, 'data_volume') + if (volume === '100000-1000000' || volume === '>1000000') { + result.riskLevel = 'hoch' + result.description += 'Grosse Datenmengen erhoehen das Risiko fuer Betroffene. ' + } + + // 8. Branche + Geschaeftsmodell → Verarbeitungszweck + if (profile?.industry?.length) { + const ind = profile.industry[0] + const purposeMap: Record = { + healthcare: 'Patientenversorgung und Gesundheitsdatenverarbeitung', + finance: 'Finanzdienstleistungen und Bonitaetspruefung', + education: 'Bildungsverwaltung und Schuelerbetreuung', + tech: 'Software-Entwicklung und Cloud-Dienste', + retail: 'Handel und Kundenbeziehungsmanagement', + legal: 'Mandatsbearbeitung und Rechtsberatung', + } + result.processingPurpose = purposeMap[ind] || '' + result.processingActivity = purposeMap[ind] || '' + } + + if (profile?.businessModel === 'b2c') { + result.dataSubjects.push('Endverbraucher') + result.legalBasis = 'Art. 6 Abs. 1 lit. b DSGVO (Vertragserfuellung)' + } else if (profile?.businessModel === 'b2b') { + result.dataSubjects.push('Geschaeftskunden', 'Ansprechpartner') + result.legalBasis = 'Art. 6 Abs. 1 lit. f DSGVO (Berechtigtes Interesse)' + } + + // Deduplicate + result.dataCategories = [...new Set(result.dataCategories)] + result.dataSubjects = [...new Set(result.dataSubjects)] + result.measures = [...new Set(result.measures)] + result.affectedRights = [...new Set(result.affectedRights)] + + // Default title if nothing triggered + if (!result.title) { + result.title = `DSFA — ${profile?.companyName || 'Datenverarbeitung'}` + } + + return result +} + +/** + * Check if DSFA is required based on scope answers (Art. 35 Abs. 3 triggers). + */ +export function isDSFARequired(scopeAnswers: ScopeProfilingAnswer[]): { + required: boolean + triggers: string[] +} { + const triggers: string[] = [] + + if (getAnswer(scopeAnswers, 'data_art9') === true || getAnswer(scopeAnswers, 'data_art9') === 'yes') { + triggers.push('Besondere Datenkategorien (Art. 9 DSGVO)') + } + if (getAnswer(scopeAnswers, 'data_minors') === true || getAnswer(scopeAnswers, 'data_minors') === 'yes') { + triggers.push('Daten Minderjaehriger (Art. 8 DSGVO)') + } + if (getAnswer(scopeAnswers, 'proc_adm_scoring') === true || getAnswer(scopeAnswers, 'proc_adm_scoring') === 'yes') { + triggers.push('Automatisierte Einzelentscheidungen (Art. 22 DSGVO)') + } + if (getAnswer(scopeAnswers, 'proc_video_surveillance') === true || getAnswer(scopeAnswers, 'proc_video_surveillance') === 'yes') { + triggers.push('Systematische Ueberwachung (Art. 35 Abs. 3 lit. c)') + } + const vol = getAnswer(scopeAnswers, 'data_volume') + if (vol === '100000-1000000' || vol === '>1000000') { + triggers.push('Umfangreiche Datenverarbeitung (Art. 35 Abs. 3 lit. b)') + } + + return { required: triggers.length > 0, triggers } +}