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

SearchResults.tsx 12 KiB

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