2025-11-19 20:42:31 +01:00

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;
});
}
);