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/DSFASidebar.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

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>
)
}