Refactored more on data tables
This commit is contained in:
parent
2b6df3899b
commit
4f230fa208
@ -32,10 +32,32 @@ export async function POST(request: NextRequest) {
|
|||||||
queryParts.push(`sort=${sort.field}:${sort.order}`);
|
queryParts.push(`sort=${sort.field}:${sort.order}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track date ranges separately so we can emit BETWEEN/>/< syntax
|
||||||
|
const dateRanges: Record<string, { start?: string; end?: string }> = {};
|
||||||
|
|
||||||
// Process filters - convert FilterValue objects to operator/value format
|
// Process filters - convert FilterValue objects to operator/value format
|
||||||
for (const [key, filterValue] of Object.entries(filters)) {
|
for (const [key, filterValue] of Object.entries(filters)) {
|
||||||
if (!filterValue) continue;
|
if (!filterValue) continue;
|
||||||
|
|
||||||
|
// Handle date range helpers (e.g. Created_start / Created_end)
|
||||||
|
if (/_start$|_end$/.test(key)) {
|
||||||
|
const baseField = key.replace(/_(start|end)$/, "");
|
||||||
|
if (!dateRanges[baseField]) {
|
||||||
|
dateRanges[baseField] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetKey = key.endsWith("_start") ? "start" : "end";
|
||||||
|
const stringValue =
|
||||||
|
typeof filterValue === "string"
|
||||||
|
? filterValue
|
||||||
|
: (filterValue as { value?: string }).value;
|
||||||
|
|
||||||
|
if (stringValue) {
|
||||||
|
dateRanges[baseField][targetKey] = stringValue;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let op: string;
|
let op: string;
|
||||||
let value: string;
|
let value: string;
|
||||||
|
|
||||||
@ -57,6 +79,24 @@ export async function POST(request: NextRequest) {
|
|||||||
queryParts.push(`${key}=${op}/${encodedValue}`);
|
queryParts.push(`${key}=${op}/${encodedValue}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit date range filters using backend format
|
||||||
|
for (const [field, { start, end }] of Object.entries(dateRanges)) {
|
||||||
|
if (start && end) {
|
||||||
|
queryParts.push(
|
||||||
|
`${field}=BETWEEN/${encodeURIComponent(start)}/${encodeURIComponent(
|
||||||
|
end
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start) {
|
||||||
|
queryParts.push(`${field}=>/${encodeURIComponent(start)}`);
|
||||||
|
} else if (end) {
|
||||||
|
queryParts.push(`${field}=</${encodeURIComponent(end)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const queryString = queryParts.join("&");
|
const queryString = queryParts.join("&");
|
||||||
const backendUrl = `${BE_BASE_URL}/api/v1/transactions${queryString ? `?${queryString}` : ""}`;
|
const backendUrl = `${BE_BASE_URL}/api/v1/transactions${queryString ? `?${queryString}` : ""}`;
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import {
|
|||||||
} from "@mui/x-data-grid";
|
} from "@mui/x-data-grid";
|
||||||
import { getAudits } from "@/app/services/audits";
|
import { getAudits } from "@/app/services/audits";
|
||||||
import "./page.scss";
|
import "./page.scss";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import { Box, debounce } from "@mui/material";
|
||||||
|
|
||||||
type AuditRow = Record<string, unknown> & { id: string | number };
|
type AuditRow = Record<string, unknown> & { id: string | number };
|
||||||
|
|
||||||
@ -178,6 +180,8 @@ export default function AuditPage() {
|
|||||||
pageSize: DEFAULT_PAGE_SIZE,
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
});
|
});
|
||||||
const [sortModel, setSortModel] = useState<GridSortModel>([]);
|
const [sortModel, setSortModel] = useState<GridSortModel>([]);
|
||||||
|
const [entitySearch, setEntitySearch] = useState<string>("");
|
||||||
|
const [entitySearchInput, setEntitySearchInput] = useState<string>("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@ -191,11 +195,16 @@ export default function AuditPage() {
|
|||||||
? `${sortModel[0].field}:${sortModel[0].sort}`
|
? `${sortModel[0].field}:${sortModel[0].sort}`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const entityParam = entitySearch.trim()
|
||||||
|
? `LIKE/${entitySearch.trim()}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = (await getAudits({
|
const payload = (await getAudits({
|
||||||
limit: paginationModel.pageSize,
|
limit: paginationModel.pageSize,
|
||||||
page: paginationModel.page + 1,
|
page: paginationModel.page + 1,
|
||||||
sort: sortParam,
|
sort: sortParam,
|
||||||
|
entity: entityParam,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
})) as AuditApiResponse;
|
})) as AuditApiResponse;
|
||||||
|
|
||||||
@ -229,7 +238,7 @@ export default function AuditPage() {
|
|||||||
fetchAudits();
|
fetchAudits();
|
||||||
|
|
||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [paginationModel, sortModel]);
|
}, [paginationModel, sortModel, entitySearch]);
|
||||||
|
|
||||||
const handlePaginationChange = (model: GridPaginationModel) => {
|
const handlePaginationChange = (model: GridPaginationModel) => {
|
||||||
setPaginationModel(model);
|
setPaginationModel(model);
|
||||||
@ -240,6 +249,26 @@ export default function AuditPage() {
|
|||||||
setPaginationModel(prev => ({ ...prev, page: 0 }));
|
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(
|
const pageTitle = useMemo(
|
||||||
() =>
|
() =>
|
||||||
sortModel.length && sortModel[0].field
|
sortModel.length && sortModel[0].field
|
||||||
@ -250,6 +279,16 @@ export default function AuditPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="audits-page">
|
<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>
|
<h1 className="page-title">{pageTitle}</h1>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="error-alert" role="alert">
|
<div className="error-alert" role="alert">
|
||||||
|
|||||||
@ -18,11 +18,18 @@ export default function AllTransactionPage() {
|
|||||||
const pagination = useSelector(selectPagination);
|
const pagination = useSelector(selectPagination);
|
||||||
const sort = useSelector(selectSort);
|
const sort = useSelector(selectSort);
|
||||||
|
|
||||||
const [tableRows, setTableRows] = useState<TransactionRow[]>([]);
|
const [tableData, setTableData] = useState<{
|
||||||
|
transactions: TransactionRow[];
|
||||||
|
total: number;
|
||||||
|
}>({ transactions: [], total: 0 });
|
||||||
|
const [totalRows, setRowCount] = useState(0);
|
||||||
const extraColumns: string[] = []; // static for now
|
const extraColumns: string[] = []; // static for now
|
||||||
|
|
||||||
// Memoize rows to avoid new reference each render
|
// Memoize rows to avoid new reference each render
|
||||||
const memoizedRows = useMemo(() => tableRows, [tableRows]);
|
const memoizedRows = useMemo(
|
||||||
|
() => tableData.transactions,
|
||||||
|
[tableData.transactions]
|
||||||
|
);
|
||||||
|
|
||||||
// Fetch data when filters, pagination, or sort changes
|
// Fetch data when filters, pagination, or sort changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -37,12 +44,12 @@ export default function AllTransactionPage() {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
dispatch(setAdvancedSearchError("Failed to fetch transactions"));
|
dispatch(setAdvancedSearchError("Failed to fetch transactions"));
|
||||||
setTableRows([]);
|
setTableData({ transactions: [], total: 0 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const backendData = await response.json();
|
const data = await response.json();
|
||||||
const transactions = backendData.transactions || [];
|
const transactions = data.transactions || [];
|
||||||
|
|
||||||
const rows = transactions.map((tx: BackendTransaction) => ({
|
const rows = transactions.map((tx: BackendTransaction) => ({
|
||||||
id: tx.id || 0,
|
id: tx.id || 0,
|
||||||
@ -59,19 +66,25 @@ export default function AllTransactionPage() {
|
|||||||
modified: tx.modified,
|
modified: tx.modified,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setTableRows(rows);
|
setTableData({ transactions: rows, total: data?.total });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(
|
dispatch(
|
||||||
setAdvancedSearchError(
|
setAdvancedSearchError(
|
||||||
error instanceof Error ? error.message : "Unknown error"
|
error instanceof Error ? error.message : "Unknown error"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
setTableRows([]);
|
setTableData({ transactions: [], total: 0 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [dispatch, filters, pagination, sort]);
|
}, [dispatch, filters, pagination, sort]);
|
||||||
|
|
||||||
return <DataTable rows={memoizedRows} extraColumns={extraColumns} />;
|
return (
|
||||||
|
<DataTable
|
||||||
|
rows={memoizedRows}
|
||||||
|
extraColumns={extraColumns}
|
||||||
|
totalRows={tableData.total}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export default function DepositTransactionPage() {
|
|||||||
const pagination = useSelector(selectPagination);
|
const pagination = useSelector(selectPagination);
|
||||||
const sort = useSelector(selectSort);
|
const sort = useSelector(selectSort);
|
||||||
const [tableRows, setTableRows] = useState<TransactionRow[]>([]);
|
const [tableRows, setTableRows] = useState<TransactionRow[]>([]);
|
||||||
|
const [rowCount, setRowCount] = useState(0);
|
||||||
|
|
||||||
const memoizedRows = useMemo(() => tableRows, [tableRows]);
|
const memoizedRows = useMemo(() => tableRows, [tableRows]);
|
||||||
|
|
||||||
@ -75,6 +76,7 @@ export default function DepositTransactionPage() {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
setTableRows(rows);
|
setTableRows(rows);
|
||||||
|
setRowCount(100);
|
||||||
dispatch(setStatus("succeeded"));
|
dispatch(setStatus("succeeded"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -89,5 +91,7 @@ export default function DepositTransactionPage() {
|
|||||||
fetchDeposits();
|
fetchDeposits();
|
||||||
}, [dispatch, depositFilters, pagination, sort]);
|
}, [dispatch, depositFilters, pagination, sort]);
|
||||||
|
|
||||||
return <DataTable rows={memoizedRows} enableStatusActions />;
|
return (
|
||||||
|
<DataTable rows={memoizedRows} enableStatusActions totalRows={rowCount} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import {
|
|||||||
} from "@/app/redux/advanedSearch/advancedSearchSlice";
|
} from "@/app/redux/advanedSearch/advancedSearchSlice";
|
||||||
import { selectFilters } from "@/app/redux/advanedSearch/selectors";
|
import { selectFilters } from "@/app/redux/advanedSearch/selectors";
|
||||||
import { normalizeValue, defaultOperatorForField } from "./utils/utils";
|
import { normalizeValue, defaultOperatorForField } from "./utils/utils";
|
||||||
|
import { selectConditionOperators } from "@/app/redux/metadata/selectors";
|
||||||
|
|
||||||
// -----------------------------------------------------
|
// -----------------------------------------------------
|
||||||
// COMPONENT
|
// COMPONENT
|
||||||
@ -42,7 +43,9 @@ export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) {
|
|||||||
// Local form state for UI (synced with Redux)
|
// Local form state for UI (synced with Redux)
|
||||||
const [formValues, setFormValues] = useState<Record<string, string>>({});
|
const [formValues, setFormValues] = useState<Record<string, string>>({});
|
||||||
const [operators, setOperators] = useState<Record<string, string>>({});
|
const [operators, setOperators] = useState<Record<string, string>>({});
|
||||||
|
const conditionOperators = useSelector(selectConditionOperators);
|
||||||
|
|
||||||
|
console.log("[conditionOperators]", conditionOperators);
|
||||||
// -----------------------------------------------------
|
// -----------------------------------------------------
|
||||||
// SYNC REDUX FILTERS TO LOCAL STATE ON LOAD
|
// SYNC REDUX FILTERS TO LOCAL STATE ON LOAD
|
||||||
// -----------------------------------------------------
|
// -----------------------------------------------------
|
||||||
@ -255,12 +258,14 @@ export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) {
|
|||||||
updateOperator(field, e.target.value)
|
updateOperator(field, e.target.value)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MenuItem value=">=">Greater or equal</MenuItem>
|
{Object.entries(conditionOperators ?? {}).map(
|
||||||
<MenuItem value="<=">Less or equal</MenuItem>
|
([key, value]) => (
|
||||||
<MenuItem value="=">Equal</MenuItem>
|
<MenuItem key={key} value={value}>
|
||||||
<MenuItem value="!=">Not equal</MenuItem>
|
{key.replace(/_/g, " ")}{" "}
|
||||||
<MenuItem value=">">Greater</MenuItem>
|
{/* Optional: make it readable */}
|
||||||
<MenuItem value="<">Less</MenuItem>
|
</MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,3 @@
|
|||||||
// -----------------------------------------------------
|
|
||||||
// UTILITIES
|
|
||||||
// -----------------------------------------------------
|
|
||||||
|
|
||||||
export const extractOperator = (val?: string | null): string | null => {
|
|
||||||
if (!val) return null;
|
|
||||||
|
|
||||||
const match = val.match(/^(==|!=|>=|<=|LIKE|>|<)/);
|
|
||||||
return match ? match[0] : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatWithOperator = (operator: string, value: string) =>
|
|
||||||
`${operator}/${value}`;
|
|
||||||
|
|
||||||
export const normalizeValue = (input: any): string => {
|
export const normalizeValue = (input: any): string => {
|
||||||
if (input == null) return "";
|
if (input == null) return "";
|
||||||
if (typeof input === "string" || typeof input === "number")
|
if (typeof input === "string" || typeof input === "number")
|
||||||
@ -27,22 +13,6 @@ export const normalizeValue = (input: any): string => {
|
|||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const encodeFilter = (fullValue: string): string => {
|
|
||||||
// Split ONLY on the first slash
|
|
||||||
const index = fullValue.indexOf("/");
|
|
||||||
if (index === -1) return fullValue;
|
|
||||||
|
|
||||||
const operator = fullValue.slice(0, index);
|
|
||||||
const rawValue = fullValue.slice(index + 1);
|
|
||||||
|
|
||||||
return `${operator}/${encodeURIComponent(rawValue)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const decodeFilter = (encoded: string): string => {
|
|
||||||
const [operator, encodedValue] = encoded.split("/");
|
|
||||||
return `${operator}/${decodeURIComponent(encodedValue)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Default operator based on field and type
|
// Default operator based on field and type
|
||||||
export const defaultOperatorForField = (
|
export const defaultOperatorForField = (
|
||||||
field: string,
|
field: string,
|
||||||
|
|||||||
@ -1,29 +1,38 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { DataGrid } from "@mui/x-data-grid";
|
import { DataGrid, GridPaginationModel } from "@mui/x-data-grid";
|
||||||
import { Box, Paper, Alert } from "@mui/material";
|
import { Box, Paper, Alert } from "@mui/material";
|
||||||
import DataTableHeader from "./DataTableHeader";
|
import DataTableHeader from "./DataTableHeader";
|
||||||
import StatusChangeDialog from "./StatusChangeDialog";
|
import StatusChangeDialog from "./StatusChangeDialog";
|
||||||
import Spinner from "@/app/components/Spinner/Spinner";
|
import Spinner from "@/app/components/Spinner/Spinner";
|
||||||
import { selectStatus, selectError } from "@/app/redux/advanedSearch/selectors";
|
import {
|
||||||
import { selectEnhancedColumns } from "./re-selectors";
|
selectStatus,
|
||||||
import { useSelector } from "react-redux";
|
selectError,
|
||||||
|
selectPagination,
|
||||||
|
selectPaginationModel,
|
||||||
|
} from "@/app/redux/advanedSearch/selectors";
|
||||||
|
import { makeSelectEnhancedColumns } from "./re-selectors";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { DataRowBase } from "./types";
|
import { DataRowBase } from "./types";
|
||||||
|
import { setPagination } from "@/app/redux/advanedSearch/advancedSearchSlice";
|
||||||
|
import { AppDispatch } from "@/app/redux/store";
|
||||||
|
|
||||||
interface DataTableProps<TRow extends DataRowBase> {
|
interface DataTableProps<TRow extends DataRowBase> {
|
||||||
rows: TRow[];
|
rows: TRow[];
|
||||||
extraColumns?: string[];
|
extraColumns?: string[];
|
||||||
enableStatusActions?: boolean;
|
enableStatusActions?: boolean;
|
||||||
|
totalRows?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataTable = <TRow extends DataRowBase>({
|
const DataTable = <TRow extends DataRowBase>({
|
||||||
rows,
|
rows: localRows,
|
||||||
extraColumns,
|
extraColumns,
|
||||||
enableStatusActions = false,
|
enableStatusActions = false,
|
||||||
|
totalRows: totalRows,
|
||||||
}: DataTableProps<TRow>) => {
|
}: DataTableProps<TRow>) => {
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
const [showExtraColumns, setShowExtraColumns] = useState(false);
|
const [showExtraColumns, setShowExtraColumns] = useState(false);
|
||||||
const [localRows, setLocalRows] = useState(rows);
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
|
const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
|
||||||
const [pendingStatus, setPendingStatus] = useState<string>("");
|
const [pendingStatus, setPendingStatus] = useState<string>("");
|
||||||
@ -35,9 +44,21 @@ const DataTable = <TRow extends DataRowBase>({
|
|||||||
|
|
||||||
const status = useSelector(selectStatus);
|
const status = useSelector(selectStatus);
|
||||||
const errorMessage = useSelector(selectError);
|
const errorMessage = useSelector(selectError);
|
||||||
useEffect(() => {
|
const pagination = useSelector(selectPagination);
|
||||||
setLocalRows(rows);
|
const paginationModel = useSelector(selectPaginationModel);
|
||||||
}, [rows]);
|
|
||||||
|
const handlePaginationModelChange = useCallback(
|
||||||
|
(model: GridPaginationModel) => {
|
||||||
|
console.log("model", model);
|
||||||
|
const nextPage = model.page + 1;
|
||||||
|
const nextLimit = model.pageSize;
|
||||||
|
|
||||||
|
if (nextPage !== pagination.page || nextLimit !== pagination.limit) {
|
||||||
|
dispatch(setPagination({ page: nextPage, limit: nextLimit }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, pagination.page, pagination.limit]
|
||||||
|
);
|
||||||
|
|
||||||
const handleStatusChange = useCallback((rowId: number, newStatus: string) => {
|
const handleStatusChange = useCallback((rowId: number, newStatus: string) => {
|
||||||
setSelectedRowId(rowId);
|
setSelectedRowId(rowId);
|
||||||
@ -78,11 +99,6 @@ const DataTable = <TRow extends DataRowBase>({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLocalRows(prev =>
|
|
||||||
prev.map(row =>
|
|
||||||
row.id === selectedRowId ? { ...row, status: pendingStatus } : row
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
setReason("");
|
setReason("");
|
||||||
setPendingStatus("");
|
setPendingStatus("");
|
||||||
@ -97,18 +113,17 @@ const DataTable = <TRow extends DataRowBase>({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Columns with custom renderers
|
const selectEnhancedColumns = useMemo(makeSelectEnhancedColumns, []);
|
||||||
|
|
||||||
const enhancedColumns = useSelector(state =>
|
const enhancedColumns = useSelector(state =>
|
||||||
selectEnhancedColumns(
|
selectEnhancedColumns(state, {
|
||||||
state,
|
|
||||||
enableStatusActions,
|
enableStatusActions,
|
||||||
extraColumns,
|
extraColumns,
|
||||||
showExtraColumns,
|
showExtraColumns,
|
||||||
localRows,
|
localRows,
|
||||||
handleStatusChange
|
handleStatusChange,
|
||||||
)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{status === "loading" && <Spinner size="small" color="#fff" />}
|
{status === "loading" && <Spinner size="small" color="#fff" />}
|
||||||
@ -130,6 +145,10 @@ const DataTable = <TRow extends DataRowBase>({
|
|||||||
<DataGrid
|
<DataGrid
|
||||||
rows={localRows}
|
rows={localRows}
|
||||||
columns={enhancedColumns}
|
columns={enhancedColumns}
|
||||||
|
paginationModel={paginationModel}
|
||||||
|
onPaginationModelChange={handlePaginationModelChange}
|
||||||
|
paginationMode={totalRows ? "server" : "client"}
|
||||||
|
rowCount={totalRows}
|
||||||
pageSizeOptions={[10, 25, 50, 100]}
|
pageSizeOptions={[10, 25, 50, 100]}
|
||||||
sx={{
|
sx={{
|
||||||
border: 0,
|
border: 0,
|
||||||
|
|||||||
@ -33,5 +33,5 @@ export const TABLE_SEARCH_LABELS: ISearchLabel[] = [
|
|||||||
options: ["pending", "completed", "failed"],
|
options: ["pending", "completed", "failed"],
|
||||||
},
|
},
|
||||||
{ label: "Amount", field: "Amount", type: "text" },
|
{ label: "Amount", field: "Amount", type: "text" },
|
||||||
{ label: "Date / Time", field: "created", type: "date" },
|
{ label: "Date / Time", field: "Created", type: "date" },
|
||||||
];
|
];
|
||||||
|
|||||||
@ -15,51 +15,40 @@ const TRANSACTION_STATUS_FALLBACK: string[] = [
|
|||||||
"error",
|
"error",
|
||||||
];
|
];
|
||||||
|
|
||||||
type StatusChangeHandler = (rowId: number, newStatus: string) => void;
|
type SelectorProps = {
|
||||||
|
enableStatusActions: boolean;
|
||||||
|
extraColumns?: string[] | null;
|
||||||
|
showExtraColumns?: boolean;
|
||||||
|
localRows: DataRowBase[];
|
||||||
|
handleStatusChange: (rowId: number, newStatus: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
const selectEnableStatusActions = (
|
// -------------------------
|
||||||
_state: RootState,
|
// Basic Selectors (props-driven)
|
||||||
enableStatusActions: boolean
|
// -------------------------
|
||||||
) => enableStatusActions;
|
|
||||||
|
|
||||||
const selectExtraColumns = (
|
const propsEnableStatusActions = (_: RootState, props: SelectorProps) =>
|
||||||
_state: RootState,
|
props.enableStatusActions;
|
||||||
_enableStatusActions: boolean,
|
|
||||||
extraColumns?: string[] | null
|
|
||||||
) => extraColumns ?? null;
|
|
||||||
|
|
||||||
const selectShowExtraColumns = (
|
const propsExtraColumns = (_: RootState, props: SelectorProps) =>
|
||||||
_state: RootState,
|
props.extraColumns ?? null;
|
||||||
_enableStatusActions: boolean,
|
|
||||||
_extraColumns?: string[] | null,
|
|
||||||
showExtraColumns = false
|
|
||||||
) => showExtraColumns;
|
|
||||||
|
|
||||||
const selectLocalRows = (
|
const propsShowExtraColumns = (_: RootState, props: SelectorProps) =>
|
||||||
_state: RootState,
|
props.showExtraColumns ?? false;
|
||||||
_enableStatusActions: boolean,
|
|
||||||
_extraColumns?: string[] | null,
|
|
||||||
_showExtraColumns?: boolean,
|
|
||||||
localRows?: DataRowBase[]
|
|
||||||
) => localRows ?? [];
|
|
||||||
|
|
||||||
const noopStatusChangeHandler: StatusChangeHandler = () => {};
|
const propsLocalRows = (_: RootState, props: SelectorProps) =>
|
||||||
|
props.localRows ?? [];
|
||||||
|
|
||||||
const selectStatusChangeHandler = (
|
const propsStatusChangeHandler = (_: RootState, props: SelectorProps) =>
|
||||||
_state: RootState,
|
props.handleStatusChange;
|
||||||
_enableStatusActions: boolean,
|
|
||||||
_extraColumns?: string[] | null,
|
|
||||||
_showExtraColumns?: boolean,
|
|
||||||
_localRows?: DataRowBase[],
|
|
||||||
handleStatusChange?: StatusChangeHandler
|
|
||||||
) => handleStatusChange ?? noopStatusChangeHandler;
|
|
||||||
|
|
||||||
export const selectBaseColumns = createSelector(
|
// -------------------------
|
||||||
[selectEnableStatusActions],
|
// Base Columns
|
||||||
enableStatusActions => {
|
// -------------------------
|
||||||
if (!enableStatusActions) {
|
|
||||||
return TABLE_COLUMNS;
|
const makeSelectBaseColumns = () =>
|
||||||
}
|
createSelector([propsEnableStatusActions], enableStatusActions => {
|
||||||
|
if (!enableStatusActions) return TABLE_COLUMNS;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...TABLE_COLUMNS,
|
...TABLE_COLUMNS,
|
||||||
@ -71,168 +60,192 @@ export const selectBaseColumns = createSelector(
|
|||||||
filterable: false,
|
filterable: false,
|
||||||
} as GridColDef,
|
} as GridColDef,
|
||||||
];
|
];
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const selectVisibleColumns = createSelector(
|
// -------------------------
|
||||||
[selectBaseColumns, selectExtraColumns, selectShowExtraColumns],
|
// Visible Columns
|
||||||
(baseColumns, extraColumns, showExtraColumns) => {
|
// -------------------------
|
||||||
if (!extraColumns || extraColumns.length === 0) {
|
|
||||||
return baseColumns;
|
const makeSelectVisibleColumns = () =>
|
||||||
|
createSelector(
|
||||||
|
[makeSelectBaseColumns(), propsExtraColumns, propsShowExtraColumns],
|
||||||
|
(baseColumns, extraColumns, showExtraColumns) => {
|
||||||
|
if (!extraColumns || extraColumns.length === 0) return baseColumns;
|
||||||
|
|
||||||
|
return showExtraColumns
|
||||||
|
? baseColumns
|
||||||
|
: baseColumns.filter(col => !extraColumns.includes(col.field));
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return showExtraColumns
|
// -------------------------
|
||||||
? baseColumns
|
// Resolved Statuses (STATE-based)
|
||||||
: baseColumns.filter(col => !extraColumns.includes(col.field));
|
// -------------------------
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const selectResolvedTransactionStatuses = createSelector(
|
const makeSelectResolvedStatuses = () =>
|
||||||
[selectTransactionStatuses],
|
createSelector([selectTransactionStatuses], statuses =>
|
||||||
statuses => (statuses.length > 0 ? statuses : TRANSACTION_STATUS_FALLBACK)
|
statuses.length > 0 ? statuses : TRANSACTION_STATUS_FALLBACK
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectEnhancedColumns = createSelector(
|
// -------------------------
|
||||||
[
|
// Enhanced Columns
|
||||||
selectVisibleColumns,
|
// -------------------------
|
||||||
selectLocalRows,
|
|
||||||
selectStatusChangeHandler,
|
export const makeSelectEnhancedColumns = () =>
|
||||||
selectResolvedTransactionStatuses,
|
createSelector(
|
||||||
],
|
[
|
||||||
(
|
makeSelectVisibleColumns(),
|
||||||
visibleColumns,
|
propsLocalRows,
|
||||||
localRows,
|
propsStatusChangeHandler,
|
||||||
handleStatusChange,
|
makeSelectResolvedStatuses(),
|
||||||
resolvedStatusOptions
|
],
|
||||||
): GridColDef[] => {
|
(
|
||||||
return visibleColumns.map(col => {
|
visibleColumns,
|
||||||
if (col.field === "status") {
|
localRows,
|
||||||
return {
|
handleStatusChange,
|
||||||
...col,
|
resolvedStatusOptions
|
||||||
renderCell: (params: GridRenderCellParams) => {
|
): GridColDef[] => {
|
||||||
const value = params.value?.toLowerCase();
|
return visibleColumns.map(col => {
|
||||||
let bgColor = "#e0e0e0";
|
// --------------------------------
|
||||||
let textColor = "#000";
|
// 1. STATUS COLUMN RENDERER
|
||||||
switch (value) {
|
// --------------------------------
|
||||||
case "completed":
|
if (col.field === "status") {
|
||||||
bgColor = "#d0f0c0";
|
return {
|
||||||
textColor = "#1b5e20";
|
...col,
|
||||||
break;
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
case "pending":
|
const value = params.value?.toLowerCase();
|
||||||
bgColor = "#fff4cc";
|
let bgColor = "#e0e0e0";
|
||||||
textColor = "#9e7700";
|
let textColor = "#000";
|
||||||
break;
|
|
||||||
case "inprogress":
|
switch (value) {
|
||||||
bgColor = "#cce5ff";
|
case "completed":
|
||||||
textColor = "#004085";
|
bgColor = "#d0f0c0";
|
||||||
break;
|
textColor = "#1b5e20";
|
||||||
case "error":
|
break;
|
||||||
bgColor = "#ffcdd2";
|
case "pending":
|
||||||
textColor = "#c62828";
|
bgColor = "#fff4cc";
|
||||||
break;
|
textColor = "#9e7700";
|
||||||
}
|
break;
|
||||||
return (
|
case "inprogress":
|
||||||
|
bgColor = "#cce5ff";
|
||||||
|
textColor = "#004085";
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
bgColor = "#ffcdd2";
|
||||||
|
textColor = "#c62828";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
color: textColor,
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.5,
|
||||||
|
borderRadius: 1,
|
||||||
|
fontWeight: 500,
|
||||||
|
textTransform: "capitalize",
|
||||||
|
display: "inline-block",
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{params.value}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
// 2. USER ID COLUMN
|
||||||
|
// --------------------------------
|
||||||
|
if (col.field === "userId") {
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
headerAlign: "center",
|
||||||
|
align: "center",
|
||||||
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: bgColor,
|
display: "grid",
|
||||||
color: textColor,
|
gridTemplateColumns: "1fr auto",
|
||||||
px: 1.5,
|
alignItems: "center",
|
||||||
py: 0.5,
|
|
||||||
borderRadius: 1,
|
|
||||||
fontWeight: 500,
|
|
||||||
textTransform: "capitalize",
|
|
||||||
display: "inline-block",
|
|
||||||
width: "100%",
|
width: "100%",
|
||||||
textAlign: "center",
|
px: 1,
|
||||||
}}
|
|
||||||
>
|
|
||||||
{params.value}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (col.field === "userId") {
|
|
||||||
return {
|
|
||||||
...col,
|
|
||||||
headerAlign: "center",
|
|
||||||
align: "center",
|
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr auto",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "100%",
|
|
||||||
px: 1,
|
|
||||||
}}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
color: "text.primary",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{params.value}
|
|
||||||
</Box>
|
|
||||||
<IconButton
|
|
||||||
href={`/users/${params.value}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
size="small"
|
|
||||||
sx={{ p: 0.5, ml: 1 }}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<OpenInNewIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (col.field === "actions") {
|
|
||||||
return {
|
|
||||||
...col,
|
|
||||||
renderCell: (params: GridRenderCellParams) => {
|
|
||||||
const currentRow = localRows.find(row => row.id === params.id);
|
|
||||||
const options =
|
|
||||||
currentRow?.options?.map(option => option.value) ??
|
|
||||||
resolvedStatusOptions;
|
|
||||||
const uniqueOptions: string[] = Array.from(new Set(options));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select<string>
|
|
||||||
value={currentRow?.status ?? ""}
|
|
||||||
onChange={e =>
|
|
||||||
handleStatusChange(
|
|
||||||
params.id as number,
|
|
||||||
e.target.value as string
|
|
||||||
)
|
|
||||||
}
|
|
||||||
size="small"
|
|
||||||
fullWidth
|
|
||||||
displayEmpty
|
|
||||||
sx={{
|
|
||||||
"& .MuiOutlinedInput-notchedOutline": { border: "none" },
|
|
||||||
"& .MuiSelect-select": { py: 0.5 },
|
|
||||||
}}
|
}}
|
||||||
onClick={e => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{uniqueOptions.map(option => (
|
<Box
|
||||||
<MenuItem key={option} value={option}>
|
sx={{
|
||||||
{option}
|
fontWeight: 500,
|
||||||
</MenuItem>
|
fontSize: "0.875rem",
|
||||||
))}
|
color: "text.primary",
|
||||||
</Select>
|
}}
|
||||||
);
|
>
|
||||||
},
|
{params.value}
|
||||||
};
|
</Box>
|
||||||
}
|
<IconButton
|
||||||
|
href={`/users/${params.value}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
size="small"
|
||||||
|
sx={{ p: 0.5, ml: 1 }}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<OpenInNewIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return col;
|
// --------------------------------
|
||||||
}) as GridColDef[];
|
// 3. ACTIONS COLUMN
|
||||||
}
|
// --------------------------------
|
||||||
);
|
if (col.field === "actions") {
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
const currentRow = localRows.find(row => row.id === params.id);
|
||||||
|
|
||||||
|
const options =
|
||||||
|
currentRow?.options?.map(option => option.value) ??
|
||||||
|
resolvedStatusOptions;
|
||||||
|
|
||||||
|
const uniqueOptions = Array.from(new Set(options));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select<string>
|
||||||
|
value={currentRow?.status ?? ""}
|
||||||
|
onChange={e =>
|
||||||
|
handleStatusChange(
|
||||||
|
params.id as number,
|
||||||
|
e.target.value as string
|
||||||
|
)
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
displayEmpty
|
||||||
|
sx={{
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": { border: "none" },
|
||||||
|
"& .MuiSelect-select": { py: 0.5 },
|
||||||
|
}}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{uniqueOptions.map(option => (
|
||||||
|
<MenuItem key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return col;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { createSelector } from "@reduxjs/toolkit";
|
||||||
import { RootState } from "../store";
|
import { RootState } from "../store";
|
||||||
import {
|
import {
|
||||||
AdvancedSearchFilters,
|
AdvancedSearchFilters,
|
||||||
@ -11,6 +12,14 @@ export const selectFilters = (state: RootState): AdvancedSearchFilters =>
|
|||||||
export const selectPagination = (state: RootState) =>
|
export const selectPagination = (state: RootState) =>
|
||||||
state.advancedSearch.pagination;
|
state.advancedSearch.pagination;
|
||||||
|
|
||||||
|
export const selectPaginationModel = createSelector(
|
||||||
|
[selectPagination],
|
||||||
|
pagination => ({
|
||||||
|
page: Math.max(0, (pagination.page ?? 1) - 1),
|
||||||
|
pageSize: pagination.limit ?? 10,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const selectSort = (state: RootState) => state.advancedSearch.sort;
|
export const selectSort = (state: RootState) => state.advancedSearch.sort;
|
||||||
|
|
||||||
export const selectFilterValue = (
|
export const selectFilterValue = (
|
||||||
|
|||||||
@ -33,3 +33,8 @@ export const selectTransactionStatuses = (state: RootState): string[] =>
|
|||||||
|
|
||||||
export const selectNavigationSidebar = (state: RootState): SidebarLink[] =>
|
export const selectNavigationSidebar = (state: RootState): SidebarLink[] =>
|
||||||
state.metadata.data?.sidebar?.links ?? [];
|
state.metadata.data?.sidebar?.links ?? [];
|
||||||
|
|
||||||
|
export const selectConditionOperators = (
|
||||||
|
state: RootState
|
||||||
|
): Record<string, string> | undefined =>
|
||||||
|
state.metadata.data?.field_names?.conditions;
|
||||||
|
|||||||
@ -3,6 +3,7 @@ interface GetAuditsParams {
|
|||||||
page?: number;
|
page?: number;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
filter?: string;
|
filter?: string;
|
||||||
|
entity?: string;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ export async function getAudits({
|
|||||||
page,
|
page,
|
||||||
sort,
|
sort,
|
||||||
filter,
|
filter,
|
||||||
|
entity,
|
||||||
signal,
|
signal,
|
||||||
}: GetAuditsParams = {}) {
|
}: GetAuditsParams = {}) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
@ -19,6 +21,7 @@ export async function getAudits({
|
|||||||
if (page) params.set("page", String(page));
|
if (page) params.set("page", String(page));
|
||||||
if (sort) params.set("sort", sort);
|
if (sort) params.set("sort", sort);
|
||||||
if (filter) params.set("filter", filter);
|
if (filter) params.set("filter", filter);
|
||||||
|
if (entity) params.set("Entity", entity);
|
||||||
|
|
||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user