Files
breakpilot-compliance/admin-compliance/app/sdk/notfallplan/_components/Modals.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

297 lines
10 KiB
TypeScript

'use client'
import React from 'react'
interface ModalShellProps {
title: string
onClose: () => void
children: React.ReactNode
footer: React.ReactNode
}
function ModalShell({ title, onClose, children, footer }: ModalShellProps) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl max-w-md w-full mx-4">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 className="font-semibold text-gray-900">{title}</h3>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-6 space-y-4">{children}</div>
<div className="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">{footer}</div>
</div>
</div>
)
}
// ---- Contact Modal ----
export function ContactModal({
newContact,
setNewContact,
onClose,
onCreate,
saving,
}: {
newContact: { name: string; role: string; email: string; phone: string; is_primary: boolean; available_24h: boolean }
setNewContact: React.Dispatch<React.SetStateAction<typeof newContact>>
onClose: () => void
onCreate: () => void
saving: boolean
}) {
return (
<ModalShell
title="Notfallkontakt hinzufuegen"
onClose={onClose}
footer={
<>
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">
Abbrechen
</button>
<button
onClick={onCreate}
disabled={!newContact.name || saving}
className="px-4 py-2 text-sm text-white bg-blue-600 hover:bg-blue-700 rounded-lg font-medium disabled:opacity-50"
>
{saving ? 'Speichern...' : 'Hinzufuegen'}
</button>
</>
}
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name *</label>
<input
type="text"
value={newContact.name}
onChange={e => setNewContact(p => ({ ...p, name: e.target.value }))}
placeholder="Vollstaendiger Name"
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">Rolle</label>
<input
type="text"
value={newContact.role}
onChange={e => setNewContact(p => ({ ...p, role: e.target.value }))}
placeholder="z.B. Datenschutzbeauftragter"
className="w-full border rounded px-3 py-2 text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">E-Mail</label>
<input
type="email"
value={newContact.email}
onChange={e => setNewContact(p => ({ ...p, email: e.target.value }))}
placeholder="email@example.com"
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">Telefon</label>
<input
type="tel"
value={newContact.phone}
onChange={e => setNewContact(p => ({ ...p, phone: e.target.value }))}
placeholder="+49..."
className="w-full border rounded px-3 py-2 text-sm"
/>
</div>
</div>
<div className="flex items-center gap-6">
<label className="flex items-center gap-2 text-sm cursor-pointer">
<input
type="checkbox"
checked={newContact.is_primary}
onChange={e => setNewContact(p => ({ ...p, is_primary: e.target.checked }))}
className="rounded"
/>
Primaerkontakt
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer">
<input
type="checkbox"
checked={newContact.available_24h}
onChange={e => setNewContact(p => ({ ...p, available_24h: e.target.checked }))}
className="rounded"
/>
24/7 erreichbar
</label>
</div>
</ModalShell>
)
}
// ---- Exercise Modal ----
export function ExerciseModal({
newExercise,
setNewExercise,
onClose,
onCreate,
saving,
}: {
newExercise: { title: string; exercise_type: string; exercise_date: string; notes: string }
setNewExercise: React.Dispatch<React.SetStateAction<typeof newExercise>>
onClose: () => void
onCreate: () => void
saving: boolean
}) {
return (
<ModalShell
title="Neue Uebung planen"
onClose={onClose}
footer={
<>
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">
Abbrechen
</button>
<button
onClick={onCreate}
disabled={!newExercise.title || saving}
className="px-4 py-2 text-sm text-white bg-blue-600 hover:bg-blue-700 rounded-lg font-medium disabled:opacity-50"
>
{saving ? 'Speichern...' : 'Uebung erstellen'}
</button>
</>
}
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Titel *</label>
<input
type="text"
value={newExercise.title}
onChange={e => setNewExercise(p => ({ ...p, title: e.target.value }))}
placeholder="z.B. Tabletop-Uebung Ransomware"
className="w-full border rounded px-3 py-2 text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Uebungstyp</label>
<select
value={newExercise.exercise_type}
onChange={e => setNewExercise(p => ({ ...p, exercise_type: e.target.value }))}
className="w-full border rounded px-3 py-2 text-sm"
>
<option value="tabletop">Tabletop</option>
<option value="simulation">Simulation</option>
<option value="walkthrough">Walkthrough</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Datum</label>
<input
type="date"
value={newExercise.exercise_date}
onChange={e => setNewExercise(p => ({ ...p, exercise_date: e.target.value }))}
className="w-full border rounded px-3 py-2 text-sm"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notizen</label>
<textarea
value={newExercise.notes}
onChange={e => setNewExercise(p => ({ ...p, notes: e.target.value }))}
placeholder="Ziele, Teilnehmer, Ablauf..."
rows={3}
className="w-full border rounded px-3 py-2 text-sm"
/>
</div>
</ModalShell>
)
}
// ---- Scenario Modal ----
export function ScenarioModal({
newScenario,
setNewScenario,
onClose,
onCreate,
saving,
}: {
newScenario: { title: string; category: string; severity: string; description: string }
setNewScenario: React.Dispatch<React.SetStateAction<typeof newScenario>>
onClose: () => void
onCreate: () => void
saving: boolean
}) {
return (
<ModalShell
title="Notfallszenario hinzufuegen"
onClose={onClose}
footer={
<>
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">
Abbrechen
</button>
<button
onClick={onCreate}
disabled={!newScenario.title || saving}
className="px-4 py-2 text-sm text-white bg-blue-600 hover:bg-blue-700 rounded-lg font-medium disabled:opacity-50"
>
{saving ? 'Speichern...' : 'Hinzufuegen'}
</button>
</>
}
>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Titel *</label>
<input
type="text"
value={newScenario.title}
onChange={e => setNewScenario(p => ({ ...p, title: e.target.value }))}
placeholder="z.B. Ransomware-Angriff auf Produktivsysteme"
className="w-full border rounded px-3 py-2 text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Kategorie</label>
<select
value={newScenario.category}
onChange={e => setNewScenario(p => ({ ...p, category: e.target.value }))}
className="w-full border rounded px-3 py-2 text-sm"
>
<option value="data_breach">Datenpanne</option>
<option value="system_failure">Systemausfall</option>
<option value="physical">Physischer Vorfall</option>
<option value="other">Sonstiges</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Schweregrad</label>
<select
value={newScenario.severity}
onChange={e => setNewScenario(p => ({ ...p, severity: e.target.value }))}
className="w-full border rounded px-3 py-2 text-sm"
>
<option value="low">Niedrig</option>
<option value="medium">Mittel</option>
<option value="high">Hoch</option>
<option value="critical">Kritisch</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung</label>
<textarea
value={newScenario.description}
onChange={e => setNewScenario(p => ({ ...p, description: e.target.value }))}
placeholder="Kurzbeschreibung des Szenarios und moeglicher Auswirkungen..."
rows={3}
className="w-full border rounded px-3 py-2 text-sm"
/>
</div>
</ModalShell>
)
}