"use client"; import { useEffect, useMemo, useState } from "react"; import { DataGrid, GridColDef, GridPaginationModel, GridSortModel, } from "@mui/x-data-grid"; import { getAudits } from "@/app/services/audits"; import "./page.scss"; import TextField from "@mui/material/TextField"; import { Box, debounce } from "@mui/material"; type AuditRow = Record & { id: string | number }; interface AuditApiResponse { total?: number; limit?: number; page?: number; data?: unknown; items?: unknown[]; audits?: unknown[]; logs?: unknown[]; results?: unknown[]; records?: unknown[]; meta?: { total?: number }; pagination?: { total?: number }; } const DEFAULT_PAGE_SIZE = 25; const FALLBACK_COLUMNS: GridColDef[] = [ { field: "placeholder", headerName: "Audit Data", flex: 1, sortable: false, filterable: false, }, ]; const CANDIDATE_ARRAY_KEYS: (keyof AuditApiResponse)[] = [ "items", "audits", "logs", "results", "records", ]; const normalizeValue = (value: unknown): string | number => { if (value === null || value === undefined) { return ""; } if (typeof value === "string" || typeof value === "number") { return value; } if (typeof value === "boolean") { return value ? "true" : "false"; } return JSON.stringify(value); }; const toTitle = (field: string) => field .replace(/_/g, " ") .replace(/-/g, " ") .replace(/([a-z])([A-Z])/g, "$1 $2") .replace(/\s+/g, " ") .trim() .replace(/^\w/g, char => char.toUpperCase()); const deriveColumns = (rows: AuditRow[]): GridColDef[] => { if (!rows.length) return []; return Object.keys(rows[0]).map(field => ({ field, headerName: toTitle(field), flex: field === "id" ? 0 : 1, minWidth: field === "id" ? 140 : 200, sortable: true, })); }; const extractArray = (payload: AuditApiResponse): unknown[] => { if (Array.isArray(payload)) { return payload; } for (const key of CANDIDATE_ARRAY_KEYS) { const candidate = payload[key]; if (Array.isArray(candidate)) { return candidate; } } const dataRecord = payload.data && typeof payload.data === "object" && !Array.isArray(payload.data) ? (payload.data as Record) : null; if (dataRecord) { for (const key of CANDIDATE_ARRAY_KEYS) { const candidate = dataRecord[key]; if (Array.isArray(candidate)) { return candidate; } } } if (Array.isArray(payload.data)) { return payload.data; } return []; }; const resolveTotal = (payload: AuditApiResponse, fallback: number): number => { const fromPayload = payload.total; const fromMeta = payload.meta?.total; const fromPagination = payload.pagination?.total; const fromData = payload.data && typeof payload.data === "object" && !Array.isArray(payload.data) ? (payload.data as { total?: number }).total : undefined; return ( (typeof fromPayload === "number" && fromPayload) || (typeof fromMeta === "number" && fromMeta) || (typeof fromPagination === "number" && fromPagination) || (typeof fromData === "number" && fromData) || fallback ); }; const normalizeRows = (entries: unknown[], page: number): AuditRow[] => entries.map((entry, index) => { if (!entry || typeof entry !== "object" || Array.isArray(entry)) { return { id: `${page}-${index}`, value: normalizeValue(entry), }; } const record = entry as Record; const normalized: Record = {}; Object.entries(record).forEach(([key, value]) => { normalized[key] = normalizeValue(value); }); const identifier = record.id ?? record.audit_id ?? record.log_id ?? record._id ?? `${page}-${index}`; return { id: (identifier as string | number) ?? `${page}-${index}`, ...normalized, }; }); export default function AuditPage() { const [rows, setRows] = useState([]); const [columns, setColumns] = useState([]); const [rowCount, setRowCount] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [paginationModel, setPaginationModel] = useState({ page: 0, pageSize: DEFAULT_PAGE_SIZE, }); const [sortModel, setSortModel] = useState([]); const [entitySearch, setEntitySearch] = useState(""); const [entitySearchInput, setEntitySearchInput] = useState(""); useEffect(() => { const controller = new AbortController(); const fetchAudits = async () => { setLoading(true); setError(null); const sortParam = sortModel.length && sortModel[0].field && sortModel[0].sort ? `${sortModel[0].field}:${sortModel[0].sort}` : undefined; const entityParam = entitySearch.trim() ? `LIKE/${entitySearch.trim()}` : undefined; try { const payload = (await getAudits({ limit: paginationModel.pageSize, page: paginationModel.page + 1, sort: sortParam, entity: entityParam, signal: controller.signal, })) as AuditApiResponse; const auditEntries = extractArray(payload); const normalized = normalizeRows(auditEntries, paginationModel.page); setColumns(prev => normalized.length ? deriveColumns(normalized) : prev.length ? prev : FALLBACK_COLUMNS ); setRows(normalized); setRowCount(resolveTotal(payload, normalized.length)); } catch (err) { if (controller.signal.aborted) return; const message = err instanceof Error ? err.message : "Failed to load audits"; setError(message); setRows([]); setColumns(prev => (prev.length ? prev : FALLBACK_COLUMNS)); } finally { if (!controller.signal.aborted) { setLoading(false); } } }; fetchAudits(); return () => controller.abort(); }, [paginationModel, sortModel, entitySearch]); const handlePaginationChange = (model: GridPaginationModel) => { setPaginationModel(model); }; const handleSortModelChange = (model: GridSortModel) => { setSortModel(model); setPaginationModel(prev => ({ ...prev, page: 0 })); }; const debouncedSetEntitySearch = useMemo( () => debounce((value: string) => { setEntitySearch(value); setPaginationModel(prev => ({ ...prev, page: 0 })); }, 500), [] ); useEffect(() => { return () => { debouncedSetEntitySearch.clear(); }; }, [debouncedSetEntitySearch]); const handleEntitySearchChange = (value: string) => { setEntitySearchInput(value); debouncedSetEntitySearch(value); }; const pageTitle = useMemo( () => sortModel.length && sortModel[0].field ? `Audit Logs ยท sorted by ${toTitle(sortModel[0].field)}` : "Audit Logs", [sortModel] ); return (
handleEntitySearchChange(e.target.value)} sx={{ width: 300, backgroundColor: "#f0f0f0" }} />

{pageTitle}

{error && (
{error}
)}
); }