feat: Betrieb-Module → 100% — Echte CRUD-Flows, kein Mock-Data
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
Alle 7 Betrieb-Module von 30–75% auf 100% gebracht: **Gruppe 1 — UI-Ergänzungen (Backend bereits vorhanden):** - incidents/page.tsx: IncidentCreateModal + IncidentDetailDrawer (Status-Transitions) - whistleblower/page.tsx: WhistleblowerCreateModal + CaseDetailPanel (Kommentare, Zuweisung) - dsr/page.tsx: DSRCreateModal + DSRDetailPanel (Workflow-Timeline, Status-Buttons) - vendor-compliance/page.tsx: VendorCreateModal + "Neuer Vendor" Button **Gruppe 2 — Escalations Full Stack:** - Migration 011: compliance_escalations Tabelle - Backend: escalation_routes.py (7 Endpoints: list/create/get/update/status/stats/delete) - Proxy: /api/sdk/v1/escalations/[[...path]] → backend:8002 - Frontend: Mock-Array komplett ersetzt durch echte API + EscalationCreateModal + EscalationDetailDrawer **Gruppe 2 — Consent Templates:** - Migration 010: compliance_consent_email_templates + compliance_consent_gdpr_processes (7+7 Seed-Einträge) - Backend: consent_template_routes.py (GET/POST/PUT/DELETE Templates + GET/PUT GDPR-Prozesse) - Proxy: /api/sdk/v1/consent-templates/[[...path]] - Frontend: consent-management/page.tsx lädt Templates + Prozesse aus DB (ApiTemplateEditor, ApiGdprProcessEditor) **Gruppe 3 — Notfallplan:** - Migration 012: 4 Tabellen (contacts, scenarios, checklists, exercises) - Backend: notfallplan_routes.py (vollständiges CRUD + /stats) - Proxy: /api/sdk/v1/notfallplan/[[...path]] - Frontend: notfallplan/page.tsx — DB-backed Kontakte + Szenarien + Übungen, ContactCreateModal + ScenarioCreateModal **Infrastruktur:** - __init__.py: escalation_router + consent_template_router + notfallplan_router registriert - Deploy-Skripte: apply_escalations_migration.sh, apply_consent_templates_migration.sh, apply_notfallplan_migration.sh - Tests: 40 neue Tests (test_escalation_routes.py, test_consent_template_routes.py, test_notfallplan_routes.py) - flow-data.ts: Completion aller 7 Module auf 100% gesetzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -323,6 +323,43 @@ function CountdownTimer({ detectedAt }: { detectedAt: string }) {
|
||||
// MAIN PAGE
|
||||
// =============================================================================
|
||||
|
||||
// =============================================================================
|
||||
// API TYPES (for DB-backed Notfallplan data)
|
||||
// =============================================================================
|
||||
|
||||
interface ApiContact {
|
||||
id: string
|
||||
name: string
|
||||
role: string | null
|
||||
email: string | null
|
||||
phone: string | null
|
||||
is_primary: boolean
|
||||
available_24h: boolean
|
||||
created_at: string | null
|
||||
}
|
||||
|
||||
interface ApiScenario {
|
||||
id: string
|
||||
title: string
|
||||
category: string | null
|
||||
severity: string
|
||||
description: string | null
|
||||
is_active: boolean
|
||||
created_at: string | null
|
||||
}
|
||||
|
||||
interface ApiExercise {
|
||||
id: string
|
||||
title: string
|
||||
exercise_type: string
|
||||
exercise_date: string | null
|
||||
outcome: string | null
|
||||
notes: string | null
|
||||
created_at: string | null
|
||||
}
|
||||
|
||||
const NOTFALLPLAN_API = '/api/sdk/v1/notfallplan'
|
||||
|
||||
export default function NotfallplanPage() {
|
||||
const { state } = useSDK()
|
||||
const [activeTab, setActiveTab] = useState<Tab>('config')
|
||||
@@ -334,6 +371,18 @@ export default function NotfallplanPage() {
|
||||
const [showAddExercise, setShowAddExercise] = useState(false)
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
// API-backed state
|
||||
const [apiContacts, setApiContacts] = useState<ApiContact[]>([])
|
||||
const [apiScenarios, setApiScenarios] = useState<ApiScenario[]>([])
|
||||
const [apiExercises, setApiExercises] = useState<ApiExercise[]>([])
|
||||
const [apiLoading, setApiLoading] = useState(false)
|
||||
const [showContactModal, setShowContactModal] = useState(false)
|
||||
const [showScenarioModal, setShowScenarioModal] = useState(false)
|
||||
const [newContact, setNewContact] = useState({ name: '', role: '', email: '', phone: '', is_primary: false, available_24h: false })
|
||||
const [newScenario, setNewScenario] = useState({ title: '', category: 'data_breach', severity: 'medium', description: '' })
|
||||
const [savingContact, setSavingContact] = useState(false)
|
||||
const [savingScenario, setSavingScenario] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const data = loadFromStorage()
|
||||
setConfig(data.config)
|
||||
@@ -342,6 +391,103 @@ export default function NotfallplanPage() {
|
||||
setExercises(data.exercises)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadApiData()
|
||||
}, [])
|
||||
|
||||
async function loadApiData() {
|
||||
setApiLoading(true)
|
||||
try {
|
||||
const [contactsRes, scenariosRes, exercisesRes] = await Promise.all([
|
||||
fetch(`${NOTFALLPLAN_API}/contacts`),
|
||||
fetch(`${NOTFALLPLAN_API}/scenarios`),
|
||||
fetch(`${NOTFALLPLAN_API}/exercises`),
|
||||
])
|
||||
if (contactsRes.ok) {
|
||||
const data = await contactsRes.json()
|
||||
setApiContacts(Array.isArray(data) ? data : [])
|
||||
}
|
||||
if (scenariosRes.ok) {
|
||||
const data = await scenariosRes.json()
|
||||
setApiScenarios(Array.isArray(data) ? data : [])
|
||||
}
|
||||
if (exercisesRes.ok) {
|
||||
const data = await exercisesRes.json()
|
||||
setApiExercises(Array.isArray(data) ? data : [])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load Notfallplan API data:', err)
|
||||
} finally {
|
||||
setApiLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateContact() {
|
||||
if (!newContact.name) return
|
||||
setSavingContact(true)
|
||||
try {
|
||||
const res = await fetch(`${NOTFALLPLAN_API}/contacts`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newContact),
|
||||
})
|
||||
if (res.ok) {
|
||||
const created = await res.json()
|
||||
setApiContacts(prev => [created, ...prev])
|
||||
setShowContactModal(false)
|
||||
setNewContact({ name: '', role: '', email: '', phone: '', is_primary: false, available_24h: false })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to create contact:', err)
|
||||
} finally {
|
||||
setSavingContact(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteContact(id: string) {
|
||||
try {
|
||||
const res = await fetch(`${NOTFALLPLAN_API}/contacts/${id}`, { method: 'DELETE' })
|
||||
if (res.ok) {
|
||||
setApiContacts(prev => prev.filter(c => c.id !== id))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete contact:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateScenario() {
|
||||
if (!newScenario.title) return
|
||||
setSavingScenario(true)
|
||||
try {
|
||||
const res = await fetch(`${NOTFALLPLAN_API}/scenarios`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newScenario),
|
||||
})
|
||||
if (res.ok) {
|
||||
const created = await res.json()
|
||||
setApiScenarios(prev => [created, ...prev])
|
||||
setShowScenarioModal(false)
|
||||
setNewScenario({ title: '', category: 'data_breach', severity: 'medium', description: '' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to create scenario:', err)
|
||||
} finally {
|
||||
setSavingScenario(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteScenario(id: string) {
|
||||
try {
|
||||
const res = await fetch(`${NOTFALLPLAN_API}/scenarios/${id}`, { method: 'DELETE' })
|
||||
if (res.ok) {
|
||||
setApiScenarios(prev => prev.filter(s => s.id !== id))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete scenario:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
saveToStorage({ config, incidents, templates, exercises })
|
||||
setSaved(true)
|
||||
@@ -359,6 +505,16 @@ export default function NotfallplanPage() {
|
||||
<div className="space-y-6">
|
||||
<StepHeader stepId="notfallplan" />
|
||||
|
||||
{/* API Stats Banner */}
|
||||
{!apiLoading && (apiContacts.length > 0 || apiScenarios.length > 0) && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg px-4 py-3 flex items-center gap-6 text-sm">
|
||||
<span className="font-medium text-blue-900">Notfallplan-Datenbank:</span>
|
||||
<span className="text-blue-700">{apiContacts.length} Kontakte</span>
|
||||
<span className="text-blue-700">{apiScenarios.length} Szenarien</span>
|
||||
<span className="text-blue-700">{apiExercises.length} Uebungen</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
@@ -399,7 +555,101 @@ export default function NotfallplanPage() {
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'config' && (
|
||||
<ConfigTab config={config} setConfig={setConfig} />
|
||||
<>
|
||||
{/* API Contacts & Scenarios section */}
|
||||
<div className="space-y-4">
|
||||
{/* Contacts */}
|
||||
<div className="bg-white rounded-lg border p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold">Notfallkontakte (Datenbank)</h3>
|
||||
<p className="text-sm text-gray-500">Zentral gespeicherte Kontakte fuer den Notfall</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowContactModal(true)}
|
||||
className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700"
|
||||
>
|
||||
+ Kontakt hinzufuegen
|
||||
</button>
|
||||
</div>
|
||||
{apiLoading ? (
|
||||
<div className="text-sm text-gray-500 py-4 text-center">Lade Kontakte...</div>
|
||||
) : apiContacts.length === 0 ? (
|
||||
<div className="text-sm text-gray-400 py-4 text-center">Noch keine Kontakte hinterlegt</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{apiContacts.map(contact => (
|
||||
<div key={contact.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
{contact.is_primary && <span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full font-medium">Primaer</span>}
|
||||
{contact.available_24h && <span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full font-medium">24/7</span>}
|
||||
<div>
|
||||
<span className="font-medium text-sm">{contact.name}</span>
|
||||
{contact.role && <span className="text-gray-500 text-sm ml-2">({contact.role})</span>}
|
||||
<div className="text-xs text-gray-400">{contact.email} {contact.phone && `| ${contact.phone}`}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteContact(contact.id)}
|
||||
className="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded hover:bg-red-50"
|
||||
>
|
||||
Loeschen
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Scenarios */}
|
||||
<div className="bg-white rounded-lg border p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold">Notfallszenarien (Datenbank)</h3>
|
||||
<p className="text-sm text-gray-500">Definierte Szenarien und Reaktionsplaene</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowScenarioModal(true)}
|
||||
className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700"
|
||||
>
|
||||
+ Szenario hinzufuegen
|
||||
</button>
|
||||
</div>
|
||||
{apiLoading ? (
|
||||
<div className="text-sm text-gray-500 py-4 text-center">Lade Szenarien...</div>
|
||||
) : apiScenarios.length === 0 ? (
|
||||
<div className="text-sm text-gray-400 py-4 text-center">Noch keine Szenarien definiert</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{apiScenarios.map(scenario => (
|
||||
<div key={scenario.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<span className="font-medium text-sm">{scenario.title}</span>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
{scenario.category && <span className="text-xs bg-slate-100 text-slate-600 px-2 py-0.5 rounded">{scenario.category}</span>}
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${
|
||||
scenario.severity === 'critical' ? 'bg-red-100 text-red-700' :
|
||||
scenario.severity === 'high' ? 'bg-orange-100 text-orange-700' :
|
||||
scenario.severity === 'medium' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-green-100 text-green-700'
|
||||
}`}>{scenario.severity}</span>
|
||||
</div>
|
||||
{scenario.description && <p className="text-xs text-gray-500 mt-1 truncate max-w-lg">{scenario.description}</p>}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteScenario(scenario.id)}
|
||||
className="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded hover:bg-red-50"
|
||||
>
|
||||
Loeschen
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ConfigTab config={config} setConfig={setConfig} />
|
||||
</>
|
||||
)}
|
||||
{activeTab === 'incidents' && (
|
||||
<IncidentsTab
|
||||
@@ -413,12 +663,211 @@ export default function NotfallplanPage() {
|
||||
<TemplatesTab templates={templates} setTemplates={setTemplates} />
|
||||
)}
|
||||
{activeTab === 'exercises' && (
|
||||
<ExercisesTab
|
||||
exercises={exercises}
|
||||
setExercises={setExercises}
|
||||
showAdd={showAddExercise}
|
||||
setShowAdd={setShowAddExercise}
|
||||
/>
|
||||
<>
|
||||
{/* API Exercises */}
|
||||
{(apiExercises.length > 0 || !apiLoading) && (
|
||||
<div className="bg-white rounded-lg border p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-base font-semibold">Uebungen (Datenbank)</h3>
|
||||
<span className="text-sm text-gray-500">{apiExercises.length} Eintraege</span>
|
||||
</div>
|
||||
{apiExercises.length === 0 ? (
|
||||
<div className="text-sm text-gray-400 py-2 text-center">Noch keine Uebungen in der Datenbank</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{apiExercises.map(ex => (
|
||||
<div key={ex.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<span className="font-medium text-sm">{ex.title}</span>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs bg-slate-100 text-slate-600 px-2 py-0.5 rounded">{ex.exercise_type}</span>
|
||||
{ex.exercise_date && <span className="text-xs text-gray-400">{new Date(ex.exercise_date).toLocaleDateString('de-DE')}</span>}
|
||||
{ex.outcome && <span className={`text-xs px-2 py-0.5 rounded ${ex.outcome === 'passed' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>{ex.outcome}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ExercisesTab
|
||||
exercises={exercises}
|
||||
setExercises={setExercises}
|
||||
showAdd={showAddExercise}
|
||||
setShowAdd={setShowAddExercise}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Contact Create Modal */}
|
||||
{showContactModal && (
|
||||
<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">Notfallkontakt hinzufuegen</h3>
|
||||
<button onClick={() => setShowContactModal(false)} 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">
|
||||
<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>
|
||||
</div>
|
||||
<div className="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
|
||||
<button onClick={() => setShowContactModal(false)} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreateContact}
|
||||
disabled={!newContact.name || savingContact}
|
||||
className="px-4 py-2 text-sm text-white bg-blue-600 hover:bg-blue-700 rounded-lg font-medium disabled:opacity-50"
|
||||
>
|
||||
{savingContact ? 'Speichern...' : 'Hinzufuegen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Scenario Create Modal */}
|
||||
{showScenarioModal && (
|
||||
<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">Notfallszenario hinzufuegen</h3>
|
||||
<button onClick={() => setShowScenarioModal(false)} 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">
|
||||
<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>
|
||||
</div>
|
||||
<div className="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
|
||||
<button onClick={() => setShowScenarioModal(false)} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreateScenario}
|
||||
disabled={!newScenario.title || savingScenario}
|
||||
className="px-4 py-2 text-sm text-white bg-blue-600 hover:bg-blue-700 rounded-lg font-medium disabled:opacity-50"
|
||||
>
|
||||
{savingScenario ? 'Speichern...' : 'Hinzufuegen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user