'use client' /** * Notfallplan & Breach Response (Art. 33/34 DSGVO) * * 4 Tabs: * 1. Notfallplan-Konfiguration (Meldewege, Zustaendigkeiten, Eskalation) * 2. Incident-Register (Art. 33 Abs. 5) * 3. Melde-Templates (Art. 33 + Art. 34) * 4. Uebungen & Tests */ import { useState, useEffect, useCallback } from 'react' import { useSDK } from '@/lib/sdk' import StepHeader from '@/components/sdk/StepHeader/StepHeader' // ============================================================================= // TYPES // ============================================================================= type Tab = 'config' | 'incidents' | 'templates' | 'exercises' type IncidentStatus = 'detected' | 'classified' | 'assessed' | 'reported' | 'not_reportable' | 'closed' type IncidentSeverity = 'low' | 'medium' | 'high' | 'critical' interface NotfallplanConfig { meldewege: MeldeStep[] zustaendigkeiten: Zustaendigkeit[] aufsichtsbehoerde: { name: string state: string email: string phone: string url: string } eskalationsstufen: Eskalationsstufe[] sofortmassnahmen: string[] } interface MeldeStep { id: string order: number role: string name: string action: string maxHours: number } interface Zustaendigkeit { id: string role: string name: string email: string phone: string } interface Eskalationsstufe { id: string level: number label: string triggerCondition: string actions: string[] } interface Incident { id: string title: string description: string detectedAt: string detectedBy: string status: IncidentStatus severity: IncidentSeverity affectedDataCategories: string[] estimatedAffectedPersons: number measures: string[] art34Required: boolean art34Justification: string reportedToAuthorityAt?: string notifiedAffectedAt?: string closedAt?: string closedBy?: string lessonsLearned?: string } interface MeldeTemplate { id: string type: 'art33' | 'art34' title: string content: string } interface Exercise { id: string title: string type: 'tabletop' | 'simulation' | 'full_drill' scenario: string scheduledDate: string completedDate?: string participants: string[] lessonsLearned: string nextExerciseDate?: string } // ============================================================================= // INITIAL DATA // ============================================================================= const DEFAULT_CONFIG: NotfallplanConfig = { meldewege: [ { id: '1', order: 1, role: 'Entdecker', name: '', action: 'Incident melden an IT-Sicherheit', maxHours: 0 }, { id: '2', order: 2, role: 'IT-Sicherheit', name: '', action: 'Erstbewertung und Klassifizierung', maxHours: 4 }, { id: '3', order: 3, role: 'Datenschutzbeauftragter', name: '', action: 'Meldepflicht pruefen (Art. 33)', maxHours: 12 }, { id: '4', order: 4, role: 'Geschaeftsfuehrung', name: '', action: 'Freigabe der Meldung', maxHours: 24 }, { id: '5', order: 5, role: 'DSB', name: '', action: 'Meldung an Aufsichtsbehoerde', maxHours: 72 }, ], zustaendigkeiten: [ { id: '1', role: 'Incident Owner', name: '', email: '', phone: '' }, { id: '2', role: 'Datenschutzbeauftragter', name: '', email: '', phone: '' }, { id: '3', role: 'IT-Sicherheit', name: '', email: '', phone: '' }, { id: '4', role: 'Recht / Legal', name: '', email: '', phone: '' }, { id: '5', role: 'Kommunikation / PR', name: '', email: '', phone: '' }, ], aufsichtsbehoerde: { name: '', state: '', email: '', phone: '', url: '', }, eskalationsstufen: [ { id: '1', level: 1, label: 'Standard', triggerCondition: 'Einzelfall, keine sensiblen Daten', actions: ['IT-Sicherheit informieren', 'DSB benachrichtigen'] }, { id: '2', level: 2, label: 'Erhoehte Prioritaet', triggerCondition: 'Sensible Daten oder > 100 Betroffene', actions: ['Geschaeftsfuehrung informieren', 'Notfallteam einberufen'] }, { id: '3', level: 3, label: 'Kritisch', triggerCondition: 'Besondere Kategorien Art. 9 oder > 1000 Betroffene', actions: ['Sofortige Krisensitzung', 'Art. 34 Benachrichtigung vorbereiten', 'PR/Kommunikation einbinden'] }, ], sofortmassnahmen: [ 'Betroffene Systeme isolieren / vom Netz nehmen', 'Zugriffsrechte pruefen und ggf. sperren', 'Beweise sichern (Logs, Screenshots)', 'Zeitleiste der Ereignisse dokumentieren', 'Erstbewertung: Art der Daten, Anzahl Betroffene, Schaden', 'DSB unverzueglich informieren', ], } const DEFAULT_TEMPLATES: MeldeTemplate[] = [ { id: 'tpl-art33', type: 'art33', title: 'Meldung an Aufsichtsbehoerde (Art. 33 DSGVO)', content: `Meldung einer Verletzung des Schutzes personenbezogener Daten gemaess Art. 33 DSGVO 1. Beschreibung der Verletzung: [Art der Verletzung, betroffene Kategorien personenbezogener Daten] 2. Kategorien und ungefaehre Zahl der betroffenen Personen: [Anzahl und Kategorien der Betroffenen] 3. Kategorien und ungefaehre Zahl der betroffenen Datensaetze: [Datensatz-Kategorien und Anzahl] 4. Name und Kontaktdaten des Datenschutzbeauftragten: [Name, E-Mail, Telefon] 5. Beschreibung der wahrscheinlichen Folgen: [Moegliche Auswirkungen auf die Betroffenen] 6. Beschreibung der ergriffenen/vorgeschlagenen Massnahmen: [Sofortmassnahmen und geplante Abhilfemassnahmen] 7. Zeitpunkt der Feststellung: [Datum, Uhrzeit] 8. Begruendung bei verspaeteter Meldung (falls > 72h): [Begruendung]`, }, { id: 'tpl-art34', type: 'art34', title: 'Benachrichtigung Betroffene (Art. 34 DSGVO)', content: `Benachrichtigung ueber eine Verletzung des Schutzes Ihrer personenbezogenen Daten Sehr geehrte/r [Name/Betroffene], wir informieren Sie gemaess Art. 34 DSGVO ueber eine Verletzung des Schutzes personenbezogener Daten, die Ihre Daten betrifft. Was ist passiert? [Beschreibung in klarer, einfacher Sprache] Welche Daten sind betroffen? [Betroffene Datenkategorien] Welche Folgen sind moeglich? [Moegliche Auswirkungen] Was haben wir unternommen? [Ergriffene Massnahmen] Was koennen Sie tun? [Empfehlungen fuer Betroffene, z.B. Passwort aendern] Kontakt: Unser Datenschutzbeauftragter steht Ihnen fuer Rueckfragen zur Verfuegung: [Name, E-Mail, Telefon] Mit freundlichen Gruessen [Verantwortlicher]`, }, ] const INCIDENT_STATUS_LABELS: Record = { detected: 'Erkannt', classified: 'Klassifiziert', assessed: 'Bewertet', reported: 'Gemeldet', not_reportable: 'Nicht meldepflichtig', closed: 'Abgeschlossen', } const INCIDENT_STATUS_COLORS: Record = { detected: 'bg-red-100 text-red-800', classified: 'bg-yellow-100 text-yellow-800', assessed: 'bg-blue-100 text-blue-800', reported: 'bg-green-100 text-green-800', not_reportable: 'bg-gray-100 text-gray-800', closed: 'bg-gray-100 text-gray-600', } const SEVERITY_LABELS: Record = { low: 'Niedrig', medium: 'Mittel', high: 'Hoch', critical: 'Kritisch', } const SEVERITY_COLORS: Record = { low: 'bg-green-100 text-green-800', medium: 'bg-yellow-100 text-yellow-800', high: 'bg-orange-100 text-orange-800', critical: 'bg-red-100 text-red-800', } // ============================================================================= // LOCAL STORAGE HELPERS // ============================================================================= const STORAGE_KEY = 'bp_notfallplan' function loadFromStorage(): { config: NotfallplanConfig incidents: Incident[] templates: MeldeTemplate[] exercises: Exercise[] } { if (typeof window === 'undefined') { return { config: DEFAULT_CONFIG, incidents: [], templates: DEFAULT_TEMPLATES, exercises: [] } } try { const stored = localStorage.getItem(STORAGE_KEY) if (stored) { return JSON.parse(stored) } } catch { // ignore } return { config: DEFAULT_CONFIG, incidents: [], templates: DEFAULT_TEMPLATES, exercises: [] } } function saveToStorage(data: { config: NotfallplanConfig incidents: Incident[] templates: MeldeTemplate[] exercises: Exercise[] }) { if (typeof window === 'undefined') return localStorage.setItem(STORAGE_KEY, JSON.stringify(data)) } // ============================================================================= // COMPONENT: 72h Countdown // ============================================================================= function CountdownTimer({ detectedAt }: { detectedAt: string }) { const [remaining, setRemaining] = useState('') const [overdue, setOverdue] = useState(false) useEffect(() => { function update() { const detected = new Date(detectedAt).getTime() const deadline = detected + 72 * 60 * 60 * 1000 const now = Date.now() const diff = deadline - now if (diff <= 0) { const overdueMs = Math.abs(diff) const hours = Math.floor(overdueMs / (1000 * 60 * 60)) const mins = Math.floor((overdueMs % (1000 * 60 * 60)) / (1000 * 60)) setRemaining(`${hours}h ${mins}min ueberfaellig`) setOverdue(true) } else { const hours = Math.floor(diff / (1000 * 60 * 60)) const mins = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) setRemaining(`${hours}h ${mins}min verbleibend`) setOverdue(false) } } update() const interval = setInterval(update, 60000) return () => clearInterval(interval) }, [detectedAt]) return ( 72h-Frist: {remaining} ) } // ============================================================================= // 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('config') const [config, setConfig] = useState(DEFAULT_CONFIG) const [incidents, setIncidents] = useState([]) const [templates, setTemplates] = useState(DEFAULT_TEMPLATES) const [exercises, setExercises] = useState([]) const [showAddIncident, setShowAddIncident] = useState(false) const [showAddExercise, setShowAddExercise] = useState(false) const [showExerciseModal, setShowExerciseModal] = useState(false) const [saved, setSaved] = useState(false) // API-backed state const [apiContacts, setApiContacts] = useState([]) const [apiScenarios, setApiScenarios] = useState([]) const [apiExercises, setApiExercises] = useState([]) 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 [newExercise, setNewExercise] = useState({ title: '', exercise_type: 'tabletop', exercise_date: '', notes: '' }) const [savingContact, setSavingContact] = useState(false) const [savingScenario, setSavingScenario] = useState(false) const [savingExercise, setSavingExercise] = useState(false) useEffect(() => { // Only load config and exercises from localStorage (incidents/templates use API) const data = loadFromStorage() setConfig(data.config) setExercises(data.exercises) }, []) useEffect(() => { loadApiData() }, []) async function loadApiData() { setApiLoading(true) try { const [contactsRes, scenariosRes, exercisesRes, incidentsRes, templatesRes] = await Promise.all([ fetch(`${NOTFALLPLAN_API}/contacts`), fetch(`${NOTFALLPLAN_API}/scenarios`), fetch(`${NOTFALLPLAN_API}/exercises`), fetch(`${NOTFALLPLAN_API}/incidents`), fetch(`${NOTFALLPLAN_API}/templates`), ]) 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 : []) } if (incidentsRes.ok) { const data = await incidentsRes.json() setIncidents(Array.isArray(data) ? data.map(apiToIncident) : []) } if (templatesRes.ok) { const data = await templatesRes.json() setTemplates(Array.isArray(data) && data.length > 0 ? data : DEFAULT_TEMPLATES) } } catch (err) { console.error('Failed to load Notfallplan API data:', err) } finally { setApiLoading(false) } } function apiToIncident(raw: any): Incident { return { id: raw.id, title: raw.title, description: raw.description || '', detectedAt: raw.detected_at, detectedBy: raw.detected_by || 'Admin', status: raw.status, severity: raw.severity, affectedDataCategories: raw.affected_data_categories || [], estimatedAffectedPersons: raw.estimated_affected_persons || 0, measures: raw.measures || [], art34Required: raw.art34_required || false, art34Justification: raw.art34_justification || '', reportedToAuthorityAt: raw.reported_to_authority_at, notifiedAffectedAt: raw.notified_affected_at, closedAt: raw.closed_at, closedBy: raw.closed_by, lessonsLearned: raw.lessons_learned, } } async function apiAddIncident(newIncident: Partial) { try { const res = await fetch(`${NOTFALLPLAN_API}/incidents`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: newIncident.title, description: newIncident.description, severity: newIncident.severity || 'medium', estimated_affected_persons: newIncident.estimatedAffectedPersons || 0, art34_required: newIncident.art34Required || false, art34_justification: newIncident.art34Justification || '', }), }) if (res.ok) { const created = await res.json() setIncidents(prev => [apiToIncident(created), ...prev]) } } catch (err) { console.error('Failed to create incident:', err) } } async function apiUpdateIncidentStatus(id: string, status: IncidentStatus) { try { const body: any = { status } if (status === 'reported') body.reported_to_authority_at = new Date().toISOString() if (status === 'closed') { body.closed_at = new Date().toISOString(); body.closed_by = 'Admin' } const res = await fetch(`${NOTFALLPLAN_API}/incidents/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (res.ok) { const updated = await res.json() setIncidents(prev => prev.map(inc => inc.id === id ? apiToIncident(updated) : inc)) } } catch (err) { console.error('Failed to update incident status:', err) } } async function apiDeleteIncident(id: string) { try { const res = await fetch(`${NOTFALLPLAN_API}/incidents/${id}`, { method: 'DELETE' }) if (res.ok || res.status === 204) { setIncidents(prev => prev.filter(inc => inc.id !== id)) } } catch (err) { console.error('Failed to delete incident:', err) } } async function apiSaveTemplate(template: MeldeTemplate) { try { const isNew = !template.id.startsWith('tpl-') const method = isNew || template.id.length < 10 ? 'POST' : 'PUT' const url = method === 'POST' ? `${NOTFALLPLAN_API}/templates` : `${NOTFALLPLAN_API}/templates/${template.id}` const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: template.type, title: template.title, content: template.content }), }) if (res.ok) { const saved = await res.json() setTemplates(prev => { const existing = prev.find(t => t.id === template.id) if (existing) return prev.map(t => t.id === template.id ? { ...t, ...saved } : t) return [...prev, saved] }) } } catch (err) { console.error('Failed to save template:', err) } } 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 handleCreateExercise() { if (!newExercise.title.trim()) return setSavingExercise(true) try { const res = await fetch(`${NOTFALLPLAN_API}/exercises`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: newExercise.title.trim(), exercise_type: newExercise.exercise_type, exercise_date: newExercise.exercise_date || null, notes: newExercise.notes.trim() || null, }), }) if (res.ok) { setShowExerciseModal(false) setNewExercise({ title: '', exercise_type: 'tabletop', exercise_date: '', notes: '' }) loadApiData() } } catch (e) { console.error('Uebung erstellen fehlgeschlagen:', e) } finally { setSavingExercise(false) } } async function handleDeleteExercise(id: string) { try { const res = await fetch(`${NOTFALLPLAN_API}/exercises/${id}`, { method: 'DELETE' }) if (res.ok) { setApiExercises(prev => prev.filter(e => e.id !== id)) } } catch (e) { console.error('Löschen fehlgeschlagen:', e) } } 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(() => { // Only config and exercises are persisted to localStorage; incidents/templates use API saveToStorage({ config, incidents: [], templates: DEFAULT_TEMPLATES, exercises }) setSaved(true) setTimeout(() => setSaved(false), 2000) }, [config, exercises]) const tabs: { id: Tab; label: string; count?: number }[] = [ { id: 'config', label: 'Notfallplan' }, { id: 'incidents', label: 'Incident-Register', count: incidents.filter(i => i.status !== 'closed').length || undefined }, { id: 'templates', label: 'Melde-Templates' }, { id: 'exercises', label: 'Uebungen & Tests', count: exercises.filter(e => !e.completedDate).length || undefined }, ] return (
{/* API Stats Banner */} {!apiLoading && (apiContacts.length > 0 || apiScenarios.length > 0) && (
Notfallplan-Datenbank: {apiContacts.length} Kontakte {apiScenarios.length} Szenarien {apiExercises.length} Uebungen
)} {/* Tab Navigation */}
{/* Save Button */}
{/* Tab Content */} {activeTab === 'config' && ( <> {/* API Contacts & Scenarios section */}
{/* Contacts */}

Notfallkontakte (Datenbank)

Zentral gespeicherte Kontakte fuer den Notfall

{apiLoading ? (
Lade Kontakte...
) : apiContacts.length === 0 ? (
Noch keine Kontakte hinterlegt
) : (
{apiContacts.map(contact => (
{contact.is_primary && Primaer} {contact.available_24h && 24/7}
{contact.name} {contact.role && ({contact.role})}
{contact.email} {contact.phone && `| ${contact.phone}`}
))}
)}
{/* Scenarios */}

Notfallszenarien (Datenbank)

Definierte Szenarien und Reaktionsplaene

{apiLoading ? (
Lade Szenarien...
) : apiScenarios.length === 0 ? (
Noch keine Szenarien definiert
) : (
{apiScenarios.map(scenario => (
{scenario.title}
{scenario.category && {scenario.category}} {scenario.severity}
{scenario.description &&

{scenario.description}

}
))}
)}
)} {activeTab === 'incidents' && ( )} {activeTab === 'templates' && ( )} {activeTab === 'exercises' && ( <> {/* API Exercises */} {(apiExercises.length > 0 || !apiLoading) && (

Uebungen (Datenbank)

{apiExercises.length} Eintraege
{apiExercises.length === 0 ? (
Noch keine Uebungen in der Datenbank
) : (
{apiExercises.map(ex => (
{ex.title}
{ex.exercise_type} {ex.exercise_date && {new Date(ex.exercise_date).toLocaleDateString('de-DE')}} {ex.outcome && {ex.outcome}}
))}
)}
)} )} {/* Contact Create Modal */} {showContactModal && (

Notfallkontakt hinzufuegen

setNewContact(p => ({ ...p, name: e.target.value }))} placeholder="Vollstaendiger Name" className="w-full border rounded px-3 py-2 text-sm" />
setNewContact(p => ({ ...p, role: e.target.value }))} placeholder="z.B. Datenschutzbeauftragter" className="w-full border rounded px-3 py-2 text-sm" />
setNewContact(p => ({ ...p, email: e.target.value }))} placeholder="email@example.com" className="w-full border rounded px-3 py-2 text-sm" />
setNewContact(p => ({ ...p, phone: e.target.value }))} placeholder="+49..." className="w-full border rounded px-3 py-2 text-sm" />
)} {/* Exercise Create Modal */} {showExerciseModal && (

Neue Uebung planen

setNewExercise(p => ({ ...p, title: e.target.value }))} placeholder="z.B. Tabletop-Uebung Ransomware" className="w-full border rounded px-3 py-2 text-sm" />
setNewExercise(p => ({ ...p, exercise_date: e.target.value }))} className="w-full border rounded px-3 py-2 text-sm" />