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.
 
 

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