351 lines
9.9 KiB
TypeScript
351 lines
9.9 KiB
TypeScript
"use client";
|
|
import { useState } from "react";
|
|
import { useSearchParams, useRouter } from "next/navigation";
|
|
import {
|
|
Button,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
FormControl,
|
|
Select,
|
|
MenuItem,
|
|
FormControlLabel,
|
|
Checkbox,
|
|
Stack,
|
|
Paper,
|
|
TextField,
|
|
Box,
|
|
IconButton,
|
|
} from "@mui/material";
|
|
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
|
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
|
import AdvancedSearch from "../AdvancedSearch/AdvancedSearch";
|
|
import SearchFilters from "@/app/components/searchFilter/SearchFilters";
|
|
import { exportData } from "@/app/utils/exportData";
|
|
import StatusChangeDialog from "./StatusChangeDialog";
|
|
import { IDataTable } from "./types";
|
|
|
|
interface IDataTableProps<TRow, TColumn> {
|
|
data: IDataTable<TRow, TColumn>;
|
|
}
|
|
|
|
export type TWithId = { id: number };
|
|
|
|
const DataTable = <TRow extends TWithId, TColumn extends GridColDef>({
|
|
data,
|
|
}: IDataTableProps<TRow, TColumn>) => {
|
|
const { tableRows, tableColumns, tableSearchLabels, extraColumns } = data;
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
|
|
const [rows, setRows] = useState<TRow[]>(tableRows);
|
|
const [open, setOpen] = useState(false);
|
|
const [fileType, setFileType] = useState<"csv" | "xls" | "xlsx">("csv");
|
|
const [onlyCurrentTable, setOnlyCurrentTable] = useState(false);
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
|
|
const [newStatus, setNewStatus] = useState<string>("");
|
|
const [reason, setReason] = useState<string>("");
|
|
const [showExtraColumns, setShowExtraColumns] = useState(false);
|
|
|
|
const filters = Object.fromEntries(searchParams.entries());
|
|
|
|
const handleClickField = (field: string, value: string) => {
|
|
const params = new URLSearchParams(searchParams.toString());
|
|
params.set(field, value);
|
|
router.push(`?${params.toString()}`);
|
|
router.refresh();
|
|
};
|
|
|
|
const handleStatusChange = (id: number, newStatus: string) => {
|
|
setSelectedRowId(id);
|
|
setNewStatus(newStatus);
|
|
setModalOpen(true);
|
|
};
|
|
|
|
const handleStatusSave = () => {
|
|
console.log(
|
|
`Status changed for row with ID ${selectedRowId}. New status: ${newStatus}. Reason: ${reason}`,
|
|
);
|
|
|
|
setRows(
|
|
rows.map((row) =>
|
|
row.id === selectedRowId ? { ...row, status: newStatus } : row,
|
|
),
|
|
);
|
|
setModalOpen(false);
|
|
setReason("");
|
|
};
|
|
|
|
const getColumnsWithDropdown = (columns: TColumn[]): GridColDef[] => {
|
|
return columns.map((col) => {
|
|
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>
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
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()} // keep row click from firing when clicking inside
|
|
>
|
|
<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 row = tableRows.find((r) => r.id === params.id) as {
|
|
id: number;
|
|
status?: string;
|
|
options?: { value: string; label: string }[];
|
|
};
|
|
|
|
const options = row?.options;
|
|
if (!options) return params.value;
|
|
|
|
return (
|
|
<Select
|
|
value={params.value ?? row.status}
|
|
onChange={(e) =>
|
|
handleStatusChange(params.id as number, e.target.value)
|
|
}
|
|
size="small"
|
|
sx={{
|
|
width: "100%",
|
|
"& .MuiOutlinedInput-notchedOutline": { border: "none" },
|
|
"& .MuiSelect-select": { py: 0.5 },
|
|
}}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{options.map((option) => (
|
|
<MenuItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
return col;
|
|
});
|
|
};
|
|
|
|
let filteredColumns = tableColumns;
|
|
if (extraColumns && extraColumns.length > 0) {
|
|
filteredColumns = showExtraColumns
|
|
? tableColumns
|
|
: tableColumns.filter((col) => !extraColumns.includes(col.field));
|
|
}
|
|
|
|
return (
|
|
<Paper sx={{ width: "calc(100vw - 300px)", overflowX: "hidden" }}>
|
|
<Stack
|
|
direction="row"
|
|
justifyContent="space-between"
|
|
alignItems="center"
|
|
p={2}
|
|
flexWrap="wrap"
|
|
gap={2}
|
|
>
|
|
<TextField
|
|
label="Search"
|
|
variant="outlined"
|
|
size="small"
|
|
onChange={(e) => console.log(`setSearchQuery(${e.target.value})`)}
|
|
sx={{ width: 300 }}
|
|
/>
|
|
<AdvancedSearch labels={tableSearchLabels} />
|
|
<SearchFilters filters={filters} />
|
|
<Button
|
|
variant="outlined"
|
|
startIcon={<FileUploadIcon />}
|
|
onClick={() => setOpen(true)}
|
|
>
|
|
Export
|
|
</Button>
|
|
{extraColumns && extraColumns.length > 0 && (
|
|
<Button
|
|
variant="outlined"
|
|
onClick={() => setShowExtraColumns((prev) => !prev)}
|
|
>
|
|
{showExtraColumns ? "Hide Extra Columns" : "Show Extra Columns"}
|
|
</Button>
|
|
)}
|
|
</Stack>
|
|
|
|
<Box sx={{ width: "calc(100vw - 300px)", overflowX: "auto" }}>
|
|
<Box sx={{ minWidth: 1200 }}>
|
|
<DataGrid
|
|
rows={tableRows}
|
|
columns={getColumnsWithDropdown(filteredColumns)}
|
|
initialState={{
|
|
pagination: { paginationModel: { pageSize: 50 } },
|
|
}}
|
|
pageSizeOptions={[50, 100]}
|
|
sx={{
|
|
border: 0,
|
|
cursor: "pointer",
|
|
"& .MuiDataGrid-cell": {
|
|
py: 1,
|
|
textAlign: "center",
|
|
justifyContent: "center",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
},
|
|
"& .MuiDataGrid-columnHeader": {
|
|
textAlign: "center",
|
|
justifyContent: "center",
|
|
},
|
|
}}
|
|
onCellClick={(params) => {
|
|
if (params.field !== "actions") {
|
|
handleClickField(params.field, params.value as string);
|
|
}
|
|
}}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
|
|
<StatusChangeDialog
|
|
open={modalOpen}
|
|
newStatus={newStatus}
|
|
reason={reason}
|
|
setReason={setReason}
|
|
handleClose={() => setModalOpen(false)}
|
|
handleSave={handleStatusSave}
|
|
/>
|
|
|
|
<Dialog open={open} onClose={() => setOpen(false)}>
|
|
<DialogTitle>Export Transactions</DialogTitle>
|
|
<DialogContent>
|
|
<FormControl fullWidth sx={{ mt: 2 }}>
|
|
<Select
|
|
value={fileType}
|
|
onChange={(e) =>
|
|
setFileType(e.target.value as "csv" | "xls" | "xlsx")
|
|
}
|
|
>
|
|
<MenuItem value="csv">CSV</MenuItem>
|
|
<MenuItem value="xls">XLS</MenuItem>
|
|
<MenuItem value="xlsx">XLSX</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={onlyCurrentTable}
|
|
onChange={(e) => setOnlyCurrentTable(e.target.checked)}
|
|
/>
|
|
}
|
|
label="Only export current table"
|
|
sx={{ mt: 2 }}
|
|
/>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={() =>
|
|
exportData(
|
|
tableRows,
|
|
tableColumns,
|
|
fileType,
|
|
onlyCurrentTable,
|
|
setOpen,
|
|
)
|
|
}
|
|
>
|
|
Export
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</Paper>
|
|
);
|
|
};
|
|
|
|
export default DataTable;
|