Extract components and hooks from 4 oversized pages (518–508 LOC each) to bring each page.tsx under 300 LOC (hard cap 500). Zero behavior changes. - dsr/new: TypeSelector, SourceSelector → _components/; useNewDSRForm → _hooks/ - compliance-hub: QuickActions, StatsRow, DomainChart, MappingsAndFindings, RegulationsTable → _components/; useComplianceHub → _hooks/ - iace/[projectId]/monitoring: Badges, EventForm, ResolveModal, TimelineEvent → _components/; useMonitoring → _hooks/ - cookie-banner: BannerPreview, CategoryCard → _components/; useCookieBanner → _hooks/ Result: page.tsx LOC: dsr/new=259, compliance-hub=95, monitoring=157, cookie-banner=212 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
158 lines
6.5 KiB
TypeScript
158 lines
6.5 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import { useParams } from 'next/navigation'
|
|
import { useMonitoring } from './_hooks/useMonitoring'
|
|
import { EventForm } from './_components/EventForm'
|
|
import { ResolveModal } from './_components/ResolveModal'
|
|
import { TimelineEvent } from './_components/TimelineEvent'
|
|
|
|
export default function MonitoringPage() {
|
|
const params = useParams()
|
|
const projectId = params.projectId as string
|
|
|
|
const {
|
|
events, loading, showForm, resolvingEvent,
|
|
filterType, filterStatus, filteredEvents,
|
|
openCount, resolvedCount,
|
|
setShowForm, setResolvingEvent, setFilterType, setFilterStatus,
|
|
handleSubmit, handleResolve,
|
|
} = useMonitoring(projectId)
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Monitoring</h1>
|
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
Post-Market Surveillance -- Ueberwachung von Vorfaellen, Updates, Drift und Regulierungsaenderungen.
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowForm(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>
|
|
Ereignis erfassen
|
|
</button>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
{events.length > 0 && (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 text-center">
|
|
<div className="text-2xl font-bold text-gray-900 dark:text-white">{events.length}</div>
|
|
<div className="text-xs text-gray-500">Gesamt</div>
|
|
</div>
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-red-200 p-4 text-center">
|
|
<div className="text-2xl font-bold text-red-600">{openCount}</div>
|
|
<div className="text-xs text-red-600">Offen</div>
|
|
</div>
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-green-200 p-4 text-center">
|
|
<div className="text-2xl font-bold text-green-600">{resolvedCount}</div>
|
|
<div className="text-xs text-green-600">Geloest</div>
|
|
</div>
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-orange-200 p-4 text-center">
|
|
<div className="text-2xl font-bold text-orange-600">
|
|
{events.filter((e) => e.severity === 'critical' || e.severity === 'high').length}
|
|
</div>
|
|
<div className="text-xs text-orange-600">Hoch/Kritisch</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Filters */}
|
|
{events.length > 0 && (
|
|
<div className="flex items-center gap-3">
|
|
<select
|
|
value={filterType}
|
|
onChange={(e) => setFilterType(e.target.value)}
|
|
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
|
>
|
|
<option value="">Alle Typen</option>
|
|
<option value="incident">Vorfaelle</option>
|
|
<option value="update">Updates</option>
|
|
<option value="drift_alert">Drift-Warnungen</option>
|
|
<option value="regulation_change">Regulierungsaenderungen</option>
|
|
</select>
|
|
<select
|
|
value={filterStatus}
|
|
onChange={(e) => setFilterStatus(e.target.value)}
|
|
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
|
>
|
|
<option value="">Alle Status</option>
|
|
<option value="open">Offen</option>
|
|
<option value="investigating">In Untersuchung</option>
|
|
<option value="resolved">Geloest</option>
|
|
<option value="closed">Geschlossen</option>
|
|
</select>
|
|
<span className="text-sm text-gray-500">
|
|
{filteredEvents.length} Ereignisse
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Form */}
|
|
{showForm && (
|
|
<EventForm onSubmit={handleSubmit} onCancel={() => setShowForm(false)} />
|
|
)}
|
|
|
|
{/* Resolve Modal */}
|
|
{resolvingEvent && (
|
|
<ResolveModal
|
|
event={resolvingEvent}
|
|
onSubmit={handleResolve}
|
|
onClose={() => setResolvingEvent(null)}
|
|
/>
|
|
)}
|
|
|
|
{/* Timeline */}
|
|
{filteredEvents.length > 0 ? (
|
|
<div className="pl-1">
|
|
{filteredEvents
|
|
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
|
.map((event) => (
|
|
<TimelineEvent
|
|
key={event.id}
|
|
event={event}
|
|
onResolve={() => setResolvingEvent(event)}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : (
|
|
!showForm && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-12 text-center">
|
|
<div className="w-16 h-16 mx-auto bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center mb-4">
|
|
<svg className="w-8 h-8 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Keine Monitoring-Ereignisse</h3>
|
|
<p className="mt-2 text-gray-500 max-w-md mx-auto">
|
|
Erfassen Sie Vorfaelle, Software-Updates, KI-Drift-Warnungen und Regulierungsaenderungen
|
|
im Rahmen der Post-Market Surveillance.
|
|
</p>
|
|
<button
|
|
onClick={() => setShowForm(true)}
|
|
className="mt-6 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
|
>
|
|
Erstes Ereignis erfassen
|
|
</button>
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
)
|
|
}
|