FPSMS-frontend
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 

394 wiersze
11 KiB

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