'use client'; import { useState, useEffect, useCallback } from 'react'; import { type ColumnDef } from '@tanstack/react-table'; import { formatDistanceToNow } from 'date-fns'; import { Search } from 'lucide-react'; import { DataTable } from '@/components/shared/data-table'; import { PageHeader } from '@/components/shared/page-header'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { apiFetch } from '@/lib/api/client'; interface AuditEntry { id: string; userId: string | null; action: string; entityType: string; entityId: string; fieldChanged: string | null; oldValue: Record | null; newValue: Record | null; metadata: Record | null; ipAddress: string | null; createdAt: string; } const ACTION_COLORS: Record = { create: 'bg-green-600', update: 'bg-blue-500', delete: 'bg-red-600', archive: 'bg-orange-500', restore: 'bg-teal-500', login: 'bg-gray-500', permission_denied: 'bg-red-800', }; const ENTITY_TYPES = [ 'client', 'interest', 'berth', 'document', 'expense', 'invoice', 'reminder', 'user', 'role', 'port', 'setting', 'tag', 'webhook', ]; export function AuditLogList() { const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [entityTypeFilter, setEntityTypeFilter] = useState('all'); const [actionFilter, setActionFilter] = useState('all'); const [search, setSearch] = useState(''); const fetchLogs = useCallback(async () => { setLoading(true); try { const params = new URLSearchParams({ page: String(page), limit: '50', }); if (entityTypeFilter !== 'all') params.set('entityType', entityTypeFilter); if (actionFilter !== 'all') params.set('action', actionFilter); if (search) params.set('search', search); const res = await apiFetch<{ data: AuditEntry[]; pagination: { total: number }; }>(`/api/v1/admin/audit?${params}`); setEntries(res.data); setTotal(res.pagination.total); } finally { setLoading(false); } }, [page, entityTypeFilter, actionFilter, search]); useEffect(() => { void fetchLogs(); }, [fetchLogs]); const columns: ColumnDef[] = [ { accessorKey: 'createdAt', header: 'Time', cell: ({ row }) => (
{new Date(row.original.createdAt).toLocaleString()}
{formatDistanceToNow(new Date(row.original.createdAt), { addSuffix: true })}
), size: 180, }, { accessorKey: 'action', header: 'Action', cell: ({ row }) => ( {row.original.action} ), size: 100, }, { accessorKey: 'entityType', header: 'Entity', cell: ({ row }) => (
{row.original.entityType} {row.original.entityId.slice(0, 8)}...
), }, { id: 'changes', header: 'Changes', cell: ({ row }) => { const { newValue, fieldChanged } = row.original; if (fieldChanged) return {fieldChanged}; if (newValue) { const keys = Object.keys(newValue); return ( {keys.slice(0, 3).join(', ')} {keys.length > 3 ? ` +${keys.length - 3} more` : ''} ); } return ; }, }, { accessorKey: 'userId', header: 'User', cell: ({ row }) => ( {row.original.userId ? row.original.userId.slice(0, 8) + '...' : 'system'} ), size: 100, }, ]; return (
{ setSearch(e.target.value); setPage(1); }} />
row.id} emptyState={

No audit log entries found.

} /> {total > 50 && (
Page {page} of {Math.ceil(total / 50)}
)}
); }