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>
183 lines
7.4 KiB
TypeScript
183 lines
7.4 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import {
|
|
DSFA_RISK_LEVEL_LABELS,
|
|
calculateRiskLevel,
|
|
} from '@/lib/sdk/dsfa/types'
|
|
import type { DSFARiskCategory } from '@/lib/sdk/dsfa/types'
|
|
import type { SDMGoal } from '@/lib/sdk/dsfa/types'
|
|
import {
|
|
RISK_CATALOG,
|
|
RISK_CATEGORY_LABELS,
|
|
} from '@/lib/sdk/dsfa/risk-catalog'
|
|
import type { CatalogRisk } from '@/lib/sdk/dsfa/risk-catalog'
|
|
import { SDM_GOAL_LABELS } from '@/lib/sdk/dsfa/mitigation-library'
|
|
|
|
export function AddRiskModal({
|
|
likelihood,
|
|
impact,
|
|
onClose,
|
|
onAdd,
|
|
}: {
|
|
likelihood: 'low' | 'medium' | 'high'
|
|
impact: 'low' | 'medium' | 'high'
|
|
onClose: () => void
|
|
onAdd: (data: { category: string; description: string }) => void
|
|
}) {
|
|
const [mode, setMode] = useState<'catalog' | 'manual'>('catalog')
|
|
const [category, setCategory] = useState('confidentiality')
|
|
const [description, setDescription] = useState('')
|
|
const [catalogFilter, setCatalogFilter] = useState<DSFARiskCategory | 'all'>('all')
|
|
const [sdmFilter, setSdmFilter] = useState<SDMGoal | 'all'>('all')
|
|
|
|
const { level } = calculateRiskLevel(likelihood, impact)
|
|
|
|
const filteredCatalog = RISK_CATALOG.filter(r => {
|
|
if (catalogFilter !== 'all' && r.category !== catalogFilter) return false
|
|
if (sdmFilter !== 'all' && r.sdmGoal !== sdmFilter) return false
|
|
return true
|
|
})
|
|
|
|
function selectCatalogRisk(risk: CatalogRisk) {
|
|
setCategory(risk.category)
|
|
setDescription(`${risk.title}\n\n${risk.description}`)
|
|
setMode('manual')
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-xl p-6 max-w-2xl w-full mx-4 max-h-[85vh] overflow-y-auto">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Risiko hinzufuegen</h3>
|
|
|
|
<div className="mb-4 p-3 rounded-lg bg-gray-50">
|
|
<div className="text-sm text-gray-500">
|
|
Eintrittswahrscheinlichkeit: <span className="font-medium text-gray-700">{likelihood === 'low' ? 'Niedrig' : likelihood === 'medium' ? 'Mittel' : 'Hoch'}</span>
|
|
{' | '}
|
|
Auswirkung: <span className="font-medium text-gray-700">{impact === 'low' ? 'Niedrig' : impact === 'medium' ? 'Mittel' : 'Hoch'}</span>
|
|
{' | '}
|
|
Risikostufe: <span className="font-medium">{DSFA_RISK_LEVEL_LABELS[level]}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tab Toggle */}
|
|
<div className="flex border-b mb-4">
|
|
<button
|
|
onClick={() => setMode('catalog')}
|
|
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
|
|
mode === 'catalog' ? 'border-purple-500 text-purple-600' : 'border-transparent text-gray-500 hover:text-gray-700'
|
|
}`}
|
|
>
|
|
Aus Katalog waehlen ({RISK_CATALOG.length})
|
|
</button>
|
|
<button
|
|
onClick={() => setMode('manual')}
|
|
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
|
|
mode === 'manual' ? 'border-purple-500 text-purple-600' : 'border-transparent text-gray-500 hover:text-gray-700'
|
|
}`}
|
|
>
|
|
Manuell eingeben
|
|
</button>
|
|
</div>
|
|
|
|
{mode === 'catalog' ? (
|
|
<div className="space-y-3">
|
|
{/* Filters */}
|
|
<div className="flex gap-2">
|
|
<select
|
|
value={catalogFilter}
|
|
onChange={e => setCatalogFilter(e.target.value as DSFARiskCategory | 'all')}
|
|
className="text-sm border rounded px-2 py-1"
|
|
>
|
|
<option value="all">Alle Kategorien</option>
|
|
{Object.entries(RISK_CATEGORY_LABELS).map(([key, label]) => (
|
|
<option key={key} value={key}>{label}</option>
|
|
))}
|
|
</select>
|
|
<select
|
|
value={sdmFilter}
|
|
onChange={e => setSdmFilter(e.target.value as SDMGoal | 'all')}
|
|
className="text-sm border rounded px-2 py-1"
|
|
>
|
|
<option value="all">Alle SDM-Ziele</option>
|
|
{Object.entries(SDM_GOAL_LABELS).map(([key, label]) => (
|
|
<option key={key} value={key}>{label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Catalog List */}
|
|
<div className="max-h-[40vh] overflow-y-auto space-y-2">
|
|
{filteredCatalog.map(risk => (
|
|
<button
|
|
key={risk.id}
|
|
onClick={() => selectCatalogRisk(risk)}
|
|
className="w-full text-left p-3 rounded-lg border hover:border-purple-300 hover:bg-purple-50 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="text-xs font-mono text-gray-400">{risk.id}</span>
|
|
<span className="text-xs px-2 py-0.5 rounded-full bg-gray-100 text-gray-600">
|
|
{RISK_CATEGORY_LABELS[risk.category]}
|
|
</span>
|
|
<span className="text-xs px-2 py-0.5 rounded-full bg-blue-50 text-blue-600">
|
|
{SDM_GOAL_LABELS[risk.sdmGoal]}
|
|
</span>
|
|
</div>
|
|
<div className="text-sm font-medium text-gray-900">{risk.title}</div>
|
|
<div className="text-xs text-gray-500 mt-1 line-clamp-2">{risk.description}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
{filteredCatalog.length === 0 && (
|
|
<p className="text-sm text-gray-500 text-center py-4">Keine Risiken fuer die gewaehlten Filter.</p>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Kategorie</label>
|
|
<select
|
|
value={category}
|
|
onChange={(e) => setCategory(e.target.value)}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
>
|
|
<option value="confidentiality">Vertraulichkeit</option>
|
|
<option value="integrity">Integritaet</option>
|
|
<option value="availability">Verfuegbarkeit</option>
|
|
<option value="rights_freedoms">Rechte & Freiheiten</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Beschreibung</label>
|
|
<textarea
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
rows={4}
|
|
placeholder="Beschreiben Sie das Risiko..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-3 mt-6">
|
|
<button
|
|
onClick={onClose}
|
|
className="flex-1 py-2 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onClick={() => onAdd({ category, description })}
|
|
disabled={!description.trim() || mode === 'catalog'}
|
|
className="flex-1 py-2 px-4 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 disabled:opacity-50"
|
|
>
|
|
Hinzufuegen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|