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

357 lines
10 KiB
TypeScript

"use client";
import { useState, useEffect, useMemo } from "react";
import {
Box,
TextField,
IconButton,
InputAdornment,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Checkbox,
Paper,
MenuItem,
InputLabel,
Select,
FormControl,
SelectChangeEvent,
} from "@mui/material";
import { debounce } from "@mui/material/utils";
import SearchIcon from "@mui/icons-material/Search";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import AddIcon from "@mui/icons-material/Add";
import { useRouter, useSearchParams } from "next/navigation";
import StatusChangeDialog from "../../DataTable/StatusChangeDialog";
import { HistoryModal } from "@/app/components/HistoryModal/HistoryModal";
import { getTransactionsHistory } from "@/app/services/transactionsHistory";
export interface TableColumn<T> {
field: keyof T | string;
headerName: string;
render?: (value: unknown, row: T) => React.ReactNode;
}
interface MenuItemOption {
value: string;
label?: string;
}
interface DynamicTableProps<T extends { id: string | number }> {
data: {
rows: T[];
columns: TableColumn<T>[];
actions: MenuItemOption[];
};
searchParamKey?: string;
}
interface Transaction {
date: string;
method: string;
amount: number | string;
status: string;
}
export function ApproveTable<T extends { id: string | number }>({
data,
searchParamKey = "merchantId",
}: DynamicTableProps<T>) {
const { rows, columns, actions } = data;
const router = useRouter();
const searchParams = useSearchParams();
const [selected, setSelected] = useState<(string | number)[]>([]);
const [search, setSearch] = useState("");
const [modalOpen, setModalOpen] = useState(false);
const [historyModal, setHistoryModal] = useState(false);
const [reason, setReason] = useState<string>("");
const [action, setAction] = useState("");
const [transactionsHistoryResponse, setTransactionsHistoryResponse] =
useState<{
deposits: Transaction[];
withdrawals: Transaction[];
}>({
deposits: [],
withdrawals: [],
});
useEffect(() => {
const urlValue = searchParams.get(searchParamKey) ?? "";
setSearch(urlValue);
}, [searchParams, searchParamKey]);
const updateURL = useMemo(
() =>
debounce((value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value) params.set(searchParamKey, value);
else params.delete(searchParamKey);
router.replace(`?${params.toString()}`, { scroll: false });
}, 400),
[router, searchParams, searchParamKey]
);
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setSearch(value);
updateURL(value);
};
const handleCheckboxChange = (id: string | number, checked: boolean) => {
setSelected(prev => (checked ? [...prev, id] : prev.filter(x => x !== id)));
};
const handleToggleAll = (checked: boolean) => {
setSelected(checked ? rows.map(r => r.id) : []);
};
const handleActionChange = (e: SelectChangeEvent<string>) => {
const selectedAction = e.target.value;
setAction(selectedAction);
if (selected.length > 0) {
setModalOpen(true);
console.log("Selected Ids", selected);
console.log("Selected Action:", selectedAction);
} else {
alert("No rows selected for this action!");
}
};
const handleStatusSave = () => {
console.log(
`Status changed for row with ID ${selected}. New status: ${action}. Reason: ${reason}`
);
setModalOpen(false);
setReason("");
};
// NEW: plus button handler
const handlePlusClick = async (row: T) => {
console.log("Plus clicked for", row);
const params = `userId=${row.id}`;
const response = await getTransactionsHistory({ query: params });
setTransactionsHistoryResponse(response);
setHistoryModal(true);
};
const stickySecondColLeft = 48;
return (
<>
<Box p={2}>
<Box
mb={2}
display="flex"
justifyContent="space-between"
alignItems="center"
>
<TextField
variant="outlined"
placeholder="Search..."
size="small"
value={search}
onChange={handleSearchChange}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton>
<SearchIcon />
</IconButton>
</InputAdornment>
),
}}
/>
<Box sx={{ width: 180, display: "flex", justifyContent: "center" }}>
<FormControl fullWidth>
<InputLabel>Action</InputLabel>
<Select
value={action}
label="Action"
onChange={handleActionChange}
size="small"
>
{actions.map(item => (
<MenuItem key={item.value} value={item.value}>
{item.label ?? item.value}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
</Box>
<TableContainer
component={Paper}
sx={{
overflowX: "auto",
maxHeight: 500,
width: "calc(100vw - 300px)",
}}
>
<Table
stickyHeader
size="small"
sx={{
minWidth: 1200,
}}
>
<TableHead>
<TableRow>
{/* sticky select-all checkbox */}
<TableCell
padding="checkbox"
sx={{
position: "sticky",
left: 0,
backgroundColor: "white",
zIndex: 3,
}}
>
<Checkbox
checked={selected.length === rows.length && rows.length > 0}
indeterminate={
selected.length > 0 && selected.length < rows.length
}
onChange={e => handleToggleAll(e.target.checked)}
/>
</TableCell>
{/* NEW: sticky plus column header (empty) */}
<TableCell
padding="checkbox"
sx={{
position: "sticky",
left: stickySecondColLeft,
backgroundColor: "white",
zIndex: 2,
width: 48,
maxWidth: 48,
}}
/>
{columns.map((col, i) => (
<TableCell key={i}>{col.headerName}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, idx) => (
<TableRow key={idx}>
{/* sticky checkbox per-row */}
<TableCell
padding="checkbox"
sx={{
position: "sticky",
left: 0,
backgroundColor: "white",
zIndex: 2,
}}
>
<Checkbox
checked={selected.includes(row.id)}
onChange={e =>
handleCheckboxChange(row.id, e.target.checked)
}
/>
</TableCell>
{/* NEW: sticky plus button cell */}
<TableCell
padding="checkbox"
sx={{
position: "sticky",
left: stickySecondColLeft,
backgroundColor: "white",
zIndex: 1,
width: 48,
maxWidth: 48,
}}
>
<IconButton
size="small"
onClick={() => handlePlusClick(row)}
sx={{ p: 0.5 }}
aria-label="expand"
>
<AddIcon fontSize="small" />
</IconButton>
</TableCell>
{columns.map((col, colIdx) => {
const value = row[col.field as keyof T];
if (col.field === "id") {
return (
<TableCell
key={colIdx}
sx={{
p: "6px 16px",
}}
>
<Box
sx={{
display: "grid",
gridTemplateColumns: "1fr auto",
alignItems: "center",
width: "100%",
}}
>
<Box sx={{ textAlign: "center" }}>
{value as React.ReactNode}
</Box>
<IconButton
size="small"
component="a"
href={`/details/${value}`}
target="_blank"
rel="noopener noreferrer"
sx={{ p: 0.5, ml: 1 }}
>
<OpenInNewIcon fontSize="small" />
</IconButton>
</Box>
</TableCell>
);
}
return (
<TableCell key={colIdx}>
{value as React.ReactNode}
</TableCell>
);
})}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
<StatusChangeDialog
open={modalOpen}
newStatus={action}
reason={reason}
setReason={setReason}
handleClose={() => setModalOpen(false)}
handleSave={handleStatusSave}
/>
<HistoryModal
open={historyModal}
onClose={() => {
setHistoryModal(false);
setTransactionsHistoryResponse({ deposits: [], withdrawals: [] });
}}
deposits={transactionsHistoryResponse.deposits}
withdrawals={transactionsHistoryResponse.withdrawals}
/>
</>
);
}