FPSMS-frontend
Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

425 linhas
12 KiB

  1. "use client";
  2. import React, {
  3. ChangeEvent,
  4. Dispatch,
  5. MouseEvent,
  6. SetStateAction,
  7. useCallback,
  8. useState,
  9. } from "react";
  10. import Paper from "@mui/material/Paper";
  11. import Table from "@mui/material/Table";
  12. import TableBody from "@mui/material/TableBody";
  13. import TableCell, { TableCellProps } from "@mui/material/TableCell";
  14. import TableContainer from "@mui/material/TableContainer";
  15. import TableHead from "@mui/material/TableHead";
  16. import TablePagination, {
  17. TablePaginationProps,
  18. } from "@mui/material/TablePagination";
  19. import TableRow from "@mui/material/TableRow";
  20. import IconButton, { IconButtonOwnProps } from "@mui/material/IconButton";
  21. import {
  22. ButtonOwnProps,
  23. Checkbox,
  24. Icon,
  25. IconOwnProps,
  26. SxProps,
  27. Theme,
  28. } from "@mui/material";
  29. import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
  30. import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
  31. export interface ResultWithId {
  32. id: string | number;
  33. }
  34. type ColumnType = "icon" | "decimal" | "integer" | "checkbox";
  35. interface BaseColumn<T extends ResultWithId> {
  36. name: keyof T;
  37. label: string;
  38. align?: TableCellProps["align"];
  39. headerAlign?: TableCellProps["align"];
  40. sx?: SxProps<Theme> | undefined;
  41. style?: Partial<HTMLElement["style"]> & { [propName: string]: string };
  42. type?: ColumnType;
  43. renderCell?: (params: T) => React.ReactNode;
  44. }
  45. interface IconColumn<T extends ResultWithId> extends BaseColumn<T> {
  46. name: keyof T;
  47. type: "icon";
  48. icon?: React.ReactNode;
  49. icons?: { [columnValue in keyof T]: React.ReactNode };
  50. color?: IconOwnProps["color"];
  51. colors?: { [columnValue in keyof T]: IconOwnProps["color"] };
  52. }
  53. interface DecimalColumn<T extends ResultWithId> extends BaseColumn<T> {
  54. type: "decimal";
  55. }
  56. interface IntegerColumn<T extends ResultWithId> extends BaseColumn<T> {
  57. type: "integer";
  58. }
  59. interface CheckboxColumn<T extends ResultWithId> extends BaseColumn<T> {
  60. type: "checkbox";
  61. disabled?: (params: T) => boolean;
  62. // checkboxIds: readonly (string | number)[],
  63. // setCheckboxIds: (ids: readonly (string | number)[]) => void
  64. }
  65. interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> {
  66. onClick: (item: T) => void;
  67. buttonIcon: React.ReactNode;
  68. buttonIcons: { [columnValue in keyof T]: React.ReactNode };
  69. buttonColor?: IconButtonOwnProps["color"];
  70. }
  71. export type Column<T extends ResultWithId> =
  72. | BaseColumn<T>
  73. | IconColumn<T>
  74. | DecimalColumn<T>
  75. | CheckboxColumn<T>
  76. | ColumnWithAction<T>;
  77. interface Props<T extends ResultWithId> {
  78. totalCount?: number;
  79. items: T[];
  80. columns: Column<T>[];
  81. noWrapper?: boolean;
  82. setPagingController?: Dispatch<
  83. SetStateAction<{
  84. pageNum: number;
  85. pageSize: number;
  86. }>
  87. >;
  88. pagingController?: { pageNum: number; pageSize: number };
  89. isAutoPaging?: boolean;
  90. checkboxIds?: (string | number)[];
  91. setCheckboxIds?: Dispatch<SetStateAction<(string | number)[]>>;
  92. onRowClick?: (item: T) => void;
  93. }
  94. function isActionColumn<T extends ResultWithId>(
  95. column: Column<T>,
  96. ): column is ColumnWithAction<T> {
  97. return Boolean((column as ColumnWithAction<T>).onClick);
  98. }
  99. function isIconColumn<T extends ResultWithId>(
  100. column: Column<T>,
  101. ): column is IconColumn<T> {
  102. return column.type === "icon";
  103. }
  104. function isDecimalColumn<T extends ResultWithId>(
  105. column: Column<T>,
  106. ): column is DecimalColumn<T> {
  107. return column.type === "decimal";
  108. }
  109. function isIntegerColumn<T extends ResultWithId>(
  110. column: Column<T>,
  111. ): column is IntegerColumn<T> {
  112. return column.type === "integer";
  113. }
  114. function isCheckboxColumn<T extends ResultWithId>(
  115. column: Column<T>,
  116. ): column is CheckboxColumn<T> {
  117. return column.type === "checkbox";
  118. }
  119. // Icon Component Functions
  120. function convertObjectKeysToLowercase<T extends object>(
  121. obj: T,
  122. ): object | undefined {
  123. return obj
  124. ? Object.fromEntries(
  125. Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]),
  126. )
  127. : undefined;
  128. }
  129. function handleIconColors<T extends ResultWithId>(
  130. column: IconColumn<T>,
  131. value: T[keyof T],
  132. ): IconOwnProps["color"] {
  133. const colors = convertObjectKeysToLowercase(column.colors ?? {});
  134. const valueKey = String(value).toLowerCase() as keyof typeof colors;
  135. if (colors && valueKey in colors) {
  136. return colors[valueKey];
  137. }
  138. return column.color ?? "primary";
  139. }
  140. function handleIconIcons<T extends ResultWithId>(
  141. column: IconColumn<T>,
  142. value: T[keyof T],
  143. ): React.ReactNode {
  144. const icons = convertObjectKeysToLowercase(column.icons ?? {});
  145. const valueKey = String(value).toLowerCase() as keyof typeof icons;
  146. if (icons && valueKey in icons) {
  147. return icons[valueKey];
  148. }
  149. return column.icon ?? <CheckCircleOutlineIcon fontSize="small" />;
  150. }
  151. export const defaultPagingController: { pageNum: number; pageSize: number } = {
  152. pageNum: 1,
  153. pageSize: 10,
  154. };
  155. export type defaultSetPagingController = Dispatch<
  156. SetStateAction<{
  157. pageNum: number;
  158. pageSize: number;
  159. }>
  160. >
  161. function SearchResults<T extends ResultWithId>({
  162. items,
  163. columns,
  164. noWrapper,
  165. pagingController,
  166. setPagingController,
  167. isAutoPaging = true,
  168. totalCount,
  169. checkboxIds = [],
  170. setCheckboxIds = undefined,
  171. onRowClick = undefined,
  172. }: Props<T>) {
  173. const [page, setPage] = React.useState(0);
  174. const [rowsPerPage, setRowsPerPage] = React.useState(10);
  175. /// this
  176. const handleChangePage: TablePaginationProps["onPageChange"] = (
  177. _event,
  178. newPage,
  179. ) => {
  180. console.log(_event);
  181. setPage(newPage);
  182. if (setPagingController) {
  183. setPagingController({
  184. ...(pagingController ?? defaultPagingController),
  185. pageNum: newPage + 1,
  186. });
  187. }
  188. };
  189. const handleChangeRowsPerPage: TablePaginationProps["onRowsPerPageChange"] = (
  190. event,
  191. ) => {
  192. console.log(event);
  193. setRowsPerPage(+event.target.value);
  194. setPage(0);
  195. if (setPagingController) {
  196. setPagingController({
  197. ...(pagingController ?? defaultPagingController),
  198. pageNum: 1,
  199. });
  200. }
  201. };
  202. // checkbox
  203. const handleRowClick = useCallback(
  204. (event: MouseEvent<unknown>, item: T, columns: Column<T>[]) => {
  205. // check is disabled or not
  206. let disabled = false;
  207. columns.forEach((col) => {
  208. if (isCheckboxColumn(col) && col.disabled) {
  209. disabled = col.disabled(item);
  210. if (disabled) {
  211. return;
  212. }
  213. }
  214. });
  215. if (disabled) {
  216. return;
  217. }
  218. // set id
  219. const id = item.id;
  220. if (setCheckboxIds) {
  221. const selectedIndex = checkboxIds.indexOf(id);
  222. let newSelected: (string | number)[] = [];
  223. if (selectedIndex === -1) {
  224. newSelected = newSelected.concat(checkboxIds, id);
  225. } else if (selectedIndex === 0) {
  226. newSelected = newSelected.concat(checkboxIds.slice(1));
  227. } else if (selectedIndex === checkboxIds.length - 1) {
  228. newSelected = newSelected.concat(checkboxIds.slice(0, -1));
  229. } else if (selectedIndex > 0) {
  230. newSelected = newSelected.concat(
  231. checkboxIds.slice(0, selectedIndex),
  232. checkboxIds.slice(selectedIndex + 1),
  233. );
  234. }
  235. setCheckboxIds(newSelected);
  236. }
  237. },
  238. [checkboxIds],
  239. );
  240. const table = (
  241. <>
  242. <TableContainer sx={{ maxHeight: 440 }}>
  243. <Table stickyHeader>
  244. <TableHead>
  245. <TableRow>
  246. {columns.map((column, idx) => (
  247. <TableCell
  248. align={column.headerAlign}
  249. sx={column.sx}
  250. key={`${column.name.toString()}${idx}`}
  251. >
  252. {column.label}
  253. </TableCell>
  254. ))}
  255. </TableRow>
  256. </TableHead>
  257. <TableBody>
  258. {isAutoPaging
  259. ? items
  260. .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
  261. .map((item) => {
  262. return (
  263. <TableRow
  264. hover
  265. tabIndex={-1}
  266. key={item.id}
  267. onClick={(event) => {
  268. setCheckboxIds
  269. ? handleRowClick(event, item, columns)
  270. : undefined
  271. if (onRowClick) {
  272. onRowClick(item)
  273. }
  274. }
  275. }
  276. role={setCheckboxIds ? "checkbox" : undefined}
  277. >
  278. {columns.map((column, idx) => {
  279. const columnName = column.name;
  280. return (
  281. <TabelCells
  282. key={`${columnName.toString()}-${idx}`}
  283. column={column}
  284. columnName={columnName}
  285. idx={idx}
  286. item={item}
  287. checkboxIds={checkboxIds}
  288. />
  289. );
  290. })}
  291. </TableRow>
  292. );
  293. })
  294. : items.map((item) => {
  295. return (
  296. <TableRow hover tabIndex={-1} key={item.id}>
  297. {columns.map((column, idx) => {
  298. const columnName = column.name;
  299. return (
  300. <TabelCells
  301. key={`${columnName.toString()}-${idx}`}
  302. column={column}
  303. columnName={columnName}
  304. idx={idx}
  305. item={item}
  306. checkboxIds={checkboxIds}
  307. />
  308. );
  309. })}
  310. </TableRow>
  311. );
  312. })}
  313. </TableBody>
  314. </Table>
  315. </TableContainer>
  316. <TablePagination
  317. rowsPerPageOptions={[10, 25, 100]}
  318. component="div"
  319. count={!totalCount || totalCount == 0 ? items.length : totalCount}
  320. // count={
  321. // !pagingController || pagingController.totalCount == 0
  322. // ? items.length
  323. // : pagingController.totalCount
  324. // }
  325. rowsPerPage={rowsPerPage}
  326. page={page}
  327. onPageChange={handleChangePage}
  328. onRowsPerPageChange={handleChangeRowsPerPage}
  329. />
  330. </>
  331. );
  332. return noWrapper ? table : <Paper sx={{ overflow: "hidden" }}>{table}</Paper>;
  333. }
  334. // Table cells
  335. interface TableCellsProps<T extends ResultWithId> {
  336. column: Column<T>;
  337. columnName: keyof T;
  338. idx: number;
  339. item: T;
  340. checkboxIds: (string | number)[];
  341. }
  342. function TabelCells<T extends ResultWithId>({
  343. column,
  344. columnName,
  345. idx,
  346. item,
  347. checkboxIds = [],
  348. }: TableCellsProps<T>) {
  349. const isItemSelected = checkboxIds.includes(item.id);
  350. return (
  351. <TableCell
  352. align={column.align}
  353. sx={column.sx}
  354. key={`${columnName.toString()}-${idx}`}
  355. >
  356. {isActionColumn(column) ? (
  357. <IconButton
  358. color={column.buttonColor ?? "primary"}
  359. onClick={() => column.onClick(item)}
  360. >
  361. {column.buttonIcon}
  362. </IconButton>
  363. ) : isIconColumn(column) ? (
  364. <Icon color={handleIconColors(column, item[columnName])}>
  365. {handleIconIcons(column, item[columnName])}
  366. </Icon>
  367. ) : isDecimalColumn(column) ? (
  368. <>{decimalFormatter.format(Number(item[columnName]))}</>
  369. ) : isIntegerColumn(column) ? (
  370. <>{integerFormatter.format(Number(item[columnName]))}</>
  371. ) : isCheckboxColumn(column) ? (
  372. <Checkbox
  373. disabled={column.disabled ? column.disabled(item) : undefined}
  374. checked={isItemSelected}
  375. />
  376. ) : column.renderCell ? (
  377. column.renderCell(item)
  378. ) : (
  379. <>{item[columnName] as string}</>
  380. )}
  381. </TableCell>
  382. );
  383. }
  384. export default SearchResults;