Files
breakpilot-compliance/admin-compliance/app/sdk/notfallplan/_components/ExercisesTab.tsx
Sharang Parnerkar ef8284dff5 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>
2026-04-11 18:51:54 +02:00

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>
)
}