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.
 
 
 

480 wiersze
14 KiB

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