334 lines
9.7 KiB
TypeScript
334 lines
9.7 KiB
TypeScript
import { createSelector } from "@reduxjs/toolkit";
|
|
import { Box, IconButton, MenuItem, Select } from "@mui/material";
|
|
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
|
import { GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|
import { RootState } from "@/app/redux/store";
|
|
import { TABLE_COLUMNS } from "../constants";
|
|
import { selectTransactionStatuses } from "@/app/redux/metadata/selectors";
|
|
import { DataRowBase } from "../types";
|
|
|
|
const TRANSACTION_STATUS_FALLBACK: string[] = [
|
|
"pending",
|
|
"completed",
|
|
"failed",
|
|
"inprogress",
|
|
"error",
|
|
];
|
|
|
|
type SelectorProps = {
|
|
enableStatusActions: boolean;
|
|
extraColumns?: string[] | null;
|
|
showExtraColumns?: boolean;
|
|
localRows: DataRowBase[];
|
|
handleStatusChange: (rowId: number, newStatus: string) => void;
|
|
};
|
|
|
|
// -------------------------
|
|
// Basic Selectors (props-driven)
|
|
// -------------------------
|
|
|
|
const propsEnableStatusActions = (_: RootState, props: SelectorProps) =>
|
|
props.enableStatusActions;
|
|
|
|
const propsExtraColumns = (_: RootState, props: SelectorProps) =>
|
|
props.extraColumns ?? null;
|
|
|
|
const propsShowExtraColumns = (_: RootState, props: SelectorProps) =>
|
|
props.showExtraColumns ?? false;
|
|
|
|
const propsLocalRows = (_: RootState, props: SelectorProps) =>
|
|
props.localRows ?? [];
|
|
|
|
const propsStatusChangeHandler = (_: RootState, props: SelectorProps) =>
|
|
props.handleStatusChange;
|
|
|
|
// -------------------------
|
|
// Helper: Format field name to header name
|
|
// -------------------------
|
|
|
|
/**
|
|
* Converts a field name to a readable header name
|
|
* e.g., "userId" -> "User ID", "transactionId" -> "Transaction ID"
|
|
*/
|
|
const formatFieldNameToHeader = (fieldName: string): string => {
|
|
// Handle camelCase: insert space before capital letters and capitalize first letter
|
|
return fieldName
|
|
.replace(/([A-Z])/g, " $1") // Add space before capital letters
|
|
.replace(/^./, str => str.toUpperCase()) // Capitalize first letter
|
|
.trim();
|
|
};
|
|
|
|
// -------------------------
|
|
// Dynamic Columns from Row Data
|
|
// -------------------------
|
|
|
|
const makeSelectDynamicColumns = () =>
|
|
createSelector([propsLocalRows], (localRows): GridColDef[] => {
|
|
// If no rows, fall back to static columns
|
|
if (!localRows || localRows.length === 0) {
|
|
return TABLE_COLUMNS;
|
|
}
|
|
|
|
// Get all unique field names from the row data
|
|
const fieldSet = new Set<string>();
|
|
localRows.forEach(row => {
|
|
Object.keys(row).forEach(key => {
|
|
if (key !== "options") {
|
|
// Exclude internal fields
|
|
fieldSet.add(key);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Build columns from actual row data fields
|
|
const dynamicColumns: GridColDef[] = Array.from(fieldSet).map(field => {
|
|
// Format field name to readable header
|
|
const headerName = formatFieldNameToHeader(field);
|
|
|
|
// Set default widths based on field type
|
|
let width = 150;
|
|
if (field.includes("id") || field.includes("Id")) {
|
|
width = 180;
|
|
} else if (field === "amount" || field === "currency") {
|
|
width = 120;
|
|
} else if (field === "status") {
|
|
width = 120;
|
|
} else if (
|
|
field.includes("date") ||
|
|
field.includes("Date") ||
|
|
field === "dateTime" ||
|
|
field === "created" ||
|
|
field === "modified"
|
|
) {
|
|
width = 180;
|
|
}
|
|
|
|
return {
|
|
field,
|
|
headerName,
|
|
width,
|
|
sortable: true,
|
|
filterable: true,
|
|
} as GridColDef;
|
|
});
|
|
|
|
return dynamicColumns;
|
|
});
|
|
|
|
// -------------------------
|
|
// Base Columns
|
|
// -------------------------
|
|
|
|
const makeSelectBaseColumns = () =>
|
|
createSelector(
|
|
[makeSelectDynamicColumns(), propsEnableStatusActions],
|
|
(dynamicColumns, enableStatusActions) => {
|
|
const baseColumns = dynamicColumns;
|
|
|
|
if (!enableStatusActions) return baseColumns;
|
|
|
|
return [
|
|
...baseColumns,
|
|
{
|
|
field: "actions",
|
|
headerName: "Actions",
|
|
width: 160,
|
|
sortable: false,
|
|
filterable: false,
|
|
} as GridColDef,
|
|
];
|
|
}
|
|
);
|
|
|
|
// -------------------------
|
|
// Visible Columns
|
|
// -------------------------
|
|
|
|
const makeSelectVisibleColumns = () =>
|
|
createSelector(
|
|
[makeSelectBaseColumns(), propsExtraColumns, propsShowExtraColumns],
|
|
(baseColumns, extraColumns, showExtraColumns) => {
|
|
// Columns are already built from row data, so they're all valid
|
|
if (!extraColumns || extraColumns.length === 0) return baseColumns;
|
|
|
|
const visibleColumns = showExtraColumns
|
|
? baseColumns
|
|
: baseColumns.filter(col => !extraColumns.includes(col.field));
|
|
|
|
console.log("visibleColumns", visibleColumns);
|
|
return visibleColumns;
|
|
}
|
|
);
|
|
// -------------------------
|
|
// Resolved Statuses (STATE-based)
|
|
// -------------------------
|
|
|
|
const makeSelectResolvedStatuses = () =>
|
|
createSelector([selectTransactionStatuses], statuses =>
|
|
statuses.length > 0 ? statuses : TRANSACTION_STATUS_FALLBACK
|
|
);
|
|
|
|
// -------------------------
|
|
// Enhanced Columns
|
|
// -------------------------
|
|
|
|
export const makeSelectEnhancedColumns = () =>
|
|
createSelector(
|
|
[
|
|
makeSelectVisibleColumns(),
|
|
propsLocalRows,
|
|
propsStatusChangeHandler,
|
|
makeSelectResolvedStatuses(),
|
|
],
|
|
(
|
|
visibleColumns,
|
|
localRows,
|
|
handleStatusChange,
|
|
resolvedStatusOptions
|
|
): GridColDef[] => {
|
|
console.log("visibleColumns", visibleColumns);
|
|
return visibleColumns.map(col => {
|
|
// --------------------------------
|
|
// 1. STATUS COLUMN RENDERER
|
|
// --------------------------------
|
|
if (col.field === "status") {
|
|
return {
|
|
...col,
|
|
renderCell: (params: GridRenderCellParams) => {
|
|
const value = params.value?.toLowerCase();
|
|
let bgColor = "#e0e0e0";
|
|
let textColor = "#000";
|
|
|
|
switch (value) {
|
|
case "completed":
|
|
bgColor = "#d0f0c0";
|
|
textColor = "#1b5e20";
|
|
break;
|
|
case "pending":
|
|
bgColor = "#fff4cc";
|
|
textColor = "#9e7700";
|
|
break;
|
|
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
|
|
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>
|
|
),
|
|
};
|
|
}
|
|
|
|
// --------------------------------
|
|
// 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;
|
|
});
|
|
}
|
|
);
|