import Stack from "@mui/material/Stack"; import Grid2 from "@mui/material/Grid"; 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"; import type { TablePaginationOwnProps } from "@mui/material/TablePagination"; import { useTranslation } from "react-i18next"; import { useState, Fragment, useEffect } from "react"; import { useTheme } from "@mui/material/styles"; import { SPACING, LAYOUT } from "@/Utils/Theme/constants"; import type { SxProps, Theme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; export type Header = { id: number | string; content: React.ReactNode; onClick?: (event: React.MouseEvent, row: T) => void; render: (row: T) => React.ReactNode; }; type DataTableProps = { headers: Header[]; data: T[]; onRowClick?: (row: T) => void; cardsOnSmallScreens?: boolean; expandableRows?: boolean; renderExpandedContent?: (row: T) => React.ReactNode; emptyViewText?: string; emptyViewPositive?: boolean; getRowSx?: (row: T) => SxProps; }; 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, getRowSx, }: DataTableProps) { 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 ( ); } const keys = []; // Return stack of cards for small screens if (isSmall && cardsOnSmallScreens) { return ( {data.map((row) => { const key = row.id || row._id || Math.random(); keys.push(key); return ( (onRowClick ? onRowClick(row) : null)} spacing={theme.spacing(LAYOUT.XS)} sx={{ borderStyle: "solid", borderWidth: 1, borderColor: theme.palette.divider, borderRadius: theme.shape.borderRadius, padding: theme.spacing(LAYOUT.XS), cursor: onRowClick ? "pointer" : "default", }} key={key} > {headers.map((header) => { return ( {header.content} {header.render(row)}{" "} ); })} {expandableRows && renderExpandedContent && renderExpandedContent(row) && ( {renderExpandedContent(row)} )} ); })} ); } return ( {headers.map((header, idx) => { return ( {header.content} ); })} {data.map((row) => { const key = row.id || row._id || Math.random(); const isExpanded = expanded === key; return ( { if (expandableRows) handleExpand(row); else if (onRowClick) onRowClick(row); }} > {headers.map((header, index) => { return ( header.onClick!(e, row) : undefined } > {header.render(row)} ); })} {expandableRows && renderExpandedContent && renderExpandedContent(row) && ( {renderExpandedContent(row)} )} ); })}
); } interface TablePaginationActionsProps { count: number; page: number; rowsPerPage: number; onPageChange: (event: React.MouseEvent, newPage: number) => void; } function TablePaginationActions(props: TablePaginationActionsProps) { const theme = useTheme(); const { count, page, rowsPerPage, onPageChange } = props; const handleFirstPageButtonClick = (event: React.MouseEvent) => { onPageChange(event, 0); }; const handleBackButtonClick = (event: React.MouseEvent) => { onPageChange(event, page - 1); }; const handleNextButtonClick = (event: React.MouseEvent) => { onPageChange(event, page + 1); }; const handleLastPageButtonClick = (event: React.MouseEvent) => { onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)); }; return ( {theme.direction === "rtl" ? ( ) : ( )} {theme.direction === "rtl" ? ( ) : ( )} = Math.ceil(count / rowsPerPage) - 1} aria-label="next page" > {theme.direction === "rtl" ? ( ) : ( )} = Math.ceil(count / rowsPerPage) - 1} aria-label="last page" > {theme.direction === "rtl" ? ( ) : ( )} ); } interface HasMoreTablePaginationActionsProps { hasMore?: boolean; page: number; onPageChange: (event: React.MouseEvent, newPage: number) => void; } function HasMoreTablePaginationActions(props: HasMoreTablePaginationActionsProps) { const theme = useTheme(); const { hasMore, page, onPageChange } = props; const handleFirstPageButtonClick = (event: React.MouseEvent) => { onPageChange(event, 0); }; const handleBackButtonClick = (event: React.MouseEvent) => { onPageChange(event, page - 1); }; const handleNextButtonClick = (event: React.MouseEvent) => { onPageChange(event, page + 1); }; return ( {theme.direction === "rtl" ? ( ) : ( )} {theme.direction === "rtl" ? ( ) : ( )} {theme.direction === "rtl" ? ( ) : ( )} ); } interface PaginationProps extends TablePaginationOwnProps { component?: React.ElementType; hasMore?: boolean; itemsOnPage?: number; } export const Pagination = ({ ...props }: PaginationProps) => { const { hasMore, itemsOnPage, ...rest } = props; const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm")); const theme = useTheme(); 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]); const labelDisplayedRows = ({ from, to, count, }: { from: number; to: number; count: number; }) => { if (hasMore) { return to === 0 ? "" : `${from}–${to}`; } return `${from}–${to} of ${count}`; }; return ( typeof hasMore === "boolean" ? ( ) : ( ) } rowsPerPageOptions={[5, 10, 25]} labelDisplayedRows={labelDisplayedRows} {...rest} sx={{ "& .MuiTablePagination-toolbar": isSmall ? { display: "grid", gridTemplateColumns: "1fr 1fr", gridAutoRows: "auto", rowGap: theme.spacing(SPACING.SM), 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 ( {text ?? t("common.table.empty")} ); };