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.
 
 

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