'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 // ============================================================================= 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 [saved, setSaved] = useState(false) useEffect(() => { const data = loadFromStorage() setConfig(data.config) setIncidents(data.incidents) setTemplates(data.templates) setExercises(data.exercises) }, []) const handleSave = useCallback(() => { saveToStorage({ config, incidents, templates, exercises }) setSaved(true) setTimeout(() => setSaved(false), 2000) }, [config, incidents, templates, 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 (
{/* Tab Navigation */}
{/* Save Button */}
{/* Tab Content */} {activeTab === 'config' && ( )} {activeTab === 'incidents' && ( )} {activeTab === 'templates' && ( )} {activeTab === 'exercises' && ( )}
) } // ============================================================================= // TAB 1: Notfallplan-Konfiguration // ============================================================================= function ConfigTab({ config, setConfig, }: { config: NotfallplanConfig setConfig: React.Dispatch> }) { return (
{/* Meldewege */}

Meldewege (intern → Aufsichtsbehoerde)

Definieren Sie die interne Eskalationskette bei einer Datenpanne.

{config.meldewege.map((step, idx) => (
{step.order}
{ const updated = [...config.meldewege] updated[idx] = { ...updated[idx], role: e.target.value } setConfig(prev => ({ ...prev, meldewege: updated })) }} placeholder="Rolle" className="text-sm border rounded px-2 py-1" /> { const updated = [...config.meldewege] updated[idx] = { ...updated[idx], name: e.target.value } setConfig(prev => ({ ...prev, meldewege: updated })) }} placeholder="Name" className="text-sm border rounded px-2 py-1" /> { const updated = [...config.meldewege] updated[idx] = { ...updated[idx], action: e.target.value } setConfig(prev => ({ ...prev, meldewege: updated })) }} placeholder="Aktion" className="text-sm border rounded px-2 py-1" />
max. {step.maxHours}h
))}
{/* Zustaendigkeiten */}

Zustaendigkeiten

{config.zustaendigkeiten.map((z, idx) => ( ))}
Rolle Name E-Mail Telefon
{z.role} { const updated = [...config.zustaendigkeiten] updated[idx] = { ...updated[idx], name: e.target.value } setConfig(prev => ({ ...prev, zustaendigkeiten: updated })) }} placeholder="Name eingeben" className="w-full text-sm border rounded px-2 py-1" /> { const updated = [...config.zustaendigkeiten] updated[idx] = { ...updated[idx], email: e.target.value } setConfig(prev => ({ ...prev, zustaendigkeiten: updated })) }} placeholder="email@example.com" className="w-full text-sm border rounded px-2 py-1" /> { const updated = [...config.zustaendigkeiten] updated[idx] = { ...updated[idx], phone: e.target.value } setConfig(prev => ({ ...prev, zustaendigkeiten: updated })) }} placeholder="+49..." className="w-full text-sm border rounded px-2 py-1" />
{/* Aufsichtsbehoerde */}

Zustaendige Aufsichtsbehoerde

setConfig(prev => ({ ...prev, aufsichtsbehoerde: { ...prev.aufsichtsbehoerde, name: e.target.value }, }))} placeholder="z.B. LfD Niedersachsen" className="w-full text-sm border rounded px-3 py-2" />
setConfig(prev => ({ ...prev, aufsichtsbehoerde: { ...prev.aufsichtsbehoerde, state: e.target.value }, }))} placeholder="z.B. Niedersachsen" className="w-full text-sm border rounded px-3 py-2" />
setConfig(prev => ({ ...prev, aufsichtsbehoerde: { ...prev.aufsichtsbehoerde, email: e.target.value }, }))} placeholder="poststelle@lfd.niedersachsen.de" className="w-full text-sm border rounded px-3 py-2" />
setConfig(prev => ({ ...prev, aufsichtsbehoerde: { ...prev.aufsichtsbehoerde, phone: e.target.value }, }))} placeholder="+49..." className="w-full text-sm border rounded px-3 py-2" />
{/* Eskalationsstufen */}

Eskalationsstufen

{config.eskalationsstufen.map((stufe) => (
Stufe {stufe.level} {stufe.label}

Ausloeser: {stufe.triggerCondition}

    {stufe.actions.map((action, i) => (
  • {action}
  • ))}
))}
{/* Sofortmassnahmen-Checkliste */}

Sofortmassnahmen-Checkliste

Diese Massnahmen sind sofort bei Entdeckung einer Datenpanne durchzufuehren.

    {config.sofortmassnahmen.map((m, idx) => (
  • {m}
  • ))}
) } // ============================================================================= // TAB 2: Incident-Register // ============================================================================= function IncidentsTab({ incidents, setIncidents, showAdd, setShowAdd, }: { incidents: Incident[] setIncidents: React.Dispatch> showAdd: boolean setShowAdd: (v: boolean) => void }) { const [newIncident, setNewIncident] = useState>({ title: '', description: '', severity: 'medium', affectedDataCategories: [], estimatedAffectedPersons: 0, measures: [], art34Required: false, art34Justification: '', }) function addIncident() { if (!newIncident.title) return const incident: Incident = { id: `INC-${Date.now()}`, title: newIncident.title || '', description: newIncident.description || '', detectedAt: new Date().toISOString(), detectedBy: 'Admin', status: 'detected', severity: newIncident.severity as IncidentSeverity || 'medium', affectedDataCategories: newIncident.affectedDataCategories || [], estimatedAffectedPersons: newIncident.estimatedAffectedPersons || 0, measures: newIncident.measures || [], art34Required: newIncident.art34Required || false, art34Justification: newIncident.art34Justification || '', } setIncidents(prev => [incident, ...prev]) setShowAdd(false) setNewIncident({ title: '', description: '', severity: 'medium', affectedDataCategories: [], estimatedAffectedPersons: 0, measures: [], art34Required: false, art34Justification: '', }) } function updateStatus(id: string, status: IncidentStatus) { setIncidents(prev => prev.map(inc => inc.id === id ? { ...inc, status, ...(status === 'reported' ? { reportedToAuthorityAt: new Date().toISOString() } : {}), ...(status === 'closed' ? { closedAt: new Date().toISOString(), closedBy: 'Admin' } : {}), } : inc )) } return (

Incident-Register (Art. 33 Abs. 5)

Alle Datenpannen dokumentieren — auch nicht-meldepflichtige.

{/* Add Incident Form */} {showAdd && (

Neue Datenpanne erfassen

setNewIncident(prev => ({ ...prev, title: e.target.value }))} placeholder="Kurzbeschreibung der Datenpanne" className="w-full border rounded px-3 py-2 text-sm" />