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.
 
 

358 line
11 KiB

  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;