From db8eb1787b97c9d8cee070723beaffb905993be5 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Fri, 31 Jul 2026 19:26:42 +0000 Subject: [PATCH] =?UTF-8?q?feat(cc-049):=20Activity=20Timeline=20=E2=80=94?= =?UTF-8?q?=20audit-log=20timeline=20with=20date=20grouping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/dashboard/activity/page.tsx | 120 ++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/app/dashboard/activity/page.tsx diff --git a/src/app/dashboard/activity/page.tsx b/src/app/dashboard/activity/page.tsx new file mode 100644 index 0000000..c2e78e5 --- /dev/null +++ b/src/app/dashboard/activity/page.tsx @@ -0,0 +1,120 @@ +'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(); + 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('/v1/audit-log?limit=200', { tenantId }), + enabled: Boolean(tenantId), + staleTime: 30_000, + refetchInterval: 60_000, + }); + + const groups = groupByDate(entries); + + return ( +
+
+

Timeline Activitate

+

+ Cronologia operațiunilor din workspace —{' '} + {isLoading ? '…' : `${entries.length} evenimente`} +

+
+ + {isLoading && ( +
+

Se încarcă activitatea…

+
+ )} + + {!isLoading && entries.length === 0 && ( +
+

Nicio activitate înregistrată

+

+ Timeline-ul se completează automat pe măsură ce sunt efectuate operațiuni. +

+
+ )} + + {groups.map(([date, dayEntries]) => ( +
+

+ {date} +

+
+ {dayEntries.map((entry) => { + const { icon, colorClass } = getActionMeta(entry.action); + return ( +
+ + {icon} + +
+
+

+ {formatAction(entry.action)} +

+

+ {entry.resource} +

+
+ + {new Date(entry.createdAt).toLocaleTimeString('ro-RO', { + hour: '2-digit', + minute: '2-digit', + })} + +
+
+ ); + })} +
+
+ ))} +
+ ); +}