Agent-completed splits committed after agents hit rate limits before committing their work. All 4 pages now under 500 LOC: - consent-management: 1303 -> 193 LOC (+ 7 _components, _hooks, _data, _types) - control-library: 1210 -> 298 LOC (+ _components, _types) - incidents: 1150 -> 373 LOC (+ _components) - training: 1127 -> 366 LOC (+ _components) Verification: next build clean (142 pages generated). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
210 lines
8.2 KiB
TypeScript
210 lines
8.2 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
|
|
export function IncidentCreateModal({
|
|
onClose,
|
|
onSuccess
|
|
}: {
|
|
onClose: () => void
|
|
onSuccess: () => void
|
|
}) {
|
|
const [title, setTitle] = useState('')
|
|
const [category, setCategory] = useState('data_breach')
|
|
const [severity, setSeverity] = useState('medium')
|
|
const [description, setDescription] = useState('')
|
|
const [detectedBy, setDetectedBy] = useState('')
|
|
const [affectedSystems, setAffectedSystems] = useState('')
|
|
const [estimatedAffectedPersons, setEstimatedAffectedPersons] = useState('0')
|
|
const [isSaving, setIsSaving] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const handleSave = async () => {
|
|
if (!title.trim()) {
|
|
setError('Titel ist erforderlich.')
|
|
return
|
|
}
|
|
setIsSaving(true)
|
|
setError(null)
|
|
try {
|
|
const res = await fetch('/api/sdk/v1/incidents', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
title,
|
|
category,
|
|
severity,
|
|
description,
|
|
detectedBy,
|
|
affectedSystems: affectedSystems.split(',').map(s => s.trim()).filter(Boolean),
|
|
estimatedAffectedPersons: Number(estimatedAffectedPersons)
|
|
})
|
|
})
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}))
|
|
throw new Error(data.detail || data.message || `Fehler: ${res.status}`)
|
|
}
|
|
onSuccess()
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
{/* Backdrop */}
|
|
<div
|
|
className="absolute inset-0 bg-black/50"
|
|
onClick={onClose}
|
|
/>
|
|
{/* Modal */}
|
|
<div className="relative bg-white rounded-xl shadow-2xl w-full max-w-lg mx-4 p-6 z-10 max-h-[90vh] overflow-y-auto">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-xl font-semibold text-gray-900">Neuen Vorfall erfassen</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-1 text-gray-400 hover:text-gray-600 rounded transition-colors"
|
|
>
|
|
<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>
|
|
|
|
{error && (
|
|
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-4">
|
|
{/* Title */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Titel <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={title}
|
|
onChange={e => setTitle(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm"
|
|
placeholder="Kurze Beschreibung des Vorfalls"
|
|
/>
|
|
</div>
|
|
|
|
{/* Category */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Kategorie</label>
|
|
<select
|
|
value={category}
|
|
onChange={e => setCategory(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm"
|
|
>
|
|
<option value="data_breach">Datenpanne</option>
|
|
<option value="unauthorized_access">Unbefugter Zugriff</option>
|
|
<option value="data_loss">Datenverlust</option>
|
|
<option value="system_compromise">Systemkompromittierung</option>
|
|
<option value="phishing">Phishing</option>
|
|
<option value="ransomware">Ransomware</option>
|
|
<option value="insider_threat">Insider-Bedrohung</option>
|
|
<option value="physical_breach">Physischer Vorfall</option>
|
|
<option value="other">Sonstiges</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Severity */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Schweregrad</label>
|
|
<select
|
|
value={severity}
|
|
onChange={e => setSeverity(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm"
|
|
>
|
|
<option value="low">Niedrig (1)</option>
|
|
<option value="medium">Mittel (2)</option>
|
|
<option value="high">Hoch (3)</option>
|
|
<option value="critical">Kritisch (4)</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung</label>
|
|
<textarea
|
|
value={description}
|
|
onChange={e => setDescription(e.target.value)}
|
|
rows={3}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm"
|
|
placeholder="Detaillierte Beschreibung des Vorfalls"
|
|
/>
|
|
</div>
|
|
|
|
{/* Detected By */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Entdeckt von</label>
|
|
<input
|
|
type="text"
|
|
value={detectedBy}
|
|
onChange={e => setDetectedBy(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm"
|
|
placeholder="Name / Team / System"
|
|
/>
|
|
</div>
|
|
|
|
{/* Affected Systems */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Betroffene Systeme
|
|
<span className="ml-1 text-xs text-gray-400">(Kommagetrennt)</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={affectedSystems}
|
|
onChange={e => setAffectedSystems(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm"
|
|
placeholder="z.B. CRM, E-Mail-Server, Datenbank"
|
|
/>
|
|
</div>
|
|
|
|
{/* Estimated Affected Persons */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Geschaetzte betroffene Personen</label>
|
|
<input
|
|
type="number"
|
|
value={estimatedAffectedPersons}
|
|
onChange={e => setEstimatedAffectedPersons(e.target.value)}
|
|
min={0}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Buttons */}
|
|
<div className="flex items-center justify-end gap-3 mt-6 pt-4 border-t border-gray-200">
|
|
<button
|
|
onClick={onClose}
|
|
className="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={isSaving}
|
|
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
{isSaving && (
|
|
<svg className="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
</svg>
|
|
)}
|
|
Speichern
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|