payment-backoffice/app/dashboard/audits/AuditTableClient.tsx
2025-11-27 09:09:43 +01:00

215 lines
5.7 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import {
DataGrid,
GridColDef,
GridPaginationModel,
GridSortModel,
} from "@mui/x-data-grid";
import TextField from "@mui/material/TextField";
import { Box, debounce } from "@mui/material";
import useSWR from "swr";
import { getAudits } from "@/app/services/audits";
import {
AuditQueryResult,
AuditRow,
DEFAULT_PAGE_SIZE,
} from "./auditTransforms";
const FALLBACK_COLUMNS: GridColDef[] = [
{
field: "placeholder",
headerName: "Audit Data",
flex: 1,
sortable: false,
filterable: false,
},
];
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,
}));
};
interface AuditTableClientProps {
initialData: AuditQueryResult;
}
const ENTITY_PREFIX = "LIKE/";
const buildSortParam = (sortModel: GridSortModel) =>
sortModel.length && sortModel[0].field && sortModel[0].sort
? `${sortModel[0].field}:${sortModel[0].sort}`
: undefined;
const buildEntityParam = (entitySearch: string) =>
entitySearch.trim() ? `${ENTITY_PREFIX}${entitySearch.trim()}` : undefined;
export default function AuditTableClient({
initialData,
}: AuditTableClientProps) {
const [paginationModel, setPaginationModel] = useState<GridPaginationModel>({
page: initialData.pageIndex ?? 0,
pageSize: DEFAULT_PAGE_SIZE,
});
const [sortModel, setSortModel] = useState<GridSortModel>([]);
const [entitySearch, setEntitySearch] = useState<string>("");
const [entitySearchInput, setEntitySearchInput] = useState<string>("");
const [columns, setColumns] = useState<GridColDef[]>(
initialData.rows.length ? deriveColumns(initialData.rows) : FALLBACK_COLUMNS
);
const debouncedSetEntitySearch = useMemo(
() =>
debounce((value: string) => {
setEntitySearch(value);
setPaginationModel(prev => ({ ...prev, page: 0 }));
}, 500),
[]
);
useEffect(() => {
return () => {
debouncedSetEntitySearch.clear();
};
}, [debouncedSetEntitySearch]);
const sortParam = useMemo(() => buildSortParam(sortModel), [sortModel]);
const entityParam = useMemo(
() => buildEntityParam(entitySearch),
[entitySearch]
);
const { data, error, isLoading, isValidating } = useSWR(
[
"audits",
paginationModel.page,
paginationModel.pageSize,
sortParam ?? "",
entityParam ?? "",
],
() =>
getAudits({
limit: paginationModel.pageSize,
page: paginationModel.page + 1,
sort: sortParam,
entity: entityParam,
}),
{
keepPreviousData: true,
revalidateOnFocus: true,
revalidateOnReconnect: true,
fallbackData: initialData,
}
);
const rows = data?.rows ?? initialData.rows;
const rowCount = data?.total ?? initialData.total ?? rows.length;
const loading = isLoading || (isValidating && !rows.length);
const pageTitle = useMemo(
() =>
sortModel.length && sortModel[0].field
? `Audit Logs · sorted by ${toTitle(sortModel[0].field)}`
: "Audit Logs",
[sortModel]
);
useEffect(() => {
setColumns(prev =>
rows.length ? deriveColumns(rows) : prev.length ? prev : FALLBACK_COLUMNS
);
}, [rows]);
const handlePaginationChange = (model: GridPaginationModel) => {
setPaginationModel(model);
};
const handleSortModelChange = (model: GridSortModel) => {
setSortModel(model);
setPaginationModel(prev => ({ ...prev, page: 0 }));
};
const handleEntitySearchChange = (value: string) => {
setEntitySearchInput(value);
debouncedSetEntitySearch(value);
};
const errorMessage =
error instanceof Error
? error.message
: error
? "Failed to load audits"
: null;
return (
<div className="audits-page">
<Box sx={{ display: "flex", gap: 2, mt: 5 }}>
<TextField
label="Search by Entity"
variant="outlined"
size="small"
value={entitySearchInput}
onChange={e => handleEntitySearchChange(e.target.value)}
sx={{ width: 300, backgroundColor: "#f0f0f0" }}
/>
</Box>
<h1 className="page-title">{pageTitle}</h1>
{errorMessage && (
<div className="error-alert" role="alert">
{errorMessage}
</div>
)}
<div className="table-container">
<div className="scroll-wrapper">
<div
className="table-inner"
style={{ minWidth: `${columns.length * 200}px` }}
>
<DataGrid
rows={rows}
columns={columns.length ? columns : FALLBACK_COLUMNS}
loading={loading}
paginationMode="server"
sortingMode="server"
paginationModel={paginationModel}
onPaginationModelChange={handlePaginationChange}
rowCount={rowCount}
sortModel={sortModel}
onSortModelChange={handleSortModelChange}
pageSizeOptions={[10, 25, 50, 100]}
disableRowSelectionOnClick
sx={{
border: 0,
minHeight: 500,
"& .MuiDataGrid-cell": {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
},
}}
/>
</div>
</div>
</div>
</div>
);
}