2026-01-23 23:54:58 +00:00
|
|
|
|
import Stack from "@mui/material/Stack";
|
2026-02-01 00:49:35 +00:00
|
|
|
|
import Grid2 from "@mui/material/Grid";
|
2026-01-23 23:54:58 +00:00
|
|
|
|
import Box from "@mui/material/Box";
|
|
|
|
|
|
import Typography from "@mui/material/Typography";
|
|
|
|
|
|
import Paper from "@mui/material/Paper";
|
|
|
|
|
|
import Table from "@mui/material/Table";
|
|
|
|
|
|
import TableBody from "@mui/material/TableBody";
|
|
|
|
|
|
import TableCell from "@mui/material/TableCell";
|
|
|
|
|
|
import TableContainer from "@mui/material/TableContainer";
|
|
|
|
|
|
import TableHead from "@mui/material/TableHead";
|
|
|
|
|
|
import TableRow from "@mui/material/TableRow";
|
|
|
|
|
|
import Collapse from "@mui/material/Collapse";
|
|
|
|
|
|
import Tooltip from "@mui/material/Tooltip";
|
|
|
|
|
|
|
|
|
|
|
|
import IconButton from "@mui/material/IconButton";
|
|
|
|
|
|
import {
|
|
|
|
|
|
ChevronsLeft,
|
|
|
|
|
|
ChevronsRight,
|
|
|
|
|
|
ChevronLeft,
|
|
|
|
|
|
ChevronRight,
|
|
|
|
|
|
Coffee,
|
|
|
|
|
|
Ellipsis,
|
|
|
|
|
|
PartyPopper,
|
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
|
|
|
|
|
|
|
import TablePagination from "@mui/material/TablePagination";
|
2026-01-24 00:29:19 +00:00
|
|
|
|
import type { TablePaginationOwnProps } from "@mui/material/TablePagination";
|
2026-01-23 23:54:58 +00:00
|
|
|
|
|
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
2026-02-05 18:30:43 +00:00
|
|
|
|
import { useState, Fragment, useEffect } from "react";
|
2026-01-23 23:54:58 +00:00
|
|
|
|
import { useTheme } from "@mui/material/styles";
|
2026-02-28 16:30:04 +00:00
|
|
|
|
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
2026-02-05 21:40:36 +00:00
|
|
|
|
import type { SxProps, Theme } from "@mui/material/styles";
|
2026-01-23 23:54:58 +00:00
|
|
|
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
|
|
|
|
|
|
|
|
|
|
export type Header<T> = {
|
|
|
|
|
|
id: number | string;
|
|
|
|
|
|
content: React.ReactNode;
|
|
|
|
|
|
onClick?: (event: React.MouseEvent<HTMLTableCellElement | null>, row: T) => void;
|
|
|
|
|
|
render: (row: T) => React.ReactNode;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type DataTableProps<T extends { id?: string | number; _id?: string | number }> = {
|
|
|
|
|
|
headers: Header<T>[];
|
|
|
|
|
|
data: T[];
|
|
|
|
|
|
onRowClick?: (row: T) => void;
|
|
|
|
|
|
cardsOnSmallScreens?: boolean;
|
|
|
|
|
|
expandableRows?: boolean;
|
|
|
|
|
|
renderExpandedContent?: (row: T) => React.ReactNode;
|
|
|
|
|
|
emptyViewText?: string;
|
|
|
|
|
|
emptyViewPositive?: boolean;
|
2026-02-05 21:40:36 +00:00
|
|
|
|
getRowSx?: (row: T) => SxProps<Theme>;
|
2026-01-23 23:54:58 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export function DataTable<
|
|
|
|
|
|
T extends {
|
|
|
|
|
|
id?: string | number;
|
|
|
|
|
|
_id?: string | number;
|
|
|
|
|
|
onRowClick?: (row: T) => void;
|
|
|
|
|
|
},
|
|
|
|
|
|
>({
|
|
|
|
|
|
headers,
|
|
|
|
|
|
data,
|
|
|
|
|
|
onRowClick,
|
|
|
|
|
|
cardsOnSmallScreens = true,
|
|
|
|
|
|
expandableRows = false,
|
|
|
|
|
|
renderExpandedContent,
|
|
|
|
|
|
emptyViewText,
|
|
|
|
|
|
emptyViewPositive,
|
2026-02-05 21:40:36 +00:00
|
|
|
|
getRowSx,
|
2026-01-23 23:54:58 +00:00
|
|
|
|
}: DataTableProps<T>) {
|
|
|
|
|
|
const theme = useTheme();
|
|
|
|
|
|
const [expanded, setExpanded] = useState<(string | number) | null>(null);
|
|
|
|
|
|
const handleExpand = (row: T) => {
|
|
|
|
|
|
const key = row.id || row._id || null;
|
|
|
|
|
|
setExpanded(expanded === key ? null : key);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
|
|
|
|
|
|
|
|
|
|
|
if (data.length === 0 || headers.length === 0) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<EmptyView
|
|
|
|
|
|
text={emptyViewText}
|
|
|
|
|
|
positive={emptyViewPositive}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const keys = [];
|
|
|
|
|
|
// Return stack of cards for small screens
|
|
|
|
|
|
if (isSmall && cardsOnSmallScreens) {
|
|
|
|
|
|
return (
|
2026-02-28 16:30:04 +00:00
|
|
|
|
<Stack spacing={theme.spacing(LAYOUT.XS)}>
|
2026-01-23 23:54:58 +00:00
|
|
|
|
{data.map((row) => {
|
|
|
|
|
|
const key = row.id || row._id || Math.random();
|
|
|
|
|
|
keys.push(key);
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Stack
|
|
|
|
|
|
onClick={() => (onRowClick ? onRowClick(row) : null)}
|
2026-02-28 16:30:04 +00:00
|
|
|
|
spacing={theme.spacing(LAYOUT.XS)}
|
2026-01-23 23:54:58 +00:00
|
|
|
|
sx={{
|
|
|
|
|
|
borderStyle: "solid",
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
borderColor: theme.palette.divider,
|
|
|
|
|
|
borderRadius: theme.shape.borderRadius,
|
2026-02-28 16:30:04 +00:00
|
|
|
|
padding: theme.spacing(LAYOUT.XS),
|
2026-01-23 23:54:58 +00:00
|
|
|
|
cursor: onRowClick ? "pointer" : "default",
|
|
|
|
|
|
}}
|
|
|
|
|
|
key={key}
|
|
|
|
|
|
>
|
|
|
|
|
|
{headers.map((header) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Grid2
|
|
|
|
|
|
container
|
|
|
|
|
|
key={header.id}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Grid2
|
|
|
|
|
|
size={5}
|
|
|
|
|
|
display={"flex"}
|
|
|
|
|
|
alignItems={"center"}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Typography
|
|
|
|
|
|
component="div"
|
|
|
|
|
|
color={theme.palette.text.primary}
|
|
|
|
|
|
>
|
|
|
|
|
|
{header.content}
|
|
|
|
|
|
</Typography>
|
|
|
|
|
|
</Grid2>
|
|
|
|
|
|
<Grid2
|
|
|
|
|
|
size={7}
|
|
|
|
|
|
display="flex"
|
|
|
|
|
|
alignItems={"center"}
|
|
|
|
|
|
>
|
|
|
|
|
|
{header.render(row)}{" "}
|
|
|
|
|
|
</Grid2>
|
|
|
|
|
|
</Grid2>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
{expandableRows && renderExpandedContent && renderExpandedContent(row) && (
|
|
|
|
|
|
<Grid2 size={12}>{renderExpandedContent(row)}</Grid2>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Stack>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</Stack>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<TableContainer
|
|
|
|
|
|
component={Paper}
|
|
|
|
|
|
elevation={0}
|
2026-01-26 18:28:55 +00:00
|
|
|
|
sx={{ boxShadow: "none", overflowX: "hidden" }}
|
2026-01-23 23:54:58 +00:00
|
|
|
|
>
|
|
|
|
|
|
<Table
|
|
|
|
|
|
sx={{
|
|
|
|
|
|
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
|
2026-02-28 16:30:04 +00:00
|
|
|
|
paddingLeft: theme.spacing(LAYOUT.MD),
|
2026-01-23 23:54:58 +00:00
|
|
|
|
},
|
|
|
|
|
|
"& .MuiTableCell-root": {
|
|
|
|
|
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
|
|
|
|
},
|
|
|
|
|
|
"& .MuiTableHead-root .MuiTableRow-root": {
|
|
|
|
|
|
height: "28px",
|
|
|
|
|
|
},
|
|
|
|
|
|
"& :is(th)": {
|
|
|
|
|
|
backgroundColor: theme.palette.background.paper,
|
|
|
|
|
|
color: theme.palette.text.secondary,
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
textTransform: "uppercase",
|
2026-02-28 16:30:04 +00:00
|
|
|
|
padding: `${theme.spacing(SPACING.LG)} ${theme.spacing(LAYOUT.MD)}`,
|
2026-01-23 23:54:58 +00:00
|
|
|
|
fontSize: theme.typography.fontSize,
|
|
|
|
|
|
},
|
|
|
|
|
|
"& :is(td)": {
|
|
|
|
|
|
backgroundColor: theme.palette.background.paper,
|
|
|
|
|
|
color: theme.palette.text.secondary,
|
2026-02-28 16:30:04 +00:00
|
|
|
|
padding: `${theme.spacing(LAYOUT.MD)} ${theme.spacing(LAYOUT.MD)}`,
|
2026-01-23 23:54:58 +00:00
|
|
|
|
fontSize: theme.typography.fontSize,
|
2026-01-26 18:27:12 +00:00
|
|
|
|
overflowX: "hidden",
|
2026-01-23 23:54:58 +00:00
|
|
|
|
},
|
|
|
|
|
|
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
|
|
|
|
|
|
borderBottom: "none",
|
|
|
|
|
|
},
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<TableHead>
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
{headers.map((header, idx) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<TableCell
|
|
|
|
|
|
align={idx === 0 ? "left" : "center"}
|
|
|
|
|
|
key={header.id}
|
|
|
|
|
|
>
|
|
|
|
|
|
{header.content}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableHead>
|
|
|
|
|
|
<TableBody>
|
|
|
|
|
|
{data.map((row) => {
|
|
|
|
|
|
const key = row.id || row._id || Math.random();
|
|
|
|
|
|
const isExpanded = expanded === key;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Fragment key={key}>
|
|
|
|
|
|
<TableRow
|
|
|
|
|
|
sx={{
|
|
|
|
|
|
cursor: onRowClick ? "pointer" : "default",
|
2026-02-05 21:40:36 +00:00
|
|
|
|
...(getRowSx?.(row) as object),
|
2026-01-23 23:54:58 +00:00
|
|
|
|
}}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
if (expandableRows) handleExpand(row);
|
|
|
|
|
|
else if (onRowClick) onRowClick(row);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{headers.map((header, index) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<TableCell
|
|
|
|
|
|
align={index === 0 ? "left" : "center"}
|
|
|
|
|
|
key={header.id}
|
|
|
|
|
|
onClick={
|
|
|
|
|
|
header.onClick ? (e) => header.onClick!(e, row) : undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{header.render(row)}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
{expandableRows &&
|
|
|
|
|
|
renderExpandedContent &&
|
|
|
|
|
|
renderExpandedContent(row) && (
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableCell
|
|
|
|
|
|
colSpan={headers.length}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderBottom: isExpanded ? undefined : "none",
|
|
|
|
|
|
paddingTop: 0,
|
|
|
|
|
|
paddingBottom: 0,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Collapse
|
|
|
|
|
|
in={isExpanded}
|
|
|
|
|
|
timeout="auto"
|
|
|
|
|
|
unmountOnExit
|
|
|
|
|
|
>
|
|
|
|
|
|
<Box sx={{ pt: 4, pb: 4 }}>{renderExpandedContent(row)}</Box>
|
|
|
|
|
|
</Collapse>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Fragment>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</TableBody>
|
|
|
|
|
|
</Table>
|
|
|
|
|
|
</TableContainer>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface TablePaginationActionsProps {
|
|
|
|
|
|
count: number;
|
|
|
|
|
|
page: number;
|
|
|
|
|
|
rowsPerPage: number;
|
|
|
|
|
|
onPageChange: (event: React.MouseEvent<HTMLButtonElement>, newPage: number) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function TablePaginationActions(props: TablePaginationActionsProps) {
|
|
|
|
|
|
const theme = useTheme();
|
|
|
|
|
|
const { count, page, rowsPerPage, onPageChange } = props;
|
|
|
|
|
|
|
|
|
|
|
|
const handleFirstPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
|
|
|
|
onPageChange(event, 0);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleBackButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
|
|
|
|
onPageChange(event, page - 1);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleNextButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
|
|
|
|
onPageChange(event, page + 1);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleLastPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
|
|
|
|
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Box
|
|
|
|
|
|
sx={{ flexShrink: 0, ml: { xs: 0, md: 2.5 } }}
|
|
|
|
|
|
className="table-pagination-actions"
|
|
|
|
|
|
>
|
|
|
|
|
|
<IconButton
|
|
|
|
|
|
onClick={handleFirstPageButtonClick}
|
|
|
|
|
|
disabled={page === 0}
|
|
|
|
|
|
aria-label="first page"
|
|
|
|
|
|
>
|
|
|
|
|
|
{theme.direction === "rtl" ? (
|
|
|
|
|
|
<ChevronsRight
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ChevronsLeft
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</IconButton>
|
|
|
|
|
|
<IconButton
|
|
|
|
|
|
onClick={handleBackButtonClick}
|
|
|
|
|
|
disabled={page === 0}
|
|
|
|
|
|
aria-label="previous page"
|
|
|
|
|
|
>
|
|
|
|
|
|
{theme.direction === "rtl" ? (
|
|
|
|
|
|
<ChevronRight
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ChevronLeft
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</IconButton>
|
|
|
|
|
|
<IconButton
|
|
|
|
|
|
onClick={handleNextButtonClick}
|
|
|
|
|
|
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
|
|
|
|
|
aria-label="next page"
|
|
|
|
|
|
>
|
|
|
|
|
|
{theme.direction === "rtl" ? (
|
|
|
|
|
|
<ChevronLeft
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ChevronRight
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</IconButton>
|
|
|
|
|
|
<IconButton
|
|
|
|
|
|
onClick={handleLastPageButtonClick}
|
|
|
|
|
|
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
|
|
|
|
|
aria-label="last page"
|
|
|
|
|
|
>
|
|
|
|
|
|
{theme.direction === "rtl" ? (
|
|
|
|
|
|
<ChevronsLeft
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ChevronsRight
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</IconButton>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface HasMoreTablePaginationActionsProps {
|
|
|
|
|
|
hasMore?: boolean;
|
|
|
|
|
|
page: number;
|
|
|
|
|
|
onPageChange: (event: React.MouseEvent<HTMLButtonElement>, newPage: number) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
function HasMoreTablePaginationActions(props: HasMoreTablePaginationActionsProps) {
|
|
|
|
|
|
const theme = useTheme();
|
|
|
|
|
|
const { hasMore, page, onPageChange } = props;
|
|
|
|
|
|
|
|
|
|
|
|
const handleFirstPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
|
|
|
|
onPageChange(event, 0);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleBackButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
|
|
|
|
onPageChange(event, page - 1);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleNextButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
|
|
|
|
onPageChange(event, page + 1);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Box
|
|
|
|
|
|
sx={{ flexShrink: 0, ml: { xs: 0, md: 2.5 } }}
|
|
|
|
|
|
className="table-pagination-actions"
|
|
|
|
|
|
>
|
|
|
|
|
|
<IconButton
|
|
|
|
|
|
onClick={handleFirstPageButtonClick}
|
|
|
|
|
|
disabled={page === 0}
|
|
|
|
|
|
aria-label="first page"
|
|
|
|
|
|
>
|
|
|
|
|
|
{theme.direction === "rtl" ? (
|
|
|
|
|
|
<ChevronsRight
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ChevronsLeft
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</IconButton>
|
|
|
|
|
|
<IconButton
|
|
|
|
|
|
onClick={handleBackButtonClick}
|
|
|
|
|
|
disabled={page === 0}
|
|
|
|
|
|
aria-label="previous page"
|
|
|
|
|
|
>
|
|
|
|
|
|
{theme.direction === "rtl" ? (
|
|
|
|
|
|
<ChevronRight
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ChevronLeft
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</IconButton>
|
|
|
|
|
|
<IconButton
|
|
|
|
|
|
onClick={handleNextButtonClick}
|
|
|
|
|
|
disabled={hasMore === false}
|
|
|
|
|
|
aria-label="next page"
|
|
|
|
|
|
>
|
|
|
|
|
|
{theme.direction === "rtl" ? (
|
|
|
|
|
|
<ChevronLeft
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ChevronRight
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</IconButton>
|
|
|
|
|
|
<Tooltip title={hasMore === true ? "More pages available" : "No more pages"}>
|
|
|
|
|
|
<IconButton
|
|
|
|
|
|
disabled={hasMore === false}
|
|
|
|
|
|
aria-label="next page"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Ellipsis
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</IconButton>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface PaginationProps extends TablePaginationOwnProps {
|
|
|
|
|
|
component?: React.ElementType;
|
|
|
|
|
|
hasMore?: boolean;
|
2026-02-05 18:30:43 +00:00
|
|
|
|
itemsOnPage?: number;
|
2026-01-23 23:54:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const Pagination = ({ ...props }: PaginationProps) => {
|
2026-02-05 18:30:43 +00:00
|
|
|
|
const { hasMore, itemsOnPage, ...rest } = props;
|
2026-01-23 23:54:58 +00:00
|
|
|
|
const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm"));
|
|
|
|
|
|
const theme = useTheme();
|
2026-02-05 18:30:43 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (
|
|
|
|
|
|
typeof itemsOnPage === "number" &&
|
|
|
|
|
|
itemsOnPage === 0 &&
|
|
|
|
|
|
rest.count > 0 &&
|
|
|
|
|
|
rest.page > 0 &&
|
|
|
|
|
|
rest.onPageChange
|
|
|
|
|
|
) {
|
|
|
|
|
|
rest.onPageChange(null, rest.page - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [itemsOnPage, rest.count, rest.page, rest.onPageChange]);
|
|
|
|
|
|
|
2026-01-23 23:54:58 +00:00
|
|
|
|
const labelDisplayedRows = ({
|
|
|
|
|
|
from,
|
|
|
|
|
|
to,
|
|
|
|
|
|
count,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
from: number;
|
|
|
|
|
|
to: number;
|
|
|
|
|
|
count: number;
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
if (hasMore) {
|
|
|
|
|
|
return to === 0 ? "" : `${from}–${to}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return `${from}–${to} of ${count}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<TablePagination
|
|
|
|
|
|
ActionsComponent={(props) =>
|
|
|
|
|
|
typeof hasMore === "boolean" ? (
|
|
|
|
|
|
<HasMoreTablePaginationActions
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
hasMore={hasMore}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<TablePaginationActions {...props} />
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
rowsPerPageOptions={[5, 10, 25]}
|
|
|
|
|
|
labelDisplayedRows={labelDisplayedRows}
|
|
|
|
|
|
{...rest}
|
|
|
|
|
|
sx={{
|
|
|
|
|
|
"& .MuiTablePagination-toolbar": isSmall
|
|
|
|
|
|
? {
|
|
|
|
|
|
display: "grid",
|
|
|
|
|
|
gridTemplateColumns: "1fr 1fr",
|
|
|
|
|
|
gridAutoRows: "auto",
|
2026-02-28 16:30:04 +00:00
|
|
|
|
rowGap: theme.spacing(SPACING.SM),
|
2026-01-23 23:54:58 +00:00
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
justifyItems: "center",
|
|
|
|
|
|
paddingLeft: 0,
|
|
|
|
|
|
paddingRight: 0,
|
|
|
|
|
|
}
|
|
|
|
|
|
: { display: "flex", alignItems: "center" },
|
|
|
|
|
|
"& .MuiTablePagination-selectLabel": {
|
|
|
|
|
|
gridColumn: "1",
|
|
|
|
|
|
gridRow: "1",
|
|
|
|
|
|
justifySelf: "center",
|
|
|
|
|
|
},
|
|
|
|
|
|
"& .MuiTablePagination-select": {
|
|
|
|
|
|
gridColumn: "2",
|
|
|
|
|
|
gridRow: "1",
|
|
|
|
|
|
justifySelf: "center",
|
|
|
|
|
|
},
|
|
|
|
|
|
"& .MuiTablePagination-displayedRows": isSmall
|
|
|
|
|
|
? { gridColumn: "1 / span 2", gridRow: "2", justifySelf: "center" }
|
|
|
|
|
|
: {},
|
|
|
|
|
|
"& .table-pagination-actions": isSmall
|
|
|
|
|
|
? { gridColumn: "1 / span 2", gridRow: "3", justifySelf: "center" }
|
|
|
|
|
|
: {},
|
|
|
|
|
|
"& .MuiSelect-select": {
|
|
|
|
|
|
border: 1,
|
|
|
|
|
|
borderColor: theme.palette.divider,
|
|
|
|
|
|
borderRadius: theme.shape.borderRadius,
|
|
|
|
|
|
},
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const EmptyView = ({ text, positive }: { text?: string; positive?: boolean }) => {
|
|
|
|
|
|
const theme = useTheme();
|
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
const Icon = positive ? PartyPopper : Coffee;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Stack
|
|
|
|
|
|
alignItems={"center"}
|
|
|
|
|
|
justifyContent={"center"}
|
|
|
|
|
|
sx={{
|
2026-02-28 16:30:04 +00:00
|
|
|
|
py: theme.spacing(LAYOUT.XL),
|
|
|
|
|
|
px: theme.spacing(LAYOUT.XS),
|
2026-01-23 23:54:58 +00:00
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
borderStyle: "solid",
|
|
|
|
|
|
borderColor: theme.palette.divider,
|
|
|
|
|
|
borderRadius: theme.shape.borderRadius,
|
|
|
|
|
|
textAlign: "center",
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Box
|
|
|
|
|
|
sx={{
|
|
|
|
|
|
width: 64,
|
|
|
|
|
|
height: 64,
|
|
|
|
|
|
borderRadius: "50%",
|
|
|
|
|
|
backgroundColor: theme.palette.action.hover,
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
justifyContent: "center",
|
2026-02-28 16:30:04 +00:00
|
|
|
|
mb: theme.spacing(SPACING.LG),
|
2026-01-23 23:54:58 +00:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Icon
|
|
|
|
|
|
size={28}
|
|
|
|
|
|
strokeWidth={1}
|
|
|
|
|
|
color={theme.palette.text.secondary}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
<Typography
|
|
|
|
|
|
variant="subtitle1"
|
|
|
|
|
|
color={theme.palette.text.primary}
|
|
|
|
|
|
sx={{ fontWeight: 500 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{text ?? t("common.table.empty")}
|
|
|
|
|
|
</Typography>
|
|
|
|
|
|
</Stack>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|