102 lines
4 KiB
TypeScript
102 lines
4 KiB
TypeScript
'use client';
|
|
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { apiFetch, type AuditLogEntry } from '../../../../lib/api';
|
|
import { useSession } from '../../../../components/session-provider';
|
|
|
|
function formatAction(action: string): string {
|
|
return action.replace(/\./g, ' · ').replace(/_/g, ' ');
|
|
}
|
|
|
|
function severityColor(action: string): string {
|
|
if (action.includes('deleted') || action.includes('removed')) return 'text-signal-danger';
|
|
if (action.includes('created') || action.includes('added')) return 'text-signal-ok';
|
|
if (action.includes('updated') || action.includes('changed') || action.includes('selected'))
|
|
return 'text-signal-warn';
|
|
return 'text-ink-faint';
|
|
}
|
|
|
|
export default function AuditLogPage() {
|
|
const { activeTenant } = useSession();
|
|
const tenantId = activeTenant?.tenantId ?? '';
|
|
|
|
const { data: entries = [], isLoading } = useQuery({
|
|
queryKey: ['audit-log', tenantId],
|
|
queryFn: () => apiFetch<AuditLogEntry[]>('/v1/audit-log?limit=200', { tenantId }),
|
|
enabled: Boolean(tenantId),
|
|
staleTime: 30_000,
|
|
});
|
|
|
|
return (
|
|
<div className="max-w-4xl">
|
|
<div className="mb-6">
|
|
<h1 className="font-display text-2xl font-semibold text-ink">Audit Log</h1>
|
|
<p className="text-sm text-ink-faint">
|
|
Toate operațiunile materiale din workspace — imutabil, doar citire.
|
|
{entries.length > 0 && ` · Ultimele ${entries.length} înregistrări.`}
|
|
</p>
|
|
</div>
|
|
|
|
{isLoading && <p className="text-sm text-ink-faint">Se încarcă…</p>}
|
|
|
|
{!isLoading && entries.length === 0 && (
|
|
<p className="card p-6 text-sm text-ink-faint">
|
|
Nicio înregistrare de audit. Jurnalul se completează automat cu fiecare operațiune.
|
|
</p>
|
|
)}
|
|
|
|
{entries.length > 0 && (
|
|
<div className="card overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-ink-line bg-paper-sunken text-left">
|
|
<th className="px-4 py-2.5 text-[11px] font-semibold uppercase tracking-wide text-ink-faint">
|
|
Data
|
|
</th>
|
|
<th className="px-4 py-2.5 text-[11px] font-semibold uppercase tracking-wide text-ink-faint">
|
|
Acțiune
|
|
</th>
|
|
<th className="px-4 py-2.5 text-[11px] font-semibold uppercase tracking-wide text-ink-faint">
|
|
Resursă
|
|
</th>
|
|
<th className="px-4 py-2.5 text-[11px] font-semibold uppercase tracking-wide text-ink-faint">
|
|
Actor
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{entries.map((entry, i) => (
|
|
<tr
|
|
key={entry.id}
|
|
className={`border-b border-ink-line last:border-0 ${
|
|
i % 2 === 0 ? '' : 'bg-paper-sunken/40'
|
|
}`}
|
|
>
|
|
<td className="whitespace-nowrap px-4 py-2.5 text-xs text-ink-faint">
|
|
{new Date(entry.createdAt).toLocaleString('ro-RO', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})}
|
|
</td>
|
|
<td className={`px-4 py-2.5 font-medium ${severityColor(entry.action)}`}>
|
|
{formatAction(entry.action)}
|
|
</td>
|
|
<td className="px-4 py-2.5 font-mono text-xs text-ink-soft">
|
|
{entry.resource}
|
|
</td>
|
|
<td className="px-4 py-2.5 font-mono text-xs text-ink-faint">
|
|
{entry.actorId ? entry.actorId.slice(0, 8) + '…' : entry.actorType}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|