Extract components and hooks into _components/ and _hooks/ subdirectories to reduce each page.tsx to under 500 LOC (was 1545/1383/1316). Final line counts: evidence=213, process-tasks=304, hazards=157. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
96 lines
4.7 KiB
TypeScript
96 lines
4.7 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
|
|
export function AuditTrailPanel({ evidenceId, onClose }: { evidenceId: string; onClose: () => void }) {
|
|
const [entries, setEntries] = useState<{ id: string; action: string; actor: string; timestamp: string; details: Record<string, unknown> | null }[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
fetch(`/api/sdk/v1/compliance/audit-trail?entity_type=evidence&entity_id=${evidenceId}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const mapped = (data.entries || []).map((e: Record<string, unknown>) => ({
|
|
id: e.id as string,
|
|
action: e.action as string,
|
|
actor: (e.performed_by || 'System') as string,
|
|
timestamp: (e.performed_at || '') as string,
|
|
details: {
|
|
...(e.field_changed ? { field: e.field_changed } : {}),
|
|
...(e.old_value ? { old: e.old_value } : {}),
|
|
...(e.new_value ? { new: e.new_value } : {}),
|
|
...(e.change_summary ? { summary: e.change_summary } : {}),
|
|
} as Record<string, unknown>,
|
|
}))
|
|
setEntries(mapped)
|
|
})
|
|
.catch(() => {})
|
|
.finally(() => setLoading(false))
|
|
}, [evidenceId])
|
|
|
|
const actionLabels: Record<string, { label: string; color: string }> = {
|
|
created: { label: 'Erstellt', color: 'bg-blue-100 text-blue-700' },
|
|
uploaded: { label: 'Hochgeladen', color: 'bg-purple-100 text-purple-700' },
|
|
reviewed: { label: 'Reviewed', color: 'bg-green-100 text-green-700' },
|
|
rejected: { label: 'Abgelehnt', color: 'bg-red-100 text-red-700' },
|
|
updated: { label: 'Aktualisiert', color: 'bg-yellow-100 text-yellow-700' },
|
|
deleted: { label: 'Geloescht', color: 'bg-gray-100 text-gray-700' },
|
|
approved: { label: 'Genehmigt', color: 'bg-emerald-100 text-emerald-700' },
|
|
four_eyes_first: { label: '1. Review (4-Augen)', color: 'bg-blue-100 text-blue-700' },
|
|
four_eyes_final: { label: 'Finale Freigabe (4-Augen)', color: 'bg-emerald-100 text-emerald-700' },
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-2xl mx-4 p-6 max-h-[80vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-xl font-bold text-gray-900">Audit-Trail</h2>
|
|
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-xl">×</button>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="flex justify-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
|
</div>
|
|
) : entries.length === 0 ? (
|
|
<div className="py-12 text-center text-gray-500">
|
|
<p>Keine Audit-Trail-Eintraege vorhanden.</p>
|
|
</div>
|
|
) : (
|
|
<div className="relative">
|
|
<div className="absolute left-4 top-0 bottom-0 w-0.5 bg-gray-200" />
|
|
<div className="space-y-4">
|
|
{entries.map((entry, idx) => {
|
|
const meta = actionLabels[entry.action] || { label: entry.action, color: 'bg-gray-100 text-gray-700' }
|
|
return (
|
|
<div key={entry.id || idx} className="relative flex items-start gap-4 pl-10">
|
|
<div className="absolute left-2.5 top-1.5 w-3 h-3 rounded-full bg-white border-2 border-purple-400" />
|
|
<div className="flex-1 bg-gray-50 rounded-lg p-3">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className={`px-2 py-0.5 text-xs rounded ${meta.color}`}>{meta.label}</span>
|
|
<span className="text-xs text-gray-400">
|
|
{entry.timestamp ? new Date(entry.timestamp).toLocaleString('de-DE') : '—'}
|
|
</span>
|
|
</div>
|
|
<div className="text-sm text-gray-600">
|
|
<span className="font-medium">{entry.actor || 'System'}</span>
|
|
</div>
|
|
{entry.details && Object.keys(entry.details).length > 0 && (
|
|
<div className="mt-2 text-xs text-gray-500 font-mono bg-white rounded p-2 border">
|
|
{Object.entries(entry.details).map(([k, v]) => (
|
|
<div key={k}><span className="text-gray-400">{k}:</span> {String(v)}</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|