FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

SearchResults.tsx 14 KiB

5ヶ月前
10ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
10ヶ月前
10ヶ月前
9ヶ月前
10ヶ月前
10ヶ月前
9ヶ月前
10ヶ月前
10ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
10ヶ月前
5ヶ月前
9ヶ月前
5ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
4ヶ月前
9ヶ月前
4ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
8ヶ月前
8ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
9ヶ月前
5ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
9ヶ月前
10ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
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;