Files
breakpilot-compliance/admin-compliance/app/sdk/incidents/page.tsx
Sharang Parnerkar 375b34a0d8 refactor(admin): split consent-management, control-library, incidents, training pages
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>
2026-04-12 15:52:45 +02:00

374 lines
17 KiB
TypeScript

'use client'
import React, { useState, useEffect, useMemo, useCallback } from 'react'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import {
Incident,
IncidentSeverity,
IncidentStatus,
IncidentCategory,
IncidentStatistics,
getHoursUntil72hDeadline,
is72hDeadlineExpired
} from '@/lib/sdk/incidents/types'
import { fetchSDKIncidentList, createMockIncidents, createMockStatistics } from '@/lib/sdk/incidents/api'
import { TabNavigation, type Tab, type TabId } from './_components/TabNavigation'
import { StatCard } from './_components/StatCard'
import { FilterBar } from './_components/FilterBar'
import { IncidentCard } from './_components/IncidentCard'
import { IncidentCreateModal } from './_components/IncidentCreateModal'
import { IncidentDetailDrawer } from './_components/IncidentDetailDrawer'
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function IncidentsPage() {
const { state } = useSDK()
const [activeTab, setActiveTab] = useState<TabId>('overview')
const [incidents, setIncidents] = useState<Incident[]>([])
const [statistics, setStatistics] = useState<IncidentStatistics | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [showCreateModal, setShowCreateModal] = useState(false)
const [selectedIncident, setSelectedIncident] = useState<Incident | null>(null)
// Filters
const [selectedSeverity, setSelectedSeverity] = useState<IncidentSeverity | 'all'>('all')
const [selectedStatus, setSelectedStatus] = useState<IncidentStatus | 'all'>('all')
const [selectedCategory, setSelectedCategory] = useState<IncidentCategory | 'all'>('all')
// Load data (extracted as useCallback so it can be called from modals)
const loadData = useCallback(async () => {
setIsLoading(true)
try {
const { incidents: loadedIncidents, statistics: loadedStats } = await fetchSDKIncidentList()
setIncidents(loadedIncidents)
setStatistics(loadedStats)
} catch (error) {
console.error('Fehler beim Laden der Incident-Daten:', error)
// Fallback auf Mock-Daten
setIncidents(createMockIncidents())
setStatistics(createMockStatistics())
} finally {
setIsLoading(false)
}
}, [])
useEffect(() => {
loadData()
}, [loadData])
// Calculate tab counts
const tabCounts = useMemo(() => {
return {
active: incidents.filter(i =>
i.status === 'detected' || i.status === 'assessment' ||
i.status === 'containment' || i.status === 'remediation'
).length,
notification: incidents.filter(i =>
i.status === 'notification_required' || i.status === 'notification_sent' ||
(i.authorityNotification !== null && i.authorityNotification.status === 'pending')
).length,
closed: incidents.filter(i => i.status === 'closed').length,
deadlineExpired: incidents.filter(i => {
if (i.status === 'closed') return false
if (i.authorityNotification && (i.authorityNotification.status === 'submitted' || i.authorityNotification.status === 'acknowledged')) return false
if (i.riskAssessment && !i.riskAssessment.notificationRequired) return false
return is72hDeadlineExpired(i.detectedAt)
}).length,
deadlineApproaching: incidents.filter(i => {
if (i.status === 'closed') return false
if (i.authorityNotification && (i.authorityNotification.status === 'submitted' || i.authorityNotification.status === 'acknowledged')) return false
const hours = getHoursUntil72hDeadline(i.detectedAt)
return hours > 0 && hours <= 24
}).length
}
}, [incidents])
// Filter incidents based on active tab and filters
const filteredIncidents = useMemo(() => {
let filtered = [...incidents]
// Tab-based filtering
if (activeTab === 'active') {
filtered = filtered.filter(i =>
i.status === 'detected' || i.status === 'assessment' ||
i.status === 'containment' || i.status === 'remediation'
)
} else if (activeTab === 'notification') {
filtered = filtered.filter(i =>
i.status === 'notification_required' || i.status === 'notification_sent' ||
(i.authorityNotification !== null && i.authorityNotification.status === 'pending')
)
} else if (activeTab === 'closed') {
filtered = filtered.filter(i => i.status === 'closed')
}
// Severity filter
if (selectedSeverity !== 'all') {
filtered = filtered.filter(i => i.severity === selectedSeverity)
}
// Status filter
if (selectedStatus !== 'all') {
filtered = filtered.filter(i => i.status === selectedStatus)
}
// Category filter
if (selectedCategory !== 'all') {
filtered = filtered.filter(i => i.category === selectedCategory)
}
// Sort: most urgent first (overdue > deadline approaching > severity > detected time)
const severityOrder: Record<IncidentSeverity, number> = { critical: 0, high: 1, medium: 2, low: 3 }
return filtered.sort((a, b) => {
// Closed always at the end
if (a.status === 'closed' !== (b.status === 'closed')) return a.status === 'closed' ? 1 : -1
// Overdue first
const aExpired = is72hDeadlineExpired(a.detectedAt)
const bExpired = is72hDeadlineExpired(b.detectedAt)
if (aExpired !== bExpired) return aExpired ? -1 : 1
// Then by severity
if (severityOrder[a.severity] !== severityOrder[b.severity]) {
return severityOrder[a.severity] - severityOrder[b.severity]
}
// Then by deadline urgency
return getHoursUntil72hDeadline(a.detectedAt) - getHoursUntil72hDeadline(b.detectedAt)
})
}, [incidents, activeTab, selectedSeverity, selectedStatus, selectedCategory])
const tabs: Tab[] = [
{ id: 'overview', label: 'Uebersicht' },
{ id: 'active', label: 'Aktiv', count: tabCounts.active, countColor: 'bg-orange-100 text-orange-600' },
{ id: 'notification', label: 'Meldepflichtig', count: tabCounts.notification, countColor: 'bg-red-100 text-red-600' },
{ id: 'closed', label: 'Abgeschlossen', count: tabCounts.closed, countColor: 'bg-green-100 text-green-600' },
{ id: 'settings', label: 'Einstellungen' }
]
const stepInfo = STEP_EXPLANATIONS['incidents']
const clearFilters = () => {
setSelectedSeverity('all')
setSelectedStatus('all')
setSelectedCategory('all')
}
return (
<div className="space-y-6">
{/* Step Header */}
<StepHeader
stepId="incidents"
title={stepInfo.title}
description={stepInfo.description}
explanation={stepInfo.explanation}
tips={stepInfo.tips}
>
<button
onClick={() => setShowCreateModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 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="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Vorfall melden
</button>
</StepHeader>
{/* Tab Navigation */}
<TabNavigation
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
{/* Loading State */}
{isLoading ? (
<div className="flex items-center justify-center py-12">
<svg className="animate-spin w-8 h-8 text-purple-600" 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>
</div>
) : activeTab === 'settings' ? (
/* Settings Tab */
<div className="bg-white rounded-xl border border-gray-200 p-8 text-center">
<div className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Einstellungen</h3>
<p className="mt-2 text-gray-500">
Incident-Management-Einstellungen, Eskalationswege und Meldevorlagen
werden in einer spaeteren Version verfuegbar sein.
</p>
</div>
) : (
<>
{/* Statistics (Overview Tab) */}
{activeTab === 'overview' && statistics && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<StatCard
label="Gesamt Vorfaelle"
value={statistics.totalIncidents}
color="gray"
/>
<StatCard
label="Offene Vorfaelle"
value={statistics.openIncidents}
color="orange"
/>
<StatCard
label="Meldungen ausstehend"
value={statistics.notificationsPending}
color={statistics.notificationsPending > 0 ? 'red' : 'green'}
/>
<StatCard
label="Durchschn. Reaktionszeit"
value={`${statistics.averageResponseTimeHours}h`}
color="purple"
/>
</div>
)}
{/* Critical Alert: 72h deadline approaching or expired */}
{(tabCounts.deadlineExpired > 0 || tabCounts.deadlineApproaching > 0) && (
<div className="bg-red-50 border border-red-200 rounded-xl p-4 flex items-center gap-4">
<div className="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div className="flex-1">
<h4 className="font-medium text-red-800">
{tabCounts.deadlineExpired > 0
? `Achtung: ${tabCounts.deadlineExpired} ueberfaellige Meldung(en) - 72-Stunden-Frist ueberschritten!`
: `Warnung: ${tabCounts.deadlineApproaching} Meldung(en) mit ablaufender 72-Stunden-Frist`
}
</h4>
<p className="text-sm text-red-600">
{tabCounts.deadlineExpired > 0
? 'Die gesetzliche Meldefrist nach Art. 33 DSGVO ist abgelaufen. Handeln Sie umgehend, um Bussgelder zu vermeiden. Verspaetete Meldungen muessen begruendet werden.'
: 'Die 72-Stunden-Meldefrist nach Art. 33 DSGVO laeuft in Kuerze ab. Fuehren Sie eine Risikobewertung durch und entscheiden Sie ueber die Meldepflicht.'
}
</p>
</div>
<button
onClick={() => {
setActiveTab('active')
setSelectedStatus('all')
}}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm font-medium"
>
Anzeigen
</button>
</div>
)}
{/* Info Box (Overview Tab) */}
{activeTab === 'overview' && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h4 className="font-medium text-blue-800">Art. 33/34 DSGVO - 72-Stunden-Meldepflicht</h4>
<p className="text-sm text-blue-600 mt-1">
Nach Art. 33 DSGVO muessen Datenschutzverletzungen innerhalb von 72 Stunden
an die zustaendige Aufsichtsbehoerde gemeldet werden, sofern ein Risiko fuer
die Rechte und Freiheiten der betroffenen Personen besteht. Bei hohem Risiko
muessen gemaess Art. 34 DSGVO auch die betroffenen Personen benachrichtigt werden.
Alle Vorfaelle sind unabhaengig von der Meldepflicht zu dokumentieren (Art. 33 Abs. 5).
</p>
</div>
</div>
</div>
)}
{/* Filters */}
<FilterBar
selectedSeverity={selectedSeverity}
selectedStatus={selectedStatus}
selectedCategory={selectedCategory}
onSeverityChange={setSelectedSeverity}
onStatusChange={setSelectedStatus}
onCategoryChange={setSelectedCategory}
onClear={clearFilters}
/>
{/* Incidents List */}
<div className="space-y-4">
{filteredIncidents.map(incident => (
<IncidentCard
key={incident.id}
incident={incident}
onClick={() => setSelectedIncident(incident)}
/>
))}
</div>
{/* Empty State */}
{filteredIncidents.length === 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Keine Vorfaelle gefunden</h3>
<p className="mt-2 text-gray-500">
{selectedSeverity !== 'all' || selectedStatus !== 'all' || selectedCategory !== 'all'
? 'Passen Sie die Filter an oder'
: 'Es sind noch keine Vorfaelle erfasst worden.'
}
</p>
{(selectedSeverity !== 'all' || selectedStatus !== 'all' || selectedCategory !== 'all') ? (
<button
onClick={clearFilters}
className="mt-4 px-4 py-2 text-purple-600 hover:bg-purple-50 rounded-lg transition-colors"
>
Filter zuruecksetzen
</button>
) : (
<button
onClick={() => setShowCreateModal(true)}
className="mt-4 inline-flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 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="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Ersten Vorfall erfassen
</button>
)}
</div>
)}
</>
)}
{/* Create Modal */}
{showCreateModal && (
<IncidentCreateModal
onClose={() => setShowCreateModal(false)}
onSuccess={() => { setShowCreateModal(false); loadData() }}
/>
)}
{/* Detail Drawer */}
{selectedIncident && (
<IncidentDetailDrawer
incident={selectedIncident}
onClose={() => setSelectedIncident(null)}
onStatusChange={() => { setSelectedIncident(null); loadData() }}
onDeleted={() => { setSelectedIncident(null); loadData() }}
/>
)}
</div>
)
}