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>
269 lines
9.6 KiB
TypeScript
269 lines
9.6 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import { DSFA, DSFA_SECTIONS, DSFASectionProgress } from '@/lib/sdk/dsfa/types'
|
|
|
|
interface DSFASidebarProps {
|
|
dsfa: DSFA
|
|
activeSection: number
|
|
onSectionChange: (section: number) => void
|
|
}
|
|
|
|
// Calculate completion percentage for a section
|
|
function calculateSectionProgress(dsfa: DSFA, sectionNumber: number): number {
|
|
switch (sectionNumber) {
|
|
case 0: // Threshold Analysis
|
|
if (!dsfa.threshold_analysis) return 0
|
|
const ta = dsfa.threshold_analysis
|
|
if (ta.dsfa_required !== undefined && ta.decision_justification) return 100
|
|
if (ta.criteria_assessment?.some(c => c.applies)) return 50
|
|
return 0
|
|
|
|
case 1: // Processing Description
|
|
const s1Fields = [
|
|
dsfa.processing_purpose,
|
|
dsfa.processing_description,
|
|
dsfa.data_categories?.length,
|
|
dsfa.legal_basis,
|
|
]
|
|
return Math.round((s1Fields.filter(Boolean).length / s1Fields.length) * 100)
|
|
|
|
case 2: // Necessity & Proportionality
|
|
const s2Fields = [
|
|
dsfa.necessity_assessment,
|
|
dsfa.proportionality_assessment,
|
|
]
|
|
return Math.round((s2Fields.filter(Boolean).length / s2Fields.length) * 100)
|
|
|
|
case 3: // Risk Assessment
|
|
if (!dsfa.risks?.length) return 0
|
|
if (dsfa.overall_risk_level) return 100
|
|
return 50
|
|
|
|
case 4: // Mitigation Measures
|
|
if (!dsfa.mitigations?.length) return 0
|
|
if (dsfa.residual_risk_level) return 100
|
|
return 50
|
|
|
|
case 5: // Stakeholder Consultation (optional)
|
|
if (dsfa.stakeholder_consultation_not_appropriate && dsfa.stakeholder_consultation_not_appropriate_reason) return 100
|
|
if (dsfa.stakeholder_consultations?.length) return 100
|
|
return 0
|
|
|
|
case 6: // DPO & Authority Consultation
|
|
const s6Fields = [
|
|
dsfa.dpo_consulted,
|
|
dsfa.dpo_opinion,
|
|
]
|
|
const s6Progress = Math.round((s6Fields.filter(Boolean).length / s6Fields.length) * 100)
|
|
// Add extra progress if authority consultation is documented when required
|
|
if (dsfa.consultation_requirement?.consultation_required) {
|
|
if (dsfa.authority_consulted) return s6Progress
|
|
return Math.min(s6Progress, 75)
|
|
}
|
|
return s6Progress
|
|
|
|
case 7: // Review & Maintenance
|
|
if (!dsfa.review_schedule) return 0
|
|
const rs = dsfa.review_schedule
|
|
if (rs.next_review_date && rs.review_frequency_months && rs.review_responsible) return 100
|
|
if (rs.next_review_date) return 50
|
|
return 25
|
|
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// Check if a section is complete
|
|
function isSectionComplete(dsfa: DSFA, sectionNumber: number): boolean {
|
|
const progress = dsfa.section_progress
|
|
switch (sectionNumber) {
|
|
case 0: return progress.section_0_complete ?? false
|
|
case 1: return progress.section_1_complete ?? false
|
|
case 2: return progress.section_2_complete ?? false
|
|
case 3: return progress.section_3_complete ?? false
|
|
case 4: return progress.section_4_complete ?? false
|
|
case 5: return progress.section_5_complete ?? false
|
|
case 6: return progress.section_6_complete ?? false
|
|
case 7: return progress.section_7_complete ?? false
|
|
default: return false
|
|
}
|
|
}
|
|
|
|
// Calculate overall DSFA progress
|
|
function calculateOverallProgress(dsfa: DSFA): number {
|
|
const requiredSections = DSFA_SECTIONS.filter(s => s.required)
|
|
let totalProgress = 0
|
|
|
|
for (const section of requiredSections) {
|
|
totalProgress += calculateSectionProgress(dsfa, section.number)
|
|
}
|
|
|
|
return Math.round(totalProgress / requiredSections.length)
|
|
}
|
|
|
|
export function DSFASidebar({ dsfa, activeSection, onSectionChange }: DSFASidebarProps) {
|
|
const overallProgress = calculateOverallProgress(dsfa)
|
|
|
|
// Group sections by category
|
|
const thresholdSection = DSFA_SECTIONS.find(s => s.number === 0)
|
|
const art35Sections = DSFA_SECTIONS.filter(s => s.number >= 1 && s.number <= 4)
|
|
const stakeholderSection = DSFA_SECTIONS.find(s => s.number === 5)
|
|
const consultationSection = DSFA_SECTIONS.find(s => s.number === 6)
|
|
const reviewSection = DSFA_SECTIONS.find(s => s.number === 7)
|
|
|
|
const renderSectionItem = (section: typeof DSFA_SECTIONS[0]) => {
|
|
const progress = calculateSectionProgress(dsfa, section.number)
|
|
const isComplete = isSectionComplete(dsfa, section.number) || progress === 100
|
|
const isActive = activeSection === section.number
|
|
|
|
return (
|
|
<button
|
|
key={section.number}
|
|
onClick={() => onSectionChange(section.number)}
|
|
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-all ${
|
|
isActive
|
|
? 'bg-purple-100 text-purple-700'
|
|
: 'hover:bg-gray-100 text-gray-700'
|
|
}`}
|
|
>
|
|
{/* Status indicator */}
|
|
<div className={`w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0 ${
|
|
isComplete
|
|
? 'bg-green-500 text-white'
|
|
: isActive
|
|
? 'bg-purple-500 text-white'
|
|
: 'bg-gray-200 text-gray-500'
|
|
}`}>
|
|
{isComplete ? (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
) : (
|
|
<span className="text-xs font-medium">{section.number}</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Section info */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<span className={`text-sm font-medium truncate ${isActive ? 'text-purple-700' : ''}`}>
|
|
{section.titleDE}
|
|
</span>
|
|
{!section.required && (
|
|
<span className="text-[10px] px-1.5 py-0.5 bg-gray-200 text-gray-500 rounded">
|
|
optional
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Progress bar */}
|
|
<div className="mt-1 h-1 bg-gray-200 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full transition-all duration-300 ${
|
|
isComplete ? 'bg-green-500' : 'bg-purple-500'
|
|
}`}
|
|
style={{ width: `${progress}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress percentage */}
|
|
<span className={`text-xs font-medium ${
|
|
isComplete ? 'text-green-600' : 'text-gray-400'
|
|
}`}>
|
|
{progress}%
|
|
</span>
|
|
</button>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-4">
|
|
{/* Overall Progress */}
|
|
<div className="mb-6">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h3 className="text-sm font-semibold text-gray-900">DSFA Fortschritt</h3>
|
|
<span className="text-lg font-bold text-purple-600">{overallProgress}%</span>
|
|
</div>
|
|
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-purple-500 to-purple-600 transition-all duration-500"
|
|
style={{ width: `${overallProgress}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section 0: Threshold Analysis */}
|
|
<div className="mb-4">
|
|
<div className="text-xs font-medium text-gray-400 uppercase tracking-wider mb-2 px-3">
|
|
Vorabpruefung
|
|
</div>
|
|
{thresholdSection && renderSectionItem(thresholdSection)}
|
|
</div>
|
|
|
|
{/* Sections 1-4: Art. 35 Abs. 7 */}
|
|
<div className="mb-4">
|
|
<div className="text-xs font-medium text-gray-400 uppercase tracking-wider mb-2 px-3">
|
|
Art. 35 Abs. 7 DSGVO
|
|
</div>
|
|
<div className="space-y-1">
|
|
{art35Sections.map(section => renderSectionItem(section))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section 5: Stakeholder Consultation */}
|
|
<div className="mb-4">
|
|
<div className="text-xs font-medium text-gray-400 uppercase tracking-wider mb-2 px-3">
|
|
Betroffene
|
|
</div>
|
|
{stakeholderSection && renderSectionItem(stakeholderSection)}
|
|
</div>
|
|
|
|
{/* Section 6: DPO & Authority */}
|
|
<div className="mb-4">
|
|
<div className="text-xs font-medium text-gray-400 uppercase tracking-wider mb-2 px-3">
|
|
Konsultation
|
|
</div>
|
|
{consultationSection && renderSectionItem(consultationSection)}
|
|
</div>
|
|
|
|
{/* Section 7: Review */}
|
|
<div>
|
|
<div className="text-xs font-medium text-gray-400 uppercase tracking-wider mb-2 px-3">
|
|
Fortschreibung
|
|
</div>
|
|
{reviewSection && renderSectionItem(reviewSection)}
|
|
</div>
|
|
|
|
{/* Status Footer */}
|
|
<div className="mt-6 pt-4 border-t border-gray-200">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-gray-500">Status</span>
|
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
|
dsfa.status === 'draft' ? 'bg-gray-100 text-gray-600' :
|
|
dsfa.status === 'in_review' ? 'bg-yellow-100 text-yellow-700' :
|
|
dsfa.status === 'approved' ? 'bg-green-100 text-green-700' :
|
|
dsfa.status === 'rejected' ? 'bg-red-100 text-red-700' :
|
|
'bg-orange-100 text-orange-700'
|
|
}`}>
|
|
{dsfa.status === 'draft' ? 'Entwurf' :
|
|
dsfa.status === 'in_review' ? 'In Pruefung' :
|
|
dsfa.status === 'approved' ? 'Genehmigt' :
|
|
dsfa.status === 'rejected' ? 'Abgelehnt' :
|
|
'Ueberarbeitung'}
|
|
</span>
|
|
</div>
|
|
|
|
{dsfa.version && (
|
|
<div className="flex items-center justify-between text-sm mt-2">
|
|
<span className="text-gray-500">Version</span>
|
|
<span className="text-gray-700">{dsfa.version}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|