ceo-web/src/app/dashboard/activity/page.tsx

120 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useQuery } from '@tanstack/react-query';
import { apiFetch, type AuditLogEntry } from '../../../lib/api';
import { useSession } from '../../../components/session-provider';
function getActionMeta(action: string): { icon: string; colorClass: string } {
if (action.includes('created')) return { icon: '+', colorClass: 'text-signal-ok' };
if (action.includes('deleted') || action.includes('removed'))
return { icon: '×', colorClass: 'text-signal-danger' };
if (
action.includes('updated') ||
action.includes('changed') ||
action.includes('selected') ||
action.includes('rebuilt')
)
return { icon: '~', colorClass: 'text-signal-warn' };
if (action.includes('auth') || action.includes('login'))
return { icon: '⚿', colorClass: 'text-bronze-deep' };
return { icon: '·', colorClass: 'text-ink-faint' };
}
function formatAction(action: string): string {
return action.replace(/\./g, ' · ').replace(/_/g, ' ');
}
function groupByDate(entries: AuditLogEntry[]): [string, AuditLogEntry[]][] {
const map = new Map<string, AuditLogEntry[]>();
for (const e of entries) {
const key = new Date(e.createdAt).toLocaleDateString('ro-RO', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
if (!map.has(key)) map.set(key, []);
map.get(key)!.push(e);
}
return Array.from(map.entries());
}
export default function ActivityTimelinePage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const { data: entries = [], isLoading } = useQuery({
queryKey: ['audit-log-timeline', tenantId],
queryFn: () => apiFetch<AuditLogEntry[]>('/v1/audit-log?limit=200', { tenantId }),
enabled: Boolean(tenantId),
staleTime: 30_000,
refetchInterval: 60_000,
});
const groups = groupByDate(entries);
return (
<div className="max-w-3xl space-y-6">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Timeline Activitate</h1>
<p className="mt-1 text-sm text-ink-faint">
Cronologia operațiunilor din workspace {' '}
{isLoading ? '…' : `${entries.length} evenimente`}
</p>
</div>
{isLoading && (
<div className="card p-8 text-center">
<p className="text-sm text-ink-faint">Se încarcă activitatea</p>
</div>
)}
{!isLoading && entries.length === 0 && (
<div className="card p-8 text-center">
<p className="text-sm font-medium text-ink">Nicio activitate înregistrată</p>
<p className="mt-1 text-xs text-ink-faint">
Timeline-ul se completează automat pe măsură ce sunt efectuate operațiuni.
</p>
</div>
)}
{groups.map(([date, dayEntries]) => (
<div key={date}>
<p className="mb-3 text-xs font-semibold uppercase tracking-wider text-ink-faint">
{date}
</p>
<div className="relative border-l-2 border-paper-sunken pl-5 space-y-2">
{dayEntries.map((entry) => {
const { icon, colorClass } = getActionMeta(entry.action);
return (
<div key={entry.id} className="relative">
<span
className={`absolute -left-[1.55rem] flex h-5 w-5 items-center justify-center rounded-full border border-paper-sunken bg-paper-raised text-[10px] font-bold ${colorClass}`}
>
{icon}
</span>
<div className="card flex items-center justify-between gap-4 px-4 py-3">
<div className="min-w-0 flex-1">
<p className={`text-sm font-medium capitalize ${colorClass}`}>
{formatAction(entry.action)}
</p>
<p className="mt-0.5 truncate font-mono text-[11px] text-ink-faint">
{entry.resource}
</p>
</div>
<span className="shrink-0 text-[11px] tabular-nums text-ink-line">
{new Date(entry.createdAt).toLocaleTimeString('ro-RO', {
hour: '2-digit',
minute: '2-digit',
})}
</span>
</div>
</div>
);
})}
</div>
</div>
))}
</div>
);
}