FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SearchResults.tsx 14 KiB

1 年之前
6 月之前
1 年之前
10 月之前
1 年之前
10 月之前
1 年之前
10 月之前
10 月之前
9 月之前
10 月之前
10 月之前
1 年之前
10 月之前
1 年之前
9 月之前
10 月之前
1 年之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
1 年之前
9 月之前
1 年之前
10 月之前
1 年之前
10 月之前
9 月之前
10 月之前
1 年之前
10 月之前
10 月之前
9 月之前
1 年之前
1 年之前
10 月之前
1 年之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
9 月之前
9 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
10 月之前
9 月之前
1 年之前
10 月之前
9 月之前
10 月之前
6 月之前
10 月之前
6 月之前
10 月之前
10 月之前
10 月之前
10 月之前
1 年之前
10 月之前
10 月之前
4 月之前
10 月之前
4 月之前
10 月之前
1 年之前
9 月之前
9 月之前
9 月之前
10 月之前
10 月之前
8 月之前
8 月之前
10 月之前
10 月之前
9 月之前
10 月之前
10 月之前
10 月之前
10 月之前
6 月之前
10 月之前
1 年之前
10 月之前
10 月之前
9 月之前
10 月之前
10 月之前
9 月之前
10 月之前
9 月之前
10 月之前
9 月之前
10 月之前
1 年之前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  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. }
  97. function isActionColumn<T extends ResultWithId>(
  98. column: Column<T>,
  99. ): column is ColumnWithAction<T> {
  100. return Boolean((column as ColumnWithAction<T>).onClick);
  101. }
  102. function isIconColumn<T extends ResultWithId>(
  103. column: Column<T>,
  104. ): column is IconColumn<T> {
  105. return column.type === "icon";
  106. }
  107. function isDecimalColumn<T extends ResultWithId>(
  108. column: Column<T>,
  109. ): column is DecimalColumn<T> {
  110. return column.type === "decimal";
  111. }
  112. function isIntegerColumn<T extends ResultWithId>(
  113. column: Column<T>,
  114. ): column is IntegerColumn<T> {
  115. return column.type === "integer";
  116. }
  117. function isCheckboxColumn<T extends ResultWithId>(
  118. column: Column<T>,
  119. ): column is CheckboxColumn<T> {
  120. return column.type === "checkbox";
  121. }
  122. // Icon Component Functions
  123. function convertObjectKeysToLowercase<T extends object>(
  124. obj: T,
  125. ): object | undefined {
  126. return obj
  127. ? Object.fromEntries(
  128. Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]),
  129. )
  130. : undefined;
  131. }
  132. function handleIconColors<T extends ResultWithId>(
  133. column: IconColumn<T>,
  134. value: T[keyof T],
  135. ): IconOwnProps["color"] {
  136. const colors = convertObjectKeysToLowercase(column.colors ?? {});
  137. const valueKey = String(value).toLowerCase() as keyof typeof colors;
  138. if (colors && valueKey in colors) {
  139. return colors[valueKey];
  140. }
  141. return column.color ?? "primary";
  142. }
  143. function handleIconIcons<T extends ResultWithId>(
  144. column: IconColumn<T>,
  145. value: T[keyof T],
  146. ): React.ReactNode {
  147. const icons = convertObjectKeysToLowercase(column.icons ?? {});
  148. const valueKey = String(value).toLowerCase() as keyof typeof icons;
  149. if (icons && valueKey in icons) {
  150. return icons[valueKey];
  151. }
  152. return column.icon ?? <CheckCircleOutlineIcon fontSize="small" />;
  153. }
  154. export const defaultPagingController: { pageNum: number; pageSize: number } = {
  155. pageNum: 1,
  156. pageSize: 10,
  157. };
  158. export type defaultSetPagingController = Dispatch<
  159. SetStateAction<{
  160. pageNum: number;
  161. pageSize: number;
  162. }>
  163. >
  164. function SearchResults<T extends ResultWithId>({
  165. items,
  166. columns,
  167. noWrapper,
  168. pagingController,
  169. setPagingController,
  170. isAutoPaging = true,
  171. totalCount,
  172. checkboxIds = [],
  173. setCheckboxIds = undefined,
  174. onRowClick = undefined,
  175. }: Props<T>) {
  176. const { t } = useTranslation("dashboard");
  177. const [page, setPage] = React.useState(0);
  178. const [rowsPerPage, setRowsPerPage] = React.useState(10);
  179. /// this
  180. const handleChangePage: TablePaginationProps["onPageChange"] = (
  181. _event,
  182. newPage,
  183. ) => {
  184. console.log(_event);
  185. setPage(newPage);
  186. if (setPagingController) {
  187. setPagingController({
  188. ...(pagingController ?? defaultPagingController),
  189. pageNum: newPage + 1,
  190. });
  191. }
  192. };
  193. const handleChangeRowsPerPage: TablePaginationProps["onRowsPerPageChange"] = (
  194. event,
  195. ) => {
  196. console.log(event);
  197. const newSize = +event.target.value;
  198. setRowsPerPage(newSize);
  199. setPage(0);
  200. if (setPagingController) {
  201. setPagingController({
  202. ...(pagingController ?? defaultPagingController),
  203. pageNum: 1,
  204. pageSize: newSize,
  205. });
  206. }
  207. };
  208. // checkbox
  209. const currItems = useMemo(() => {
  210. return items.length > 10 ? items
  211. .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
  212. .map((i) => i.id)
  213. : items.map((i) => i.id)
  214. }, [items, page, rowsPerPage])
  215. const currItemsWithChecked = useMemo(() => {
  216. return filter(checkboxIds, function (c) {
  217. return currItems.includes(c);
  218. })
  219. }, [checkboxIds, items, page, rowsPerPage])
  220. const handleRowClick = useCallback(
  221. (event: MouseEvent<unknown>, item: T, columns: Column<T>[]) => {
  222. // check is disabled or not
  223. let disabled = false;
  224. columns.forEach((col) => {
  225. if (isCheckboxColumn(col) && col.disabled) {
  226. disabled = col.disabled(item);
  227. if (disabled) {
  228. return;
  229. }
  230. }
  231. });
  232. if (disabled) {
  233. return;
  234. }
  235. // set id
  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>
  271. <TableHead>
  272. <TableRow>
  273. {columns.map((column, idx) => (
  274. isCheckboxColumn(column) ?
  275. <TableCell
  276. align={column.headerAlign}
  277. sx={column.sx}
  278. key={`${column.name.toString()}${idx}`}
  279. >
  280. <Checkbox
  281. color="primary"
  282. indeterminate={currItemsWithChecked.length > 0 && currItemsWithChecked.length < currItems.length}
  283. checked={currItems.length > 0 && currItemsWithChecked.length >= currItems.length}
  284. onChange={handleSelectAllClick}
  285. />
  286. </TableCell>
  287. : <TableCell
  288. align={column.headerAlign}
  289. sx={column.sx}
  290. key={`${column.name.toString()}${idx}`}
  291. >
  292. {column.label.split('\n').map((line, index) => (
  293. <div key={index}>{line}</div> // Render each line in a div
  294. ))}
  295. </TableCell>
  296. ))}
  297. </TableRow>
  298. </TableHead>
  299. <TableBody>
  300. {isAutoPaging
  301. ? items
  302. .slice((pagingController?.pageNum ? pagingController?.pageNum - 1 :page) * (pagingController?.pageSize ?? rowsPerPage),
  303. (pagingController?.pageNum ? pagingController?.pageNum - 1 :page) * (pagingController?.pageSize ?? rowsPerPage) + (pagingController?.pageSize ?? rowsPerPage))
  304. .map((item) => {
  305. return (
  306. <TableRow
  307. hover
  308. tabIndex={-1}
  309. key={item.id}
  310. onClick={(event) => {
  311. setCheckboxIds
  312. ? handleRowClick(event, item, columns)
  313. : undefined
  314. if (onRowClick) {
  315. onRowClick(item)
  316. }
  317. }
  318. }
  319. role={setCheckboxIds ? "checkbox" : undefined}
  320. >
  321. {columns.map((column, idx) => {
  322. const columnName = column.name;
  323. return (
  324. <TabelCells
  325. key={`${columnName.toString()}-${idx}`}
  326. column={column}
  327. columnName={columnName}
  328. idx={idx}
  329. item={item}
  330. checkboxIds={checkboxIds}
  331. />
  332. );
  333. })}
  334. </TableRow>
  335. );
  336. })
  337. : items.map((item) => {
  338. return (
  339. <TableRow hover tabIndex={-1} key={item.id}
  340. onClick={(event) => {
  341. setCheckboxIds
  342. ? handleRowClick(event, item, columns)
  343. : undefined
  344. if (onRowClick) {
  345. onRowClick(item)
  346. }
  347. }
  348. }
  349. role={setCheckboxIds ? "checkbox" : undefined}
  350. >
  351. {columns.map((column, idx) => {
  352. const columnName = column.name;
  353. return (
  354. <TabelCells
  355. key={`${columnName.toString()}-${idx}`}
  356. column={column}
  357. columnName={columnName}
  358. idx={idx}
  359. item={item}
  360. checkboxIds={checkboxIds}
  361. />
  362. );
  363. })}
  364. </TableRow>
  365. );
  366. })}
  367. </TableBody>
  368. </Table>
  369. </TableContainer>
  370. <TablePagination
  371. rowsPerPageOptions={[10, 25, 100]}
  372. component="div"
  373. count={!totalCount || totalCount == 0 ? items.length : totalCount}
  374. rowsPerPage={pagingController?.pageSize ? pagingController?.pageSize : rowsPerPage}
  375. page={pagingController?.pageNum ? pagingController?.pageNum - 1 : page}
  376. onPageChange={handleChangePage}
  377. onRowsPerPageChange={handleChangeRowsPerPage}
  378. labelRowsPerPage={t("Rows per page")}
  379. labelDisplayedRows={({ from, to, count }) =>
  380. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  381. }
  382. />
  383. </>
  384. );
  385. return noWrapper ? table : <Paper variant="outlined" sx={{ overflow: "hidden" }}>{table}</Paper>;
  386. }
  387. // Table cells
  388. interface TableCellsProps<T extends ResultWithId> {
  389. column: Column<T>;
  390. columnName: keyof T;
  391. idx: number;
  392. item: T;
  393. checkboxIds: (string | number)[];
  394. }
  395. function TabelCells<T extends ResultWithId>({
  396. column,
  397. columnName,
  398. idx,
  399. item,
  400. checkboxIds = [],
  401. }: TableCellsProps<T>) {
  402. const isItemSelected = checkboxIds.includes(item.id);
  403. return (
  404. <TableCell
  405. align={column.align}
  406. sx={column.sx}
  407. key={`${columnName.toString()}-${idx}`}
  408. >
  409. {isActionColumn(column) ? (
  410. <IconButton
  411. color={column.buttonColor ?? "primary"}
  412. onClick={() => column.onClick(item)}
  413. >
  414. {column.buttonIcon}
  415. </IconButton>
  416. ) : isIconColumn(column) ? (
  417. <Icon color={handleIconColors(column, item[columnName])}>
  418. {handleIconIcons(column, item[columnName])}
  419. </Icon>
  420. ) : isDecimalColumn(column) ? (
  421. <>{decimalFormatter.format(Number(item[columnName]))}</>
  422. ) : isIntegerColumn(column) ? (
  423. <>{integerFormatter.format(Number(item[columnName]))}</>
  424. ) : isCheckboxColumn(column) ? (
  425. <Checkbox
  426. disabled={column.disabled ? column.disabled(item) : undefined}
  427. checked={isItemSelected}
  428. />
  429. ) : column.renderCell ? (
  430. column.renderCell(item)
  431. ) : (
  432. <>{item[columnName] as string}</>
  433. )}
  434. </TableCell>
  435. );
  436. }
  437. export default SearchResults;