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:
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user