FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

287 lines
9.5 KiB

  1. "use client";
  2. import React from "react";
  3. import Paper from "@mui/material/Paper";
  4. import Table from "@mui/material/Table";
  5. import TableBody from "@mui/material/TableBody";
  6. import TableCell, { TableCellProps } from "@mui/material/TableCell";
  7. import TableContainer from "@mui/material/TableContainer";
  8. import TableHead from "@mui/material/TableHead";
  9. import TablePagination, {
  10. TablePaginationProps,
  11. } from "@mui/material/TablePagination";
  12. import TableRow from "@mui/material/TableRow";
  13. import IconButton, { IconButtonOwnProps } from "@mui/material/IconButton";
  14. import { ButtonOwnProps, Icon, IconOwnProps, SxProps, Theme } from "@mui/material";
  15. import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
  16. import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
  17. export interface ResultWithId {
  18. id: string | number;
  19. }
  20. type ColumnType = "icon" | "decimal" | "integer";
  21. interface BaseColumn<T extends ResultWithId> {
  22. name: keyof T;
  23. label: string;
  24. align?: TableCellProps["align"];
  25. headerAlign?: TableCellProps["align"];
  26. sx?: SxProps<Theme> | undefined;
  27. style?: Partial<HTMLElement["style"]> & { [propName: string]: string };
  28. type?: ColumnType;
  29. renderCell?: (params: T) => React.ReactNode;
  30. }
  31. interface IconColumn<T extends ResultWithId> extends BaseColumn<T> {
  32. name: keyof T;
  33. type: "icon";
  34. icon?: React.ReactNode;
  35. icons?: { [columnValue in keyof T]: React.ReactNode };
  36. color?: IconOwnProps["color"];
  37. colors?: { [columnValue in keyof T]: IconOwnProps["color"] };
  38. }
  39. interface DecimalColumn<T extends ResultWithId> extends BaseColumn<T> {
  40. type: "decimal";
  41. }
  42. interface IntegerColumn<T extends ResultWithId> extends BaseColumn<T> {
  43. type: "integer";
  44. }
  45. interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> {
  46. onClick: (item: T) => void;
  47. buttonIcon: React.ReactNode;
  48. buttonIcons: { [columnValue in keyof T]: React.ReactNode };
  49. buttonColor?: IconButtonOwnProps["color"];
  50. }
  51. export type Column<T extends ResultWithId> =
  52. | BaseColumn<T>
  53. | IconColumn<T>
  54. | DecimalColumn<T>
  55. | ColumnWithAction<T>;
  56. interface Props<T extends ResultWithId> {
  57. items: T[],
  58. columns: Column<T>[],
  59. noWrapper?: boolean,
  60. setPagingController?: (value: (((prevState: { pageNum: number; pageSize: number; totalCount: number }) => {
  61. pageNum: number;
  62. pageSize: number;
  63. totalCount: number
  64. }) | { pageNum: number; pageSize: number; totalCount: number })) => void,
  65. pagingController: { pageNum: number; pageSize: number; totalCount: number },
  66. isAutoPaging?: boolean
  67. }
  68. function isActionColumn<T extends ResultWithId>(
  69. column: Column<T>,
  70. ): column is ColumnWithAction<T> {
  71. return Boolean((column as ColumnWithAction<T>).onClick);
  72. }
  73. function isIconColumn<T extends ResultWithId>(
  74. column: Column<T>,
  75. ): column is IconColumn<T> {
  76. return column.type === "icon";
  77. }
  78. function isDecimalColumn<T extends ResultWithId>(
  79. column: Column<T>,
  80. ): column is DecimalColumn<T> {
  81. return column.type === "decimal";
  82. }
  83. function isIntegerColumn<T extends ResultWithId>(
  84. column: Column<T>,
  85. ): column is IntegerColumn<T> {
  86. return column.type === "integer";
  87. }
  88. // Icon Component Functions
  89. function convertObjectKeysToLowercase<T extends object>(obj: T): object | undefined {
  90. return obj ? Object.fromEntries(
  91. Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value])
  92. ) : undefined;
  93. }
  94. function handleIconColors<T extends ResultWithId>(
  95. column: IconColumn<T>,
  96. value: T[keyof T],
  97. ): IconOwnProps["color"] {
  98. const colors = convertObjectKeysToLowercase(column.colors ?? {});
  99. const valueKey = String(value).toLowerCase() as keyof typeof colors;
  100. if (colors && valueKey in colors) {
  101. return colors[valueKey];
  102. }
  103. return column.color ?? "primary";
  104. };
  105. function handleIconIcons<T extends ResultWithId>(
  106. column: IconColumn<T>,
  107. value: T[keyof T],
  108. ): React.ReactNode {
  109. const icons = convertObjectKeysToLowercase(column.icons ?? {});
  110. const valueKey = String(value).toLowerCase() as keyof typeof icons;
  111. if (icons && valueKey in icons) {
  112. return icons[valueKey];
  113. }
  114. return column.icon ?? <CheckCircleOutlineIcon fontSize="small" />;
  115. };
  116. function SearchResults<T extends ResultWithId>({
  117. items,
  118. columns,
  119. noWrapper,
  120. pagingController,
  121. setPagingController,
  122. isAutoPaging = true,
  123. }: Props<T>) {
  124. const [page, setPage] = React.useState(0);
  125. const [rowsPerPage, setRowsPerPage] = React.useState(10);
  126. /// this
  127. const handleChangePage: TablePaginationProps["onPageChange"] = (
  128. _event,
  129. newPage,
  130. ) => {
  131. console.log(_event)
  132. setPage(newPage);
  133. if (setPagingController) {
  134. setPagingController({
  135. ...pagingController,
  136. pageNum: newPage + 1,
  137. })
  138. }
  139. }
  140. const handleChangeRowsPerPage: TablePaginationProps["onRowsPerPageChange"] = (
  141. event,
  142. ) => {
  143. console.log(event)
  144. setRowsPerPage(+event.target.value);
  145. setPage(0);
  146. if (setPagingController) {
  147. setPagingController({
  148. ...pagingController,
  149. pageNum: +event.target.value,
  150. })
  151. }
  152. };
  153. const table = (
  154. <>
  155. <TableContainer sx={{ maxHeight: 440 }}>
  156. <Table stickyHeader>
  157. <TableHead>
  158. <TableRow>
  159. {columns.map((column, idx) => (
  160. <TableCell align={column.headerAlign} sx={column.sx} key={`${column.name.toString()}${idx}`}>
  161. {column.label}
  162. </TableCell>
  163. ))}
  164. </TableRow>
  165. </TableHead>
  166. <TableBody>
  167. {
  168. isAutoPaging ?
  169. items
  170. .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
  171. .map((item) => {
  172. return (
  173. <TableRow hover tabIndex={-1} key={item.id}>
  174. {columns.map((column, idx) => {
  175. const columnName = column.name;
  176. return (
  177. <TabelCells key={`${columnName.toString()}-${idx}`} column={column} columnName={columnName} idx={idx} item={item}/>
  178. );
  179. })}
  180. </TableRow>
  181. );
  182. })
  183. :
  184. items
  185. .map((item) => {
  186. return (
  187. <TableRow hover tabIndex={-1} key={item.id}>
  188. {columns.map((column, idx) => {
  189. const columnName = column.name;
  190. return (
  191. <TabelCells key={`${columnName.toString()}-${idx}`} column={column} columnName={columnName} idx={idx} item={item}/>
  192. );
  193. })}
  194. </TableRow>
  195. );
  196. })
  197. }
  198. </TableBody>
  199. </Table>
  200. </TableContainer>
  201. <TablePagination
  202. rowsPerPageOptions={[10, 25, 100]}
  203. component="div"
  204. count={!pagingController || pagingController.totalCount == 0 ? items.length : pagingController.totalCount}
  205. rowsPerPage={rowsPerPage}
  206. page={page}
  207. onPageChange={handleChangePage}
  208. onRowsPerPageChange={handleChangeRowsPerPage}
  209. />
  210. </>
  211. );
  212. return noWrapper ? table : <Paper sx={{ overflow: "hidden" }}>{table}</Paper>;
  213. }
  214. // Table cells
  215. interface TableCellsProps<T extends ResultWithId> {
  216. column: Column<T>,
  217. columnName: keyof T,
  218. idx: number,
  219. item: T,
  220. }
  221. function TabelCells<T extends ResultWithId>({
  222. column,
  223. columnName,
  224. idx,
  225. item
  226. }: TableCellsProps<T>) {
  227. return (
  228. <TableCell align={column.align} sx={column.sx} key={`${columnName.toString()}-${idx}`}>
  229. {isActionColumn(column) ? (
  230. <IconButton
  231. color={column.buttonColor ?? "primary"}
  232. onClick={() => column.onClick(item)}
  233. >
  234. {column.buttonIcon}
  235. </IconButton>
  236. ) :
  237. isIconColumn(column) ? (
  238. <Icon
  239. color={handleIconColors(column, item[columnName])}
  240. >
  241. {handleIconIcons(column, item[columnName])}
  242. </Icon>
  243. ) :
  244. isDecimalColumn(column) ? (
  245. <>{decimalFormatter.format(Number(item[columnName]))}</>
  246. ) :
  247. isIntegerColumn(column) ? (
  248. <>{integerFormatter.format(Number(item[columnName]))}</>
  249. ) :
  250. (
  251. column.renderCell ? column.renderCell(item) : <>{item[columnName] as string}</>
  252. )}
  253. </TableCell>)
  254. }
  255. export default SearchResults;