FPSMS-frontend
Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

356 linhas
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", "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;