feat: DSFA pre-fill from Company Profile + Scope answers

- 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) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-04 19:36:13 +02:00
parent 95baf60da3
commit 84b21cad08
3 changed files with 248 additions and 9 deletions
@@ -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<DSFA>) => Promise<void> }) {
interface GeneratorWizardProps {
onClose: () => void
onSubmit: (data: Partial<DSFA>) => Promise<void>
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<string[]>([])
const [riskLevel, setRiskLevel] = useState<'low' | 'medium' | 'high' | 'critical'>('low')
const [selectedMeasures, setSelectedMeasures] = useState<string[]>([])
const [title, setTitle] = useState(prefill?.title || '')
const [description, setDescription] = useState(prefill?.description || '')
const [processingActivity, setProcessingActivity] = useState(prefill?.processingActivity || '')
const [selectedCategories, setSelectedCategories] = useState<string[]>(prefill?.dataCategories || [])
const riskMap2: Record<string, 'low' | 'medium' | 'high' | 'critical'> = { niedrig: 'low', mittel: 'medium', hoch: 'high', kritisch: 'critical' }
const [riskLevel, setRiskLevel] = useState<'low' | 'medium' | 'high' | 'critical'>(riskMap2[prefill?.riskLevel || ''] || 'low')
const [selectedMeasures, setSelectedMeasures] = useState<string[]>(prefill?.measures || [])
const riskMap: Record<string, 'low' | 'medium' | 'high' | 'critical'> = {
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<DSFA>)
onClose()
} finally {
setSaving(false)
+27 -1
View File
@@ -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<string>('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() {
)}
</StepHeader>
{/* DSFA Requirement Check */}
{dsfaCheck.required && dsfas.length === 0 && (
<div className="bg-red-50 border border-red-200 rounded-xl p-5">
<h3 className="font-semibold text-red-800">DSFA erforderlich (Art. 35 DSGVO)</h3>
<p className="text-sm text-red-700 mt-1">Basierend auf Ihrem Scope-Profiling wurde festgestellt:</p>
<ul className="mt-2 space-y-1">
{dsfaCheck.triggers.map(t => (
<li key={t} className="text-sm text-red-600 flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-red-500 rounded-full flex-shrink-0" />
{t}
</li>
))}
</ul>
</div>
)}
{showGenerator && (
<GeneratorWizard
onClose={() => setShowGenerator(false)}
onSubmit={handleCreateDSFA}
prefill={prefill}
/>
)}
@@ -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<string, string> = {
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<string, string> = {
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<string, string> = {
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 }
}