refactor(admin): split dsfa/[id] and notfallplan page.tsx into colocated components
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>
This commit is contained in:
182
admin-compliance/app/sdk/dsfa/[id]/_components/AddRiskModal.tsx
Normal file
182
admin-compliance/app/sdk/dsfa/[id]/_components/AddRiskModal.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user