This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/components/sdk/dsfa/ThresholdAnalysisSection.tsx
BreakPilot Dev aa0fbc0e64 feat(dsfa): Add complete 8-section DSFA module with sidebar navigation
Implement DSFA optimization plan based on DSK Kurzpapier Nr. 5:
- Section 0: ThresholdAnalysisSection (WP248, Art. 35 Abs. 3, KI-Trigger)
- Section 5: StakeholderConsultationSection (Art. 35 Abs. 9)
- Section 6: Art36Warning for authority consultation (Art. 36)
- Section 7: ReviewScheduleSection (Art. 35 Abs. 11)
- DSFASidebar with progress tracking for all 8 sections
- Extended DSFASectionProgress for sections 0, 6, 7

Replaces tab navigation with sidebar layout (1/4 + 3/4 grid).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-09 11:50:04 +01:00

419 lines
16 KiB
TypeScript

'use client'
import React, { useState, useEffect } from 'react'
import {
DSFA,
WP248_CRITERIA,
ART35_ABS3_CASES,
AI_DSFA_TRIGGERS,
checkDSFARequiredByWP248,
DSFAThresholdAnalysis,
} from '@/lib/sdk/dsfa/types'
interface ThresholdAnalysisSectionProps {
dsfa: DSFA
onUpdate: (data: Record<string, unknown>) => Promise<void>
isSubmitting: boolean
}
export function ThresholdAnalysisSection({ dsfa, onUpdate, isSubmitting }: ThresholdAnalysisSectionProps) {
// Initialize state from existing data
const existingAnalysis = dsfa.threshold_analysis
const [wp248Selected, setWp248Selected] = useState<string[]>(
dsfa.wp248_criteria_met ||
existingAnalysis?.criteria_assessment?.filter(c => c.applies).map(c => c.criterion_id) ||
[]
)
const [art35Selected, setArt35Selected] = useState<string[]>(
dsfa.art35_abs3_triggered ||
existingAnalysis?.art35_abs3_assessment?.filter(c => c.applies).map(c => c.case_id) ||
[]
)
const [aiTriggersSelected, setAiTriggersSelected] = useState<string[]>(
dsfa.ai_trigger_ids || []
)
const [dsfaRequired, setDsfaRequired] = useState<boolean | null>(
existingAnalysis?.dsfa_required ?? null
)
const [justification, setJustification] = useState(
existingAnalysis?.decision_justification || ''
)
// Calculate recommendation based on selections
const wp248Result = checkDSFARequiredByWP248(wp248Selected)
const hasArt35Trigger = art35Selected.length > 0
const hasAITrigger = aiTriggersSelected.length > 0
const recommendation = wp248Result.required || hasArt35Trigger || hasAITrigger
? 'required'
: wp248Selected.length === 1
? 'possible'
: 'not_required'
// Auto-generate justification when selections change
useEffect(() => {
if (dsfaRequired === null && !justification) {
const parts: string[] = []
if (wp248Selected.length > 0) {
const criteriaNames = wp248Selected.map(id =>
WP248_CRITERIA.find(c => c.id === id)?.code
).filter(Boolean).join(', ')
parts.push(`${wp248Selected.length} WP248-Kriterien erfuellt (${criteriaNames})`)
}
if (art35Selected.length > 0) {
parts.push(`Art. 35 Abs. 3 Regelbeispiel${art35Selected.length > 1 ? 'e' : ''} erfuellt`)
}
if (aiTriggersSelected.length > 0) {
parts.push(`${aiTriggersSelected.length} KI-spezifische${aiTriggersSelected.length > 1 ? '' : 'r'} Trigger erfuellt`)
}
if (parts.length > 0) {
setJustification(parts.join('. ') + '.')
}
}
}, [wp248Selected, art35Selected, aiTriggersSelected, dsfaRequired, justification])
const toggleWp248 = (criterionId: string) => {
setWp248Selected(prev =>
prev.includes(criterionId)
? prev.filter(id => id !== criterionId)
: [...prev, criterionId]
)
}
const toggleArt35 = (caseId: string) => {
setArt35Selected(prev =>
prev.includes(caseId)
? prev.filter(id => id !== caseId)
: [...prev, caseId]
)
}
const toggleAITrigger = (triggerId: string) => {
setAiTriggersSelected(prev =>
prev.includes(triggerId)
? prev.filter(id => id !== triggerId)
: [...prev, triggerId]
)
}
const handleSave = async () => {
const thresholdAnalysis: DSFAThresholdAnalysis = {
id: existingAnalysis?.id || crypto.randomUUID(),
dsfa_id: dsfa.id,
performed_at: new Date().toISOString(),
performed_by: 'current_user', // Would come from auth context
criteria_assessment: WP248_CRITERIA.map(c => ({
criterion_id: c.id,
applies: wp248Selected.includes(c.id),
justification: '',
})),
art35_abs3_assessment: ART35_ABS3_CASES.map(c => ({
case_id: c.id,
applies: art35Selected.includes(c.id),
justification: '',
})),
dsfa_required: dsfaRequired ?? recommendation === 'required',
decision_justification: justification,
documented: true,
}
await onUpdate({
threshold_analysis: thresholdAnalysis,
wp248_criteria_met: wp248Selected,
art35_abs3_triggered: art35Selected,
ai_trigger_ids: aiTriggersSelected,
involves_ai: aiTriggersSelected.length > 0,
})
}
return (
<div className="space-y-8">
{/* Step 1: WP248 Criteria */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Schritt 1: WP248 Kriterien pruefen
</h3>
<p className="text-sm text-gray-500 mb-4">
Pruefen Sie, welche der 9 Kriterien der Artikel-29-Datenschutzgruppe auf Ihre Verarbeitung zutreffen.
Bei 2 oder mehr erfuellten Kriterien ist eine DSFA in den meisten Faellen erforderlich.
</p>
<div className="space-y-3">
{WP248_CRITERIA.map((criterion) => (
<label
key={criterion.id}
className={`flex items-start gap-3 p-4 rounded-xl border cursor-pointer transition-all ${
wp248Selected.includes(criterion.id)
? 'bg-purple-50 border-purple-300 ring-1 ring-purple-300'
: 'bg-white border-gray-200 hover:bg-gray-50'
}`}
>
<input
type="checkbox"
checked={wp248Selected.includes(criterion.id)}
onChange={() => toggleWp248(criterion.id)}
className="mt-1 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900">{criterion.code}:</span>
<span className="text-gray-900">{criterion.title}</span>
</div>
<p className="text-sm text-gray-500 mt-1">{criterion.description}</p>
{criterion.examples.length > 0 && (
<p className="text-xs text-gray-400 mt-1">
Beispiele: {criterion.examples.join(', ')}
</p>
)}
</div>
</label>
))}
</div>
{/* WP248 Summary */}
<div className={`mt-4 p-4 rounded-xl border ${
wp248Selected.length >= 2
? 'bg-orange-50 border-orange-200'
: wp248Selected.length === 1
? 'bg-yellow-50 border-yellow-200'
: 'bg-green-50 border-green-200'
}`}>
<div className="flex items-center gap-2">
{wp248Selected.length >= 2 ? (
<svg className="w-5 h-5 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
) : (
<svg className="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
<span className="font-medium">
{wp248Selected.length} von 9 Kriterien erfuellt
</span>
</div>
<p className="text-sm mt-1 text-gray-600">
{wp248Result.reason}
</p>
</div>
</div>
{/* Step 2: Art. 35 Abs. 3 Cases */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Schritt 2: Art. 35 Abs. 3 Regelbeispiele
</h3>
<p className="text-sm text-gray-500 mb-4">
Bei Erfuellung eines Regelbeispiels ist eine DSFA zwingend erforderlich.
</p>
<div className="space-y-3">
{ART35_ABS3_CASES.map((caseItem) => (
<label
key={caseItem.id}
className={`flex items-start gap-3 p-4 rounded-xl border cursor-pointer transition-all ${
art35Selected.includes(caseItem.id)
? 'bg-red-50 border-red-300 ring-1 ring-red-300'
: 'bg-white border-gray-200 hover:bg-gray-50'
}`}
>
<input
type="checkbox"
checked={art35Selected.includes(caseItem.id)}
onChange={() => toggleArt35(caseItem.id)}
className="mt-1 rounded border-gray-300 text-red-600 focus:ring-red-500"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900">lit. {caseItem.lit}:</span>
<span className="text-gray-900">{caseItem.title}</span>
</div>
<p className="text-sm text-gray-500 mt-1">{caseItem.description}</p>
<span className="text-xs text-blue-600 mt-1 inline-block">{caseItem.gdprRef}</span>
</div>
</label>
))}
</div>
</div>
{/* Step 3: AI-specific Triggers */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Schritt 3: KI-spezifische Trigger
</h3>
<p className="text-sm text-gray-500 mb-4">
Wird kuenstliche Intelligenz eingesetzt? Diese Trigger sind in der deutschen DSFA-Muss-Liste enthalten.
</p>
<div className="space-y-3">
{AI_DSFA_TRIGGERS.map((trigger) => (
<label
key={trigger.id}
className={`flex items-start gap-3 p-4 rounded-xl border cursor-pointer transition-all ${
aiTriggersSelected.includes(trigger.id)
? 'bg-blue-50 border-blue-300 ring-1 ring-blue-300'
: 'bg-white border-gray-200 hover:bg-gray-50'
}`}
>
<input
type="checkbox"
checked={aiTriggersSelected.includes(trigger.id)}
onChange={() => toggleAITrigger(trigger.id)}
className="mt-1 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div className="flex-1">
<span className="font-medium text-gray-900">{trigger.title}</span>
<p className="text-sm text-gray-500 mt-1">{trigger.description}</p>
{trigger.examples.length > 0 && (
<p className="text-xs text-gray-400 mt-1">
Beispiele: {trigger.examples.join(', ')}
</p>
)}
</div>
</label>
))}
</div>
</div>
{/* Step 4: Decision */}
<div className="border-t border-gray-200 pt-8">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Schritt 4: Entscheidung
</h3>
<p className="text-sm text-gray-500 mb-4">
Dokumentieren Sie Ihre Entscheidung, ob eine DSFA erforderlich ist.
</p>
{/* Recommendation Banner */}
<div className={`mb-6 p-4 rounded-xl border ${
recommendation === 'required'
? 'bg-red-50 border-red-200'
: recommendation === 'possible'
? 'bg-yellow-50 border-yellow-200'
: 'bg-green-50 border-green-200'
}`}>
<div className="flex items-center gap-3">
{recommendation === 'required' ? (
<>
<div className="w-10 h-10 rounded-full bg-red-100 flex items-center justify-center">
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div>
<p className="font-semibold text-red-800">DSFA erforderlich</p>
<p className="text-sm text-red-600">
Basierend auf Ihrer Auswahl ist eine DSFA in den meisten Faellen Pflicht.
</p>
</div>
</>
) : recommendation === 'possible' ? (
<>
<div className="w-10 h-10 rounded-full bg-yellow-100 flex items-center justify-center">
<svg className="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<p className="font-semibold text-yellow-800">DSFA moeglicherweise erforderlich</p>
<p className="text-sm text-yellow-600">
Einzelfallpruefung empfohlen. Bei Unsicherheit DSFA durchfuehren.
</p>
</div>
</>
) : (
<>
<div className="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<p className="font-semibold text-green-800">DSFA wahrscheinlich nicht erforderlich</p>
<p className="text-sm text-green-600">
Keine Pflichtkriterien erfuellt. Dokumentieren Sie diese Entscheidung.
</p>
</div>
</>
)}
</div>
</div>
{/* Decision Radio Buttons */}
<div className="space-y-3 mb-6">
<label className={`flex items-center gap-3 p-4 rounded-xl border cursor-pointer transition-all ${
dsfaRequired === true
? 'bg-purple-50 border-purple-300 ring-1 ring-purple-300'
: 'bg-white border-gray-200 hover:bg-gray-50'
}`}>
<input
type="radio"
name="dsfaRequired"
checked={dsfaRequired === true}
onChange={() => setDsfaRequired(true)}
className="text-purple-600 focus:ring-purple-500"
/>
<div>
<span className="font-medium text-gray-900">DSFA erforderlich</span>
<p className="text-sm text-gray-500">Ich fuehre eine vollstaendige DSFA durch.</p>
</div>
</label>
<label className={`flex items-center gap-3 p-4 rounded-xl border cursor-pointer transition-all ${
dsfaRequired === false
? 'bg-purple-50 border-purple-300 ring-1 ring-purple-300'
: 'bg-white border-gray-200 hover:bg-gray-50'
}`}>
<input
type="radio"
name="dsfaRequired"
checked={dsfaRequired === false}
onChange={() => setDsfaRequired(false)}
className="text-purple-600 focus:ring-purple-500"
/>
<div>
<span className="font-medium text-gray-900">DSFA nicht erforderlich</span>
<p className="text-sm text-gray-500">
Die Verarbeitung erfordert keine DSFA. Die Entscheidung wird dokumentiert.
</p>
</div>
</label>
</div>
{/* Justification */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Begruendung der Entscheidung *
</label>
<p className="text-xs text-gray-500 mb-2">
Gem. DSK Kurzpapier Nr. 5 ist die Entscheidung und ihre Begruendung zu dokumentieren.
</p>
<textarea
value={justification}
onChange={(e) => setJustification(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
rows={4}
placeholder="Begruenden Sie, warum eine DSFA erforderlich/nicht erforderlich ist..."
/>
</div>
</div>
{/* Save Button */}
<div className="flex justify-end pt-4 border-t border-gray-200">
<button
onClick={handleSave}
disabled={isSubmitting || dsfaRequired === null || !justification.trim()}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors"
>
{isSubmitting ? 'Speichern...' : 'Entscheidung speichern & fortfahren'}
</button>
</div>
</div>
)
}