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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user