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

PoSearch.tsx 11 KiB

4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
2週間前
3週間前
6ヶ月前
6ヶ月前
3ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3週間前
4ヶ月前
5ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
2週間前
6ヶ月前
2週間前
6ヶ月前
2週間前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. "use client";
  2. import { PoResult } from "@/app/api/po";
  3. import React, { useCallback, useEffect, useMemo, useState } from "react";
  4. import { useTranslation } from "react-i18next";
  5. import { useRouter, useSearchParams } from "next/navigation";
  6. import SearchBox, { Criterion } from "../SearchBox";
  7. import SearchResults, { Column } from "../SearchResults";
  8. import { EditNote } from "@mui/icons-material";
  9. import { Button, Grid, Tab, Tabs, TabsProps, Typography } from "@mui/material";
  10. import QrModal from "../PoDetail/QrModal";
  11. import { WarehouseResult } from "@/app/api/warehouse";
  12. import NotificationIcon from "@mui/icons-material/NotificationImportant";
  13. import { useSession } from "next-auth/react";
  14. import { defaultPagingController } from "../SearchResults/SearchResults";
  15. import { fetchPoListClient, testing } from "@/app/api/po/actions";
  16. import dayjs from "dayjs";
  17. import { arrayToDateString, dayjsToDateString } from "@/app/utils/formatUtil";
  18. import arraySupport from "dayjs/plugin/arraySupport";
  19. import { Checkbox, Box } from "@mui/material";
  20. dayjs.extend(arraySupport);
  21. type Props = {
  22. po: PoResult[];
  23. warehouse: WarehouseResult[];
  24. totalCount: number;
  25. };
  26. type SearchQuery = Partial<Omit<PoResult, "id">>;
  27. type SearchParamNames = keyof SearchQuery;
  28. // cal offset (pageSize)
  29. // cal limit (pageSize)
  30. const PoSearch: React.FC<Props> = ({
  31. po,
  32. warehouse,
  33. totalCount: initTotalCount,
  34. }) => {
  35. const [selectedPoIds, setSelectedPoIds] = useState<number[]>([]);
  36. const [selectAll, setSelectAll] = useState(false);
  37. //const [filteredPo, setFilteredPo] = useState<PoResult[]>(po);
  38. const [filteredPo, setFilteredPo] = useState<PoResult[]>([]);
  39. const [filterArgs, setFilterArgs] = useState<Record<string, any>>({estimatedArrivalDate : dayjsToDateString(dayjs(), "input")});
  40. const { t } = useTranslation(["purchaseOrder", "dashboard"]);
  41. const router = useRouter();
  42. const [pagingController, setPagingController] = useState(
  43. defaultPagingController,
  44. );
  45. const [totalCount, setTotalCount] = useState(initTotalCount);
  46. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => {
  47. const searchCriteria: Criterion<SearchParamNames>[] = [
  48. { label: t("Supplier"), paramName: "supplier", type: "text" },
  49. { label: t("PO No."), paramName: "code", type: "text" },
  50. {
  51. label: t("Escalated"),
  52. paramName: "escalated",
  53. type: "select",
  54. options: [t("Escalated"), t("NotEscalated")],
  55. },
  56. { label: t("Order Date"), label2: t("Order Date To"), paramName: "orderDate", type: "dateRange" },
  57. {
  58. label: t("Status"),
  59. paramName: "status",
  60. type: "select-labelled",
  61. options: [
  62. { label: t(`pending`), value: `pending` },
  63. { label: t(`receiving`), value: `receiving` },
  64. { label: t(`completed`), value: `completed` },
  65. ],
  66. },
  67. { label: t("ETA"),
  68. label2: t("ETA To"),
  69. paramName: "estimatedArrivalDate",
  70. type: "dateRange",
  71. preFilledValue: {
  72. from: dayjsToDateString(dayjs(), "input"),
  73. to: dayjsToDateString(dayjs(), "input"),
  74. },
  75. },
  76. ];
  77. return searchCriteria;
  78. }, [t]);
  79. const onDetailClick = useCallback(
  80. (po: PoResult) => {
  81. setSelectedPoIds([]);
  82. setSelectAll(false);
  83. router.push(`/po/edit?id=${po.id}&start=true&selectedIds=${po.id}`);
  84. },
  85. [router],
  86. );
  87. const onDeleteClick = useCallback((po: PoResult) => {}, []);
  88. // handle single checkbox selection
  89. const handleSelectPo = useCallback((poId: number, checked: boolean) => {
  90. if (checked) {
  91. setSelectedPoIds(prev => [...prev, poId]);
  92. } else {
  93. setSelectedPoIds(prev => prev.filter(id => id !== poId));
  94. }
  95. }, []);
  96. // 处理全选
  97. const handleSelectAll = useCallback((checked: boolean) => {
  98. if (checked) {
  99. setSelectedPoIds(filteredPo.map(po => po.id));
  100. setSelectAll(true);
  101. } else {
  102. setSelectedPoIds([]);
  103. setSelectAll(false);
  104. }
  105. }, [filteredPo]);
  106. // navigate to PoDetail page
  107. const handleGoToPoDetail = useCallback(() => {
  108. if (selectedPoIds.length > 0) {
  109. const selectedIdsParam = selectedPoIds.join(',');
  110. const firstPoId = selectedPoIds[0];
  111. router.push(`/po/edit?id=${firstPoId}&start=true&selectedIds=${selectedIdsParam}`);
  112. }
  113. }, [selectedPoIds, router]);
  114. const itemColumn = useCallback((value: string | undefined) => {
  115. if (!value) {
  116. return <Grid>"N/A"</Grid>
  117. }
  118. const items = value.split(",")
  119. return items.map((item) => <Grid key={item}>{item}</Grid>)
  120. }, [])
  121. const columns = useMemo<Column<PoResult>[]>(
  122. () => [
  123. {
  124. name: "id" as keyof PoResult,
  125. label: "",
  126. renderCell: (params) => (
  127. <Checkbox
  128. checked={selectedPoIds.includes(params.id)}
  129. onChange={(e) => handleSelectPo(params.id, e.target.checked)}
  130. onClick={(e) => e.stopPropagation()}
  131. />
  132. ),
  133. width: 60,
  134. },
  135. {
  136. name: "id",
  137. label: t("Details"),
  138. onClick: onDetailClick,
  139. buttonIcon: <EditNote />,
  140. },
  141. {
  142. name: "code",
  143. label: `${t("PO No.")} ${t("&")}\n${t("Supplier")}`,
  144. renderCell: (params) => {
  145. return <>{params.code}<br/>{params.supplier}</>
  146. },
  147. },
  148. {
  149. name: "orderDate",
  150. label: `${t("Order Date")} ${t("&")}\n${t("ETA")}`,
  151. renderCell: (params) => {
  152. // return (
  153. // dayjs(params.estimatedArrivalDate)
  154. // .add(-1, "month")
  155. // .format(OUTPUT_DATE_FORMAT)
  156. // );
  157. return <>{arrayToDateString(params.orderDate)}<br/>{arrayToDateString(params.estimatedArrivalDate)}</>
  158. },
  159. },
  160. // {
  161. // name: "itemDetail",
  162. // label: t("Item Detail"),
  163. // renderCell: (params) => {
  164. // if (!params.itemDetail) {
  165. // return "N/A"
  166. // }
  167. // const items = params.itemDetail.split(",")
  168. // return items.map((item) => <Grid key={item}>{item}</Grid>)
  169. // },
  170. // },
  171. {
  172. name: "itemCode",
  173. label: t("Item Code"),
  174. renderCell: (params) => {
  175. return itemColumn(params.itemCode);
  176. },
  177. },
  178. {
  179. name: "itemName",
  180. label: t("Item Name"),
  181. renderCell: (params) => {
  182. return itemColumn(params.itemName);
  183. },
  184. },
  185. {
  186. name: "itemQty",
  187. label: t("Item Qty"),
  188. renderCell: (params) => {
  189. return itemColumn(params.itemQty);
  190. },
  191. },
  192. {
  193. name: "itemSumAcceptedQty",
  194. label: t("Item Accepted Qty"),
  195. renderCell: (params) => {
  196. return itemColumn(params.itemSumAcceptedQty);
  197. },
  198. },
  199. {
  200. name: "itemUom",
  201. label: t("Item Purchase UoM"),
  202. renderCell: (params) => {
  203. return itemColumn(params.itemUom);
  204. },
  205. },
  206. {
  207. name: "status",
  208. label: t("Status"),
  209. renderCell: (params) => {
  210. return t(`${params.status.toLowerCase()}`);
  211. },
  212. },
  213. {
  214. name: "escalated",
  215. label: t("Escalated"),
  216. renderCell: (params) => {
  217. // console.log(params.escalated);
  218. return params.escalated ? (
  219. <NotificationIcon color="warning" />
  220. ) : undefined;
  221. },
  222. },
  223. ],
  224. [selectedPoIds, handleSelectPo, onDetailClick, t], // only keep necessary dependencies
  225. );
  226. const onReset = useCallback(() => {
  227. setFilteredPo(po);
  228. }, [po]);
  229. const [isOpenScanner, setOpenScanner] = useState(false);
  230. const onOpenScanner = useCallback(() => {
  231. setOpenScanner(true);
  232. }, []);
  233. const onCloseScanner = useCallback(() => {
  234. setOpenScanner(false);
  235. }, []);
  236. const newPageFetch = useCallback(
  237. async (
  238. pagingController: Record<string, number>,
  239. filterArgs: Record<string, number>,
  240. ) => {
  241. console.log(pagingController);
  242. console.log(filterArgs);
  243. const params = {
  244. ...pagingController,
  245. ...filterArgs,
  246. };
  247. const res = await fetchPoListClient(params);
  248. // const res = await testing(params);
  249. if (res) {
  250. console.log(res);
  251. setFilteredPo(res.records);
  252. setTotalCount(res.total);
  253. }
  254. },
  255. [],
  256. );
  257. useEffect(() => {
  258. console.log(filteredPo)
  259. }, [filteredPo])
  260. /*
  261. useEffect(() => {
  262. newPageFetch(pagingController, filterArgs);
  263. }, [newPageFetch, pagingController, filterArgs]);
  264. */
  265. // when filteredPo changes, update select all state
  266. useEffect(() => {
  267. if (filteredPo.length > 0 && selectedPoIds.length === filteredPo.length) {
  268. setSelectAll(true);
  269. } else {
  270. setSelectAll(false);
  271. }
  272. }, [filteredPo, selectedPoIds]);
  273. return (
  274. <>
  275. <Grid container>
  276. <Grid item xs={8}>
  277. <Typography variant="h4" marginInlineEnd={2}>
  278. {t("Purchase Receipt")}
  279. </Typography>
  280. </Grid>
  281. <Grid item xs={4} display="flex" justifyContent="end" alignItems="end">
  282. <QrModal
  283. open={isOpenScanner}
  284. onClose={onCloseScanner}
  285. warehouse={warehouse}
  286. />
  287. <Button onClick={onOpenScanner}>{t("bind")}</Button>
  288. </Grid>
  289. </Grid>
  290. <>
  291. <SearchBox
  292. criteria={searchCriteria}
  293. onSearch={(query) => {
  294. console.log(query);
  295. setFilterArgs({
  296. code: query.code,
  297. supplier: query.supplier,
  298. status: query.status === "All" ? "" : query.status,
  299. escalated:
  300. query.escalated === "All"
  301. ? undefined
  302. : query.escalated === t("Escalated"),
  303. estimatedArrivalDate: query.estimatedArrivalDate === "Invalid Date" ? "" : query.estimatedArrivalDate,
  304. estimatedArrivalDateTo: query.estimatedArrivalDateTo === "Invalid Date" ? "" : query.estimatedArrivalDateTo,
  305. orderDate: query.orderDate === "Invalid Date" ? "" : query.orderDate,
  306. orderDateTo: query.orderDateTo === "Invalid Date" ? "" : query.orderDateTo,
  307. });
  308. setSelectedPoIds([]); // reset selected po ids
  309. setSelectAll(false); // reset select all
  310. }}
  311. onReset={onReset}
  312. />
  313. <SearchResults<PoResult>
  314. items={filteredPo}
  315. columns={columns}
  316. pagingController={pagingController}
  317. setPagingController={setPagingController}
  318. totalCount={totalCount}
  319. isAutoPaging={false}
  320. />
  321. {/* add select all and view selected button */}
  322. <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
  323. <Button
  324. variant="outlined"
  325. onClick={() => handleSelectAll(!selectAll)}
  326. startIcon={<Checkbox checked={selectAll} />}
  327. >
  328. {t("Select All")} ({selectedPoIds.length} / {filteredPo.length})
  329. </Button>
  330. <Button
  331. variant="contained"
  332. onClick={handleGoToPoDetail}
  333. disabled={selectedPoIds.length === 0}
  334. color="primary"
  335. >
  336. {t("View Selected")} ({selectedPoIds.length})
  337. </Button>
  338. </Box>
  339. </>
  340. </>
  341. );
  342. };
  343. export default PoSearch;