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:
BreakPilot Dev
2026-02-09 11:50:04 +01:00
parent 3899c86b29
commit 95e0a327c4
8 changed files with 2703 additions and 150 deletions

View File

@@ -2,6 +2,16 @@
import React from 'react'
// =============================================================================
// RE-EXPORTS FROM SEPARATE FILES
// =============================================================================
export { ThresholdAnalysisSection } from './ThresholdAnalysisSection'
export { DSFASidebar } from './DSFASidebar'
export { StakeholderConsultationSection } from './StakeholderConsultationSection'
export { Art36Warning } from './Art36Warning'
export { ReviewScheduleSection } from './ReviewScheduleSection'
// =============================================================================
// DSFA Card Component
// =============================================================================
@@ -62,56 +72,83 @@ export function DSFACard({ dsfa, onDelete, onExport }: DSFACardProps) {
// Risk Matrix Component
// =============================================================================
interface RiskMatrixProps {
risks: Array<{
id: string
title: string
probability: number
impact: number
risk_level?: string
}>
onRiskClick?: (riskId: string) => void
// DSFARisk type matching lib/sdk/dsfa/types.ts
interface DSFARiskInput {
id: string
category?: string
description: string
likelihood: 'low' | 'medium' | 'high'
impact: 'low' | 'medium' | 'high'
risk_level?: string
affected_data?: string[]
}
export function RiskMatrix({ risks, onRiskClick }: RiskMatrixProps) {
const levels = [1, 2, 3, 4, 5]
const levelLabels = ['Sehr gering', 'Gering', 'Mittel', 'Hoch', 'Sehr hoch']
interface RiskMatrixProps {
risks: DSFARiskInput[]
onRiskSelect?: (risk: DSFARiskInput) => void
onRiskClick?: (riskId: string) => void
onAddRisk?: (likelihood: 'low' | 'medium' | 'high', impact: 'low' | 'medium' | 'high') => void
selectedRiskId?: string
readOnly?: boolean
}
export function RiskMatrix({ risks, onRiskSelect, onRiskClick, onAddRisk, selectedRiskId, readOnly }: RiskMatrixProps) {
const likelihoodLevels: Array<'low' | 'medium' | 'high'> = ['high', 'medium', 'low']
const impactLevels: Array<'low' | 'medium' | 'high'> = ['low', 'medium', 'high']
const levelLabels = { low: 'Niedrig', medium: 'Mittel', high: 'Hoch' }
const cellColors: Record<string, string> = {
low: 'bg-green-100 hover:bg-green-200',
medium: 'bg-yellow-100 hover:bg-yellow-200',
high: 'bg-orange-100 hover:bg-orange-200',
critical: 'bg-red-100 hover:bg-red-200',
very_high: 'bg-red-100 hover:bg-red-200',
}
const getRiskColor = (prob: number, impact: number) => {
const score = prob * impact
if (score <= 4) return cellColors.low
if (score <= 9) return cellColors.medium
if (score <= 16) return cellColors.high
return cellColors.critical
const getRiskColor = (likelihood: 'low' | 'medium' | 'high', impact: 'low' | 'medium' | 'high') => {
const matrix: Record<string, Record<string, string>> = {
low: { low: 'low', medium: 'low', high: 'medium' },
medium: { low: 'low', medium: 'medium', high: 'high' },
high: { low: 'medium', medium: 'high', high: 'very_high' },
}
return cellColors[matrix[likelihood]?.[impact] || 'medium']
}
const handleCellClick = (likelihood: 'low' | 'medium' | 'high', impact: 'low' | 'medium' | 'high') => {
const cellRisks = risks.filter(r => r.likelihood === likelihood && r.impact === impact)
if (cellRisks.length > 0 && onRiskSelect) {
onRiskSelect(cellRisks[0])
} else if (cellRisks.length > 0 && onRiskClick) {
onRiskClick(cellRisks[0].id)
} else if (!readOnly && onAddRisk) {
onAddRisk(likelihood, impact)
}
}
return React.createElement('div', { className: 'bg-white rounded-xl border border-slate-200 p-5' },
React.createElement('h3', { className: 'font-semibold text-slate-900 mb-4' }, 'Risikomatrix'),
React.createElement('div', { className: 'grid grid-cols-6 gap-1' },
React.createElement('div', { className: 'text-xs text-slate-500 mb-2' }, 'Eintrittswahrscheinlichkeit ↑ | Schwere →'),
React.createElement('div', { className: 'grid grid-cols-4 gap-1' },
// Header row
React.createElement('div'),
...levels.map(l => React.createElement('div', {
key: `h-${l}`,
...impactLevels.map(i => React.createElement('div', {
key: `h-${i}`,
className: 'text-center text-xs text-slate-500 py-1'
}, levelLabels[l - 1])),
...levels.reverse().map(prob =>
}, levelLabels[i])),
// Grid rows
...likelihoodLevels.map(likelihood =>
[
React.createElement('div', {
key: `l-${prob}`,
key: `l-${likelihood}`,
className: 'text-right text-xs text-slate-500 pr-2 flex items-center justify-end'
}, levelLabels[prob - 1]),
...levels.map(impact => {
const cellRisks = risks.filter(r => r.probability === prob && r.impact === impact)
}, levelLabels[likelihood]),
...impactLevels.map(impact => {
const cellRisks = risks.filter(r => r.likelihood === likelihood && r.impact === impact)
const isSelected = cellRisks.some(r => r.id === selectedRiskId)
return React.createElement('div', {
key: `${prob}-${impact}`,
className: `aspect-square rounded ${getRiskColor(prob, impact)} flex items-center justify-center text-xs font-medium cursor-pointer`,
onClick: () => cellRisks[0] && onRiskClick?.(cellRisks[0].id)
}, cellRisks.length > 0 ? String(cellRisks.length) : '')
key: `${likelihood}-${impact}`,
className: `aspect-square rounded ${getRiskColor(likelihood, impact)} flex items-center justify-center text-xs font-medium cursor-pointer ${isSelected ? 'ring-2 ring-purple-500' : ''}`,
onClick: () => handleCellClick(likelihood, impact)
}, cellRisks.length > 0 ? String(cellRisks.length) : (readOnly ? '' : '+'))
})
]
).flat()