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.

PickExecutionIssuesTab.tsx 14 KiB

11 maanden geleden
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 maanden geleden
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 maanden geleden
​
​
​
​
​
​
​
11 maanden geleden
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 maanden geleden
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 maanden geleden
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. "use client";
  2. import {
  3. Box,
  4. Button,
  5. Chip,
  6. CircularProgress,
  7. Dialog,
  8. DialogTitle,
  9. DialogContent,
  10. DialogActions,
  11. Paper,
  12. Stack,
  13. Table,
  14. TableBody,
  15. TableCell,
  16. TableContainer,
  17. TableHead,
  18. TablePagination,
  19. TableRow,
  20. TextField,
  21. Typography,
  22. } from "@mui/material";
  23. import { useCallback, useEffect, useState, useMemo } from "react";
  24. import { useTranslation } from "react-i18next";
  25. import SearchBox, { Criterion } from "../SearchBox";
  26. import SearchResults, { Column } from "../SearchResults";
  27. import { defaultPagingController } from "../SearchResults/SearchResults";
  28. import {
  29. fetchAllPickExecutionIssues,
  30. PickExecutionIssue,
  31. updatePickExecutionIssueStatus,
  32. FetchPickExecutionIssuesParams,
  33. } from "@/app/api/pickOrder/actions";
  34. import dayjs from "dayjs";
  35. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  36. type SearchQuery = {
  37. issueNo: string;
  38. pickOrderCode: string;
  39. itemCode: string;
  40. lotNo: string;
  41. type: string;
  42. status: string;
  43. };
  44. type SearchParamNames = keyof SearchQuery;
  45. const PickExecutionIssuesTab: React.FC = () => {
  46. const { t } = useTranslation(["stockTake", "pickOrder"]);
  47. // States
  48. const [issues, setIssues] = useState<PickExecutionIssue[]>([]);
  49. const [loading, setLoading] = useState(false);
  50. const [pagingController, setPagingController] = useState(defaultPagingController);
  51. const [totalCount, setTotalCount] = useState(0);
  52. // Search inputs
  53. const defaultInputs = useMemo(() => ({
  54. issueNo: "",
  55. pickOrderCode: "",
  56. itemCode: "",
  57. lotNo: "",
  58. type: "",
  59. status: "",
  60. }), []);
  61. const [inputs, setInputs] = useState<Record<SearchParamNames, string>>(defaultInputs);
  62. // Handle dialog
  63. const [selectedIssue, setSelectedIssue] = useState<PickExecutionIssue | null>(null);
  64. const [handleDialogOpen, setHandleDialogOpen] = useState(false);
  65. const [handleRemark, setHandleRemark] = useState("");
  66. // Search criteria
  67. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
  68. () => [
  69. { label: t("Issue No"), paramName: "issueNo", type: "text" },
  70. { label: t("Pick Order Code"), paramName: "pickOrderCode", type: "text" },
  71. { label: t("Item Code"), paramName: "itemCode", type: "text" },
  72. { label: t("Lot No"), paramName: "lotNo", type: "text" },
  73. {
  74. label: t("Type"),
  75. paramName: "type",
  76. type: "select-labelled",
  77. options: [
  78. { value: "jo", label: t("jo") },
  79. { value: "do", label: t("do") },
  80. { value: "material", label: t("material") },
  81. ],
  82. },
  83. {
  84. label: t("Status"),
  85. paramName: "status",
  86. type: "select-labelled",
  87. options: [
  88. { value: "pending", label: t("pending") },
  89. { value: "resolved", label: t("resolved") },
  90. ],
  91. },
  92. ],
  93. [t],
  94. );
  95. // Fetch issues
  96. const refetchIssuesData = useCallback(async (
  97. query: Record<SearchParamNames, string>,
  98. actionType: "reset" | "search" | "paging" | "init",
  99. pagingController: typeof defaultPagingController,
  100. ) => {
  101. console.log("%c Action Type.", "color:red", actionType);
  102. setLoading(true);
  103. try {
  104. // Build API parameters with proper type casting
  105. let apiParams: FetchPickExecutionIssuesParams | undefined = undefined;
  106. if (query.type && query.type !== "All") {
  107. // Type assertion to ensure the string is one of the valid types
  108. const validType = query.type as "jo" | "do" | "material";
  109. apiParams = { type: validType };
  110. }
  111. // Call API with type parameter if specified
  112. const result = await fetchAllPickExecutionIssues(apiParams);
  113. // Apply client-side filtering for other parameters
  114. let filteredResult = result;
  115. if (query.issueNo) {
  116. filteredResult = filteredResult.filter(issue =>
  117. issue.issueNo.toLowerCase().includes(query.issueNo.toLowerCase())
  118. );
  119. }
  120. if (query.pickOrderCode) {
  121. filteredResult = filteredResult.filter(issue =>
  122. issue.pickOrderCode.toLowerCase().includes(query.pickOrderCode.toLowerCase())
  123. );
  124. }
  125. if (query.itemCode) {
  126. filteredResult = filteredResult.filter(issue =>
  127. issue.itemCode.toLowerCase().includes(query.itemCode.toLowerCase())
  128. );
  129. }
  130. if (query.lotNo) {
  131. filteredResult = filteredResult.filter(issue =>
  132. issue.lotNo && issue.lotNo.toLowerCase().includes(query.lotNo.toLowerCase())
  133. );
  134. }
  135. if (query.status && query.status !== "All") {
  136. filteredResult = filteredResult.filter(issue => {
  137. if (query.status === "pending") {
  138. return issue.handleStatus.toLowerCase() === "pending";
  139. }
  140. if (query.status === "resolved") {
  141. return issue.handleStatus.toLowerCase() !== "pending";
  142. }
  143. return true;
  144. });
  145. }
  146. setIssues(filteredResult);
  147. setTotalCount(filteredResult.length);
  148. } catch (error) {
  149. console.error("Error fetching pick execution issues:", error);
  150. setIssues([]);
  151. setTotalCount(0);
  152. } finally {
  153. setLoading(false);
  154. }
  155. }, []);
  156. useEffect(() => {
  157. refetchIssuesData(defaultInputs, "init", defaultPagingController);
  158. }, [refetchIssuesData, defaultInputs]);
  159. useEffect(() => {
  160. refetchIssuesData(inputs, "paging", pagingController);
  161. }, [pagingController, refetchIssuesData, inputs]);
  162. // Handlers
  163. const onReset = useCallback(() => {
  164. refetchIssuesData(defaultInputs, "reset", defaultPagingController);
  165. setInputs(() => defaultInputs);
  166. setPagingController(() => defaultPagingController);
  167. }, [refetchIssuesData, defaultInputs]);
  168. const onSearch = useCallback((query: Record<SearchParamNames, string>) => {
  169. refetchIssuesData(query, "search", defaultPagingController);
  170. setInputs(() => query);
  171. setPagingController(() => defaultPagingController);
  172. }, [refetchIssuesData]);
  173. const handleViewDetail = (issue: PickExecutionIssue) => {
  174. setSelectedIssue(issue);
  175. setHandleDialogOpen(true);
  176. setHandleRemark("");
  177. };
  178. const handleCloseDialog = () => {
  179. setHandleDialogOpen(false);
  180. setSelectedIssue(null);
  181. setHandleRemark("");
  182. };
  183. const handleMarkAsResolved = async () => {
  184. if (!selectedIssue) return;
  185. try {
  186. await updatePickExecutionIssueStatus({
  187. issueId: selectedIssue.id,
  188. handleStatus: "resolved",
  189. handleDate: new Date().toISOString(),
  190. handleRemark: handleRemark,
  191. });
  192. // Refresh data
  193. await refetchIssuesData(inputs, "search", pagingController);
  194. handleCloseDialog();
  195. } catch (error) {
  196. console.error("Error updating issue status:", error);
  197. }
  198. };
  199. const getIssueCategoryColor = (category: string) => {
  200. switch (category.toLowerCase()) {
  201. case "lot_issue":
  202. return "error";
  203. case "quality_issue":
  204. return "warning";
  205. case "quantity_mismatch":
  206. return "info";
  207. default:
  208. return "default";
  209. }
  210. };
  211. // Table columns
  212. const columns: Column<PickExecutionIssue>[] = useMemo(
  213. () => [
  214. { name: "issueNo", label: t("Issue No") },
  215. { name: "pickOrderCode", label: t("Pick Order") },
  216. {
  217. name: "itemCode",
  218. label: t("Item"),
  219. renderCell: (params) => (
  220. <Box>
  221. <Typography variant="body2">{params.itemCode}</Typography>
  222. <Typography variant="caption" color="text.secondary">
  223. {params.itemDescription}
  224. </Typography>
  225. </Box>
  226. ),
  227. },
  228. {
  229. name: "lotNo",
  230. label: t("Lot No"),
  231. renderCell: (params) => params.lotNo || "-"
  232. },
  233. { name: "requiredQty", label: t("Required Qty"), align: "right", type: "integer" },
  234. {
  235. name: "actualPickQty",
  236. label: t("Actual Qty"),
  237. align: "right",
  238. type: "integer",
  239. renderCell: (params) => params.actualPickQty || 0
  240. },
  241. {
  242. name: "missQty",
  243. label: t("Miss Qty"),
  244. align: "right",
  245. type: "integer",
  246. renderCell: (params) => params.missQty || 0
  247. },
  248. {
  249. name: "issueCategory",
  250. label: t("Category"),
  251. renderCell: (params) => (
  252. <Chip
  253. label={t(params.issueCategory)}
  254. size="small"
  255. color={getIssueCategoryColor(params.issueCategory)}
  256. />
  257. ),
  258. },
  259. {
  260. name: "handleStatus",
  261. label: t("Status"),
  262. renderCell: (params) => (
  263. <Chip
  264. label={t(params.handleStatus)}
  265. size="small"
  266. color={
  267. params.handleStatus.toLowerCase() === "pending"
  268. ? "warning"
  269. : "success"
  270. }
  271. />
  272. ),
  273. },
  274. {
  275. name: "pickExecutionDate",
  276. label: t("Date"),
  277. renderCell: (params) => dayjs(params.pickExecutionDate).format(OUTPUT_DATE_FORMAT),
  278. },
  279. {
  280. name: "id",
  281. label: t("Action"),
  282. renderCell: (params) => (
  283. <Button size="small" onClick={() => handleViewDetail(params)}>
  284. {t("Detail")}
  285. </Button>
  286. ),
  287. },
  288. ],
  289. [t, handleViewDetail],
  290. );
  291. return (
  292. <Box>
  293. <SearchBox
  294. criteria={searchCriteria}
  295. onSearch={onSearch}
  296. onReset={onReset}
  297. />
  298. <SearchResults<PickExecutionIssue>
  299. items={issues}
  300. columns={columns}
  301. pagingController={pagingController}
  302. setPagingController={setPagingController}
  303. totalCount={totalCount}
  304. />
  305. {/* Handle Issue Dialog */}
  306. <Dialog open={handleDialogOpen} onClose={handleCloseDialog} maxWidth="md" fullWidth>
  307. <DialogTitle>{t("Issue Detail")}</DialogTitle>
  308. <DialogContent>
  309. {selectedIssue && (
  310. <Stack spacing={2} sx={{ mt: 2 }}>
  311. <Box>
  312. <Typography variant="subtitle2" color="text.secondary">
  313. {t("Issue No")}
  314. </Typography>
  315. <Typography variant="body1">{selectedIssue.issueNo}</Typography>
  316. </Box>
  317. <Box>
  318. <Typography variant="subtitle2" color="text.secondary">
  319. {t("Pick Order")}
  320. </Typography>
  321. <Typography variant="body1">{selectedIssue.pickOrderCode}</Typography>
  322. </Box>
  323. <Box>
  324. <Typography variant="subtitle2" color="text.secondary">
  325. {t("Item")}
  326. </Typography>
  327. <Typography variant="body1">
  328. {selectedIssue.itemCode} - {selectedIssue.itemDescription}
  329. </Typography>
  330. </Box>
  331. {selectedIssue.lotNo && (
  332. <Box>
  333. <Typography variant="subtitle2" color="text.secondary">
  334. {t("Lot No")}
  335. </Typography>
  336. <Typography variant="body1">{selectedIssue.lotNo}</Typography>
  337. </Box>
  338. )}
  339. <Box>
  340. <Typography variant="subtitle2" color="text.secondary">
  341. {t("Location")}
  342. </Typography>
  343. <Typography variant="body1">{selectedIssue.storeLocation || "-"}</Typography>
  344. </Box>
  345. <Stack direction="row" spacing={3}>
  346. <Box>
  347. <Typography variant="subtitle2" color="text.secondary">
  348. {t("Required Qty")}
  349. </Typography>
  350. <Typography variant="body1">{selectedIssue.requiredQty}</Typography>
  351. </Box>
  352. <Box>
  353. <Typography variant="subtitle2" color="text.secondary">
  354. {t("Actual Qty")}
  355. </Typography>
  356. <Typography variant="body1">{selectedIssue.actualPickQty || 0}</Typography>
  357. </Box>
  358. <Box>
  359. <Typography variant="subtitle2" color="text.secondary">
  360. {t("Miss Qty")}
  361. </Typography>
  362. <Typography variant="body1" color="error">
  363. {selectedIssue.missQty || 0}
  364. </Typography>
  365. </Box>
  366. </Stack>
  367. {selectedIssue.issueRemark && (
  368. <Box>
  369. <Typography variant="subtitle2" color="text.secondary">
  370. {t("Issue Remark")}
  371. </Typography>
  372. <Typography variant="body1">{selectedIssue.issueRemark}</Typography>
  373. </Box>
  374. )}
  375. <Box>
  376. <Typography variant="subtitle2" color="text.secondary">
  377. {t("Picker")}
  378. </Typography>
  379. <Typography variant="body1">{selectedIssue.pickerName || "-"}</Typography>
  380. </Box>
  381. <Box>
  382. <Typography variant="subtitle2" color="text.secondary">
  383. {t("Status")}
  384. </Typography>
  385. <Chip
  386. label={t(selectedIssue.handleStatus)}
  387. size="small"
  388. color={
  389. selectedIssue.handleStatus.toLowerCase() === "pending"
  390. ? "warning"
  391. : "success"
  392. }
  393. />
  394. </Box>
  395. {selectedIssue.handleStatus.toLowerCase() === "pending" && (
  396. <TextField
  397. label={t("Handle Remark")}
  398. multiline
  399. rows={3}
  400. value={handleRemark}
  401. onChange={(e) => setHandleRemark(e.target.value)}
  402. fullWidth
  403. />
  404. )}
  405. </Stack>
  406. )}
  407. </DialogContent>
  408. <DialogActions>
  409. <Button onClick={handleCloseDialog}>{t("Close")}</Button>
  410. {selectedIssue && selectedIssue.handleStatus.toLowerCase() === "pending" && (
  411. <Button onClick={handleMarkAsResolved} variant="contained" color="primary">
  412. {t("Mark as Resolved")}
  413. </Button>
  414. )}
  415. </DialogActions>
  416. </Dialog>
  417. </Box>
  418. );
  419. };
  420. export default PickExecutionIssuesTab;