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>
This commit is contained in:
268
admin-v2/components/sdk/dsfa/DSFASidebar.tsx
Normal file
268
admin-v2/components/sdk/dsfa/DSFASidebar.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user