dsfa/[id]/page.tsx (1893 LOC -> 350 LOC) split into 9 components: Section1-5Editor, SDMCoverageOverview, RAGSearchPanel, AddRiskModal, AddMitigationModal. Page is now a thin orchestrator. notfallplan/page.tsx (1890 LOC -> 435 LOC) split into 8 modules: types.ts, ConfigTab, IncidentsTab, TemplatesTab, ExercisesTab, Modals, ApiSections. All under the 500-line hard cap. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
84 lines
3.4 KiB
TypeScript
84 lines
3.4 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import { DSFA, SDM_GOALS } from '@/lib/sdk/dsfa/types'
|
|
import type { SDMGoal } from '@/lib/sdk/dsfa/types'
|
|
import { RISK_CATALOG } from '@/lib/sdk/dsfa/risk-catalog'
|
|
import { MITIGATION_LIBRARY } from '@/lib/sdk/dsfa/mitigation-library'
|
|
|
|
export function SDMCoverageOverview({ dsfa }: { dsfa: DSFA }) {
|
|
const goals = Object.keys(SDM_GOALS) as SDMGoal[]
|
|
|
|
const goalCoverage = goals.map(goal => {
|
|
const catalogRisks = RISK_CATALOG.filter(r => r.sdmGoal === goal)
|
|
const catalogMitigations = MITIGATION_LIBRARY.filter(m => m.sdmGoals.includes(goal))
|
|
|
|
const matchedRisks = catalogRisks.filter(cr =>
|
|
dsfa.risks?.some(r => r.description?.includes(cr.title))
|
|
).length
|
|
|
|
const matchedMitigations = catalogMitigations.filter(cm =>
|
|
dsfa.mitigations?.some(m => m.description?.includes(cm.title))
|
|
).length
|
|
|
|
const hasRisks = matchedRisks > 0 || dsfa.risks?.some(r => {
|
|
const cat = r.category
|
|
if (goal === 'vertraulichkeit' && cat === 'confidentiality') return true
|
|
if (goal === 'integritaet' && cat === 'integrity') return true
|
|
if (goal === 'verfuegbarkeit' && cat === 'availability') return true
|
|
if (goal === 'nichtverkettung' && cat === 'rights_freedoms') return true
|
|
return false
|
|
})
|
|
|
|
const coverage = matchedMitigations > 0 ? 'covered' :
|
|
hasRisks ? 'gaps' : 'no_data'
|
|
|
|
return {
|
|
goal,
|
|
info: SDM_GOALS[goal],
|
|
matchedRisks,
|
|
matchedMitigations,
|
|
coverage,
|
|
}
|
|
})
|
|
|
|
return (
|
|
<div className="mt-6 bg-white rounded-xl border p-6">
|
|
<h3 className="text-md font-semibold text-gray-900 mb-1">SDM-Abdeckung (Gewaehrleistungsziele)</h3>
|
|
<p className="text-xs text-gray-500 mb-4">Uebersicht ueber die Abdeckung der 7 Gewaehrleistungsziele des Standard-Datenschutzmodells.</p>
|
|
|
|
<div className="grid grid-cols-7 gap-2">
|
|
{goalCoverage.map(({ goal, info, matchedRisks, matchedMitigations, coverage }) => (
|
|
<div
|
|
key={goal}
|
|
className={`p-3 rounded-lg text-center border ${
|
|
coverage === 'covered' ? 'bg-green-50 border-green-200' :
|
|
coverage === 'gaps' ? 'bg-yellow-50 border-yellow-200' :
|
|
'bg-gray-50 border-gray-200'
|
|
}`}
|
|
>
|
|
<div className={`text-lg mb-1 ${
|
|
coverage === 'covered' ? 'text-green-600' :
|
|
coverage === 'gaps' ? 'text-yellow-600' :
|
|
'text-gray-400'
|
|
}`}>
|
|
{coverage === 'covered' ? '\u2713' : coverage === 'gaps' ? '!' : '\u2013'}
|
|
</div>
|
|
<div className="text-xs font-medium text-gray-900 leading-tight">{info.name}</div>
|
|
<div className="text-[10px] text-gray-500 mt-1">
|
|
{matchedRisks}R / {matchedMitigations}M
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4 mt-3 text-xs text-gray-500">
|
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-green-500"></span> Abgedeckt</span>
|
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-yellow-500"></span> Luecken</span>
|
|
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-gray-300"></span> Keine Daten</span>
|
|
<span className="ml-auto">R = Risiken, M = Massnahmen</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|