'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' import { ConfigTab, IncidentsTab, TemplatesTab, ExercisesTab, ContactModal, ExerciseModal, ScenarioModal, ApiContactsSection, ApiScenariosSection, ApiExercisesSection, DEFAULT_CONFIG, DEFAULT_TEMPLATES, NOTFALLPLAN_API, loadFromStorage, saveToStorage, apiToIncident, } from './_components' import type { Tab, NotfallplanConfig, Incident, IncidentStatus, MeldeTemplate, Exercise, ApiContact, ApiScenario, ApiExercise, } from './_components' 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(() => { 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) } } async function apiAddIncident(newInc: Partial) { try { const res = await fetch(`${NOTFALLPLAN_API}/incidents`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: newInc.title, description: newInc.description, severity: newInc.severity || 'medium', estimated_affected_persons: newInc.estimatedAffectedPersons || 0, art34_required: newInc.art34Required || false, art34_justification: newInc.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: Record = { 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 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) } } 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('Loeschen fehlgeschlagen:', e) } } const handleSave = useCallback(() => { 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' && ( <> setShowContactModal(true)} onDelete={handleDeleteContact} /> setShowScenarioModal(true)} onDelete={handleDeleteScenario} /> )} {activeTab === 'incidents' && ( )} {activeTab === 'templates' && ( )} {activeTab === 'exercises' && ( <> setShowExerciseModal(true)} onDelete={handleDeleteExercise} /> )} {/* Modals */} {showContactModal && ( setShowContactModal(false)} onCreate={handleCreateContact} saving={savingContact} /> )} {showExerciseModal && ( setShowExerciseModal(false)} onCreate={handleCreateExercise} saving={savingExercise} /> )} {showScenarioModal && ( setShowScenarioModal(false)} onCreate={handleCreateScenario} saving={savingScenario} /> )}
) }