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

PoSearch.tsx 11 KiB

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