'use client'; import { useEffect, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { formatDistanceToNow } from 'date-fns'; import { apiFetch } from '@/lib/api/client'; interface AuditRow { id: string; action: string; entityType: string; entityId: string | null; fieldChanged: string | null; oldValue: unknown; newValue: unknown; metadata: Record | null; createdAt: string; actor: { id: string; email: string; name: string | null } | null; } const ACTION_LABELS: Record = { create: 'Created', update: 'Updated', delete: 'Deleted', archive: 'Archived', restore: 'Restored', merge: 'Merged', revert: 'Reverted', }; function formatAction(action: string): string { return ACTION_LABELS[action] ?? action.charAt(0).toUpperCase() + action.slice(1); } function formatField(field: string | null): string | null { if (!field) return null; return field.replace(/_/g, ' '); } function summarize(row: AuditRow): string { const verb = formatAction(row.action); const field = formatField(row.fieldChanged); if (field) return `${verb} ${field}`; return verb; } interface Props { endpoint: string; // e.g. /api/v1/clients/{id}/activity emptyText?: string; } export function EntityActivityFeed({ endpoint, emptyText = 'No activity yet.' }: Props) { const [now, setNow] = useState(() => Date.now()); // Re-render every minute so "x minutes ago" stays accurate without a // refetch. useEffect(() => { const t = setInterval(() => setNow(Date.now()), 60_000); return () => clearInterval(t); }, []); const { data, isLoading, error } = useQuery({ queryKey: ['entity-activity', endpoint], queryFn: () => apiFetch<{ data: AuditRow[] }>(endpoint), staleTime: 30_000, }); if (isLoading) { return
Loading activity…
; } if (error) { return (
Failed to load activity: {error instanceof Error ? error.message : 'unknown error'}
); } const rows = data?.data ?? []; if (rows.length === 0) { return
{emptyText}
; } return (
    {rows.map((row) => { const created = new Date(row.createdAt); const ago = formatDistanceToNow(created, { addSuffix: true }); // touch `now` so the closure participates in the minute re-render void now; const actor = row.actor?.name || row.actor?.email || 'System'; return (
  1. {summarize(row)} · {actor}
    {ago}
    {row.fieldChanged && (row.oldValue !== null || row.newValue !== null) ? (
    {row.oldValue !== null && row.oldValue !== undefined ? ( {String(JSON.stringify(row.oldValue)).slice(0, 80)} ) : null} {row.newValue !== null && row.newValue !== undefined ? ( → {String(JSON.stringify(row.newValue)).slice(0, 80)} ) : null}
    ) : null}
  2. ); })}
); }