'use client' import React, { useState, useEffect } from 'react' import type { Incident, IncidentStatus, IncidentSeverity } from './types' import { INCIDENT_STATUS_LABELS, INCIDENT_STATUS_COLORS, SEVERITY_LABELS, SEVERITY_COLORS, } from './types' 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} ) } export function IncidentsTab({ incidents, setIncidents, showAdd, setShowAdd, onAdd, onStatusChange, onDelete, }: { incidents: Incident[] setIncidents: React.Dispatch> showAdd: boolean setShowAdd: (v: boolean) => void onAdd?: (incident: Partial) => Promise onStatusChange?: (id: string, status: IncidentStatus) => Promise onDelete?: (id: string) => Promise }) { const [newIncident, setNewIncident] = useState>({ title: '', description: '', severity: 'medium', affectedDataCategories: [], estimatedAffectedPersons: 0, measures: [], art34Required: false, art34Justification: '', }) async function addIncident() { if (!newIncident.title) return if (onAdd) { await onAdd(newIncident) } else { 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: '', }) } async function updateStatus(id: string, status: IncidentStatus) { if (onStatusChange) { await onStatusChange(id, status) } else { 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. setShowAdd(true)} className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-medium hover:bg-red-700" > + Datenpanne melden {/* Add Incident Form */} {showAdd && ( Neue Datenpanne erfassen Titel setNewIncident(prev => ({ ...prev, title: e.target.value }))} placeholder="Kurzbeschreibung der Datenpanne" className="w-full border rounded px-3 py-2 text-sm" /> Beschreibung setNewIncident(prev => ({ ...prev, description: e.target.value }))} placeholder="Detaillierte Beschreibung: Was ist passiert, wie wurde es entdeckt?" rows={3} className="w-full border rounded px-3 py-2 text-sm" /> Schweregrad setNewIncident(prev => ({ ...prev, severity: e.target.value as IncidentSeverity }))} className="w-full border rounded px-3 py-2 text-sm" > Niedrig Mittel Hoch Kritisch Geschaetzte Betroffene setNewIncident(prev => ({ ...prev, estimatedAffectedPersons: parseInt(e.target.value) || 0 }))} className="w-full border rounded px-3 py-2 text-sm" /> setNewIncident(prev => ({ ...prev, art34Required: e.target.checked }))} className="rounded" /> Hohes Risiko fuer Betroffene (Art. 34 Benachrichtigungspflicht) setShowAdd(false)} className="px-4 py-2 border rounded-lg text-sm"> Abbrechen Datenpanne erfassen )} {/* Incidents List */} {incidents.length === 0 ? ( Keine Datenpannen erfasst Dokumentieren Sie hier alle Datenpannen — auch solche, die nicht meldepflichtig sind (Art. 33 Abs. 5). ) : ( {incidents.map(incident => ( {incident.id} {INCIDENT_STATUS_LABELS[incident.status]} {SEVERITY_LABELS[incident.severity]} {incident.art34Required && ( Art. 34 )} {incident.title} {incident.status !== 'closed' && incident.status !== 'reported' && incident.status !== 'not_reportable' && ( )} {incident.description} Entdeckt: {new Date(incident.detectedAt).toLocaleString('de-DE')} Betroffene: ~{incident.estimatedAffectedPersons} {incident.reportedToAuthorityAt && ( Gemeldet: {new Date(incident.reportedToAuthorityAt).toLocaleString('de-DE')} )} {incident.status !== 'closed' && ( {incident.status === 'detected' && ( updateStatus(incident.id, 'classified')} className="text-xs px-3 py-1 bg-yellow-100 text-yellow-800 rounded hover:bg-yellow-200"> Klassifizieren )} {incident.status === 'classified' && ( updateStatus(incident.id, 'assessed')} className="text-xs px-3 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200"> Bewerten )} {incident.status === 'assessed' && ( <> updateStatus(incident.id, 'reported')} className="text-xs px-3 py-1 bg-green-100 text-green-800 rounded hover:bg-green-200"> Als gemeldet markieren updateStatus(incident.id, 'not_reportable')} className="text-xs px-3 py-1 bg-gray-100 text-gray-800 rounded hover:bg-gray-200"> Nicht meldepflichtig > )} {(incident.status === 'reported' || incident.status === 'not_reportable') && ( updateStatus(incident.id, 'closed')} className="text-xs px-3 py-1 bg-gray-100 text-gray-600 rounded hover:bg-gray-200"> Abschliessen )} {onDelete && ( { if (window.confirm('Incident loeschen?')) onDelete(incident.id) }} className="text-xs px-2 py-1 text-red-400 hover:text-red-600 hover:bg-red-50 rounded ml-auto" > Loeschen )} )} ))} )} ) }
Alle Datenpannen dokumentieren — auch nicht-meldepflichtige.
Keine Datenpannen erfasst
Dokumentieren Sie hier alle Datenpannen — auch solche, die nicht meldepflichtig sind (Art. 33 Abs. 5).
{incident.description}