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>
208 lines
8.8 KiB
TypeScript
208 lines
8.8 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import type { Exercise } from './types'
|
|
|
|
const SCENARIO_PRESETS = [
|
|
{ label: 'Ransomware-Angriff', scenario: 'Ein Ransomware-Angriff verschluesselt saemtliche Produktivdaten. Backups sind vorhanden, aber der letzte Restore-Test liegt 6 Monate zurueck.' },
|
|
{ label: 'Datenabfluss durch Mitarbeiter', scenario: 'Ein ausscheidender Mitarbeiter hat vor seinem letzten Arbeitstag umfangreiche Kundendaten auf einen privaten USB-Stick kopiert.' },
|
|
{ label: 'Cloud-Provider-Ausfall', scenario: 'Ihr primaerer Cloud-Provider meldet einen groesseren Ausfall. Die Wiederherstellungszeit ist unbekannt. Betroffene koennen keine DSGVO-Rechte ausueben.' },
|
|
{ label: 'Phishing-Angriff', scenario: 'Mehrere Mitarbeiter haben auf einen Phishing-Link geklickt. Es besteht Verdacht, dass Anmeldedaten kompromittiert wurden und auf Personaldaten zugegriffen wurde.' },
|
|
]
|
|
|
|
export function ExercisesTab({
|
|
exercises,
|
|
setExercises,
|
|
showAdd,
|
|
setShowAdd,
|
|
}: {
|
|
exercises: Exercise[]
|
|
setExercises: React.Dispatch<React.SetStateAction<Exercise[]>>
|
|
showAdd: boolean
|
|
setShowAdd: (v: boolean) => void
|
|
}) {
|
|
const [newExercise, setNewExercise] = useState<Partial<Exercise>>({
|
|
title: '',
|
|
type: 'tabletop',
|
|
scenario: '',
|
|
scheduledDate: '',
|
|
participants: [],
|
|
lessonsLearned: '',
|
|
})
|
|
|
|
function addExercise() {
|
|
if (!newExercise.title || !newExercise.scheduledDate) return
|
|
const exercise: Exercise = {
|
|
id: `EX-${Date.now()}`,
|
|
title: newExercise.title || '',
|
|
type: (newExercise.type as Exercise['type']) || 'tabletop',
|
|
scenario: newExercise.scenario || '',
|
|
scheduledDate: newExercise.scheduledDate || '',
|
|
participants: newExercise.participants || [],
|
|
lessonsLearned: '',
|
|
}
|
|
setExercises(prev => [...prev, exercise])
|
|
setShowAdd(false)
|
|
setNewExercise({ title: '', type: 'tabletop', scenario: '', scheduledDate: '', participants: [], lessonsLearned: '' })
|
|
}
|
|
|
|
function completeExercise(id: string, lessonsLearned: string) {
|
|
setExercises(prev => prev.map(ex =>
|
|
ex.id === id
|
|
? { ...ex, completedDate: new Date().toISOString(), lessonsLearned }
|
|
: ex
|
|
))
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-lg font-semibold">Notfalluebungen & Tests</h3>
|
|
<p className="text-sm text-gray-500">Planen und dokumentieren Sie regelmaessige Notfalluebungen.</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowAdd(true)}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700"
|
|
>
|
|
+ Uebung planen
|
|
</button>
|
|
</div>
|
|
|
|
{/* Add Exercise Form */}
|
|
{showAdd && (
|
|
<div className="bg-white rounded-lg border p-6 space-y-4">
|
|
<h4 className="font-medium">Neue Uebung planen</h4>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Titel</label>
|
|
<input
|
|
type="text"
|
|
value={newExercise.title}
|
|
onChange={e => setNewExercise(prev => ({ ...prev, title: e.target.value }))}
|
|
placeholder="z.B. Tabletop: Ransomware-Angriff"
|
|
className="w-full border rounded px-3 py-2 text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Typ</label>
|
|
<select
|
|
value={newExercise.type}
|
|
onChange={e => setNewExercise(prev => ({ ...prev, type: e.target.value as Exercise['type'] }))}
|
|
className="w-full border rounded px-3 py-2 text-sm"
|
|
>
|
|
<option value="tabletop">Tabletop-Uebung</option>
|
|
<option value="simulation">Simulation</option>
|
|
<option value="full_drill">Vollstaendige Uebung</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Geplantes Datum</label>
|
|
<input
|
|
type="date"
|
|
value={newExercise.scheduledDate}
|
|
onChange={e => setNewExercise(prev => ({ ...prev, scheduledDate: e.target.value }))}
|
|
className="w-full border rounded px-3 py-2 text-sm"
|
|
/>
|
|
</div>
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Szenario
|
|
<span className="text-gray-400 font-normal ml-2">oder Vorlage waehlen:</span>
|
|
</label>
|
|
<div className="flex flex-wrap gap-2 mb-2">
|
|
{SCENARIO_PRESETS.map(preset => (
|
|
<button
|
|
key={preset.label}
|
|
onClick={() => setNewExercise(prev => ({
|
|
...prev,
|
|
scenario: preset.scenario,
|
|
title: prev.title || `Tabletop: ${preset.label}`,
|
|
}))}
|
|
className="text-xs px-3 py-1 bg-gray-100 rounded-full hover:bg-gray-200"
|
|
>
|
|
{preset.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<textarea
|
|
value={newExercise.scenario}
|
|
onChange={e => setNewExercise(prev => ({ ...prev, scenario: e.target.value }))}
|
|
placeholder="Beschreiben Sie das Uebungsszenario..."
|
|
rows={3}
|
|
className="w-full border rounded px-3 py-2 text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2 justify-end">
|
|
<button onClick={() => setShowAdd(false)} className="px-4 py-2 border rounded-lg text-sm">
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onClick={addExercise}
|
|
disabled={!newExercise.title || !newExercise.scheduledDate}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
Uebung planen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Exercises List */}
|
|
{exercises.length === 0 ? (
|
|
<div className="bg-white rounded-lg border p-12 text-center text-gray-500">
|
|
<p className="text-lg mb-2">Keine Uebungen geplant</p>
|
|
<p className="text-sm">Regelmaessige Notfalluebungen sind essentiell fuer ein funktionierendes Datenpannen-Management.</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{exercises.map(exercise => (
|
|
<div key={exercise.id} className="bg-white rounded-lg border p-6">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
exercise.completedDate ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
|
|
}`}>
|
|
{exercise.completedDate ? 'Abgeschlossen' : 'Geplant'}
|
|
</span>
|
|
<span className="text-xs text-gray-500 capitalize">
|
|
{exercise.type === 'tabletop' ? 'Tabletop' : exercise.type === 'simulation' ? 'Simulation' : 'Vollstaendige Uebung'}
|
|
</span>
|
|
</div>
|
|
<h4 className="font-medium mb-1">{exercise.title}</h4>
|
|
<p className="text-sm text-gray-600 mb-2">{exercise.scenario}</p>
|
|
<div className="flex items-center gap-4 text-xs text-gray-500">
|
|
<span>Geplant: {new Date(exercise.scheduledDate).toLocaleDateString('de-DE')}</span>
|
|
{exercise.completedDate && (
|
|
<span>Durchgefuehrt: {new Date(exercise.completedDate).toLocaleDateString('de-DE')}</span>
|
|
)}
|
|
</div>
|
|
{exercise.lessonsLearned && (
|
|
<div className="mt-3 p-3 bg-blue-50 rounded text-sm">
|
|
<span className="font-medium text-blue-800">Lessons Learned:</span>
|
|
<p className="text-blue-700 mt-1">{exercise.lessonsLearned}</p>
|
|
</div>
|
|
)}
|
|
{!exercise.completedDate && (
|
|
<div className="mt-3">
|
|
<button
|
|
onClick={() => {
|
|
const lessons = prompt('Lessons Learned aus der Uebung:')
|
|
if (lessons !== null) {
|
|
completeExercise(exercise.id, lessons)
|
|
}
|
|
}}
|
|
className="text-xs px-3 py-1 bg-green-100 text-green-800 rounded hover:bg-green-200"
|
|
>
|
|
Als durchgefuehrt markieren
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|