2025-10-25 11:39:24 +02:00

350 lines
10 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;