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.
 
 

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