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.
 
 

342 lines
13 KiB

  1. "use client";
  2. import React, { useCallback, useEffect, useState } from "react";
  3. import {
  4. Box,
  5. Button,
  6. Card,
  7. CardContent,
  8. CardActions,
  9. Stack,
  10. Typography,
  11. Chip,
  12. CircularProgress,
  13. TablePagination,
  14. Grid,
  15. } from "@mui/material";
  16. import { useTranslation } from "react-i18next";
  17. import QcStockInModal from "../Qc/QcStockInModal";
  18. import { useSession } from "next-auth/react";
  19. import { SessionWithTokens } from "@/config/authConfig";
  20. import dayjs from "dayjs";
  21. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  22. import {
  23. fetchAllJoborderProductProcessInfo,
  24. AllJoborderProductProcessInfoResponse,
  25. updateJo,
  26. fetchProductProcessesByJobOrderId,
  27. completeProductProcessLine,
  28. assignJobOrderPickOrder
  29. } from "@/app/api/jo/actions";
  30. import { StockInLineInput } from "@/app/api/stockIn";
  31. import { PrinterCombo } from "@/app/api/settings/printer";
  32. import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan";
  33. interface ProductProcessListProps {
  34. onSelectProcess: (jobOrderId: number|undefined, productProcessId: number|undefined) => void;
  35. onSelectMatchingStock: (jobOrderId: number|undefined, productProcessId: number|undefined) => void;
  36. printerCombo: PrinterCombo[];
  37. }
  38. const PER_PAGE = 6;
  39. const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess, printerCombo ,onSelectMatchingStock}) => {
  40. const { t } = useTranslation( ["common", "production","purchaseOrder"]);
  41. const { data: session } = useSession() as { data: SessionWithTokens | null };
  42. const sessionToken = session as SessionWithTokens | null;
  43. const [loading, setLoading] = useState(false);
  44. const [processes, setProcesses] = useState<AllJoborderProductProcessInfoResponse[]>([]);
  45. const [page, setPage] = useState(0);
  46. const [openModal, setOpenModal] = useState<boolean>(false);
  47. const [modalInfo, setModalInfo] = useState<StockInLineInput>();
  48. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  49. const handleAssignPickOrder = useCallback(async (pickOrderId: number, jobOrderId?: number, productProcessId?: number) => {
  50. if (!currentUserId) {
  51. alert(t("Unable to get user ID"));
  52. return;
  53. }
  54. try {
  55. console.log("🔄 Assigning pick order:", pickOrderId, "to user:", currentUserId);
  56. // 调用分配 API 并读取响应
  57. const assignResult = await assignJobOrderPickOrder(pickOrderId, currentUserId);
  58. console.log("📦 Assign result:", assignResult);
  59. // 检查分配是否成功
  60. if (assignResult.message === "Successfully assigned") {
  61. console.log("✅ Successfully assigned pick order");
  62. console.log("✅ Pick order ID:", assignResult.id);
  63. console.log("✅ Pick order code:", assignResult.code);
  64. // 分配成功后,导航到 second scan 页面
  65. if (onSelectMatchingStock && jobOrderId) {
  66. onSelectMatchingStock(jobOrderId, productProcessId);
  67. } else {
  68. alert(t("Assignment successful"));
  69. }
  70. } else {
  71. // 分配失败
  72. console.error("Assignment failed:", assignResult.message);
  73. alert(t(`Assignment failed: ${assignResult.message || "Unknown error"}`));
  74. }
  75. } catch (error: any) {
  76. console.error(" Error assigning pick order:", error);
  77. alert(t(`Unknown error: ${error?.message || "Unknown error"}。Please try again later.`));
  78. }
  79. }, [currentUserId, t, onSelectMatchingStock]);
  80. const handleViewStockIn = useCallback((process: AllJoborderProductProcessInfoResponse) => {
  81. if (!process.stockInLineId) {
  82. alert(t("Invalid Stock In Line Id"));
  83. return;
  84. }
  85. setModalInfo({
  86. id: process.stockInLineId,
  87. //expiryDate: dayjs().add(1, "month").format(OUTPUT_DATE_FORMAT),
  88. // 视需要补 itemId、jobOrderId 等
  89. });
  90. setOpenModal(true);
  91. }, [t]);
  92. const fetchProcesses = useCallback(async () => {
  93. setLoading(true);
  94. try {
  95. const data = await fetchAllJoborderProductProcessInfo();
  96. setProcesses(data || []);
  97. setPage(0);
  98. } catch (e) {
  99. console.error(e);
  100. setProcesses([]);
  101. } finally {
  102. setLoading(false);
  103. }
  104. }, []);
  105. useEffect(() => {
  106. fetchProcesses();
  107. }, [fetchProcesses]);
  108. const handleUpdateJo = useCallback(async (process: AllJoborderProductProcessInfoResponse) => {
  109. if (!process.jobOrderId) {
  110. alert(t("Invalid Job Order Id"));
  111. return;
  112. }
  113. try {
  114. setLoading(true); // 可选:已有 loading state 可复用
  115. // 1) 拉取该 JO 的所有 process,取出全部 lineId
  116. const processes = await fetchProductProcessesByJobOrderId(process.jobOrderId);
  117. const lineIds = (processes ?? [])
  118. .flatMap(p => (p as any).productProcessLines ?? [])
  119. .map(l => l.id)
  120. .filter(Boolean);
  121. // 2) 逐个调用 completeProductProcessLine
  122. for (const lineId of lineIds) {
  123. try {
  124. await completeProductProcessLine(lineId);
  125. } catch (e) {
  126. console.error("completeProductProcessLine failed for lineId:", lineId, e);
  127. }
  128. }
  129. // 3) 更新 JO 状态
  130. // await updateJo({ id: process.jobOrderId, status: "completed" });
  131. // 4) 刷新列表
  132. await fetchProcesses();
  133. } catch (e) {
  134. console.error(e);
  135. alert(t("An error has occurred. Please try again later."));
  136. } finally {
  137. setLoading(false);
  138. }
  139. }, [t, fetchProcesses]);
  140. const closeNewModal = useCallback(() => {
  141. // const response = updateJo({ id: 1, status: "storing" });
  142. setOpenModal(false); // Close the modal first
  143. fetchProcesses();
  144. // setTimeout(() => {
  145. // }, 300); // Add a delay to avoid immediate re-trigger of useEffect
  146. }, [fetchProcesses]);
  147. const startIdx = page * PER_PAGE;
  148. const paged = processes.slice(startIdx, startIdx + PER_PAGE);
  149. return (
  150. <Box>
  151. {loading ? (
  152. <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
  153. <CircularProgress />
  154. </Box>
  155. ) : (
  156. <Box>
  157. <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
  158. {t("Total processes")}: {processes.length}
  159. </Typography>
  160. <Grid container spacing={2}>
  161. {paged.map((process) => {
  162. const status = String(process.status || "");
  163. const statusLower = status.toLowerCase();
  164. const statusColor =
  165. statusLower === "completed"
  166. ? "success"
  167. : statusLower === "in_progress" || statusLower === "processing"
  168. ? "primary"
  169. : "default";
  170. const finishedCount =
  171. (process.lines || []).filter(
  172. (l) => String(l.status ?? "").trim().toLowerCase() === "completed"
  173. ).length;
  174. const totalCount = process.productProcessLineCount ?? process.lines?.length ?? 0;
  175. const linesWithStatus = (process.lines || []).filter(
  176. (l) => String(l.status ?? "").trim() !== ""
  177. );
  178. const dateDisplay = process.date
  179. ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT)
  180. : "-";
  181. const jobOrderCode =
  182. (process as any).jobOrderCode ??
  183. (process.jobOrderId ? `JO-${process.jobOrderId}` : "N/A");
  184. const inProgressLines = (process.lines || [])
  185. .filter(l => String(l.status ?? "").trim() !== "")
  186. .filter(l => String(l.status).toLowerCase() === "in_progress");
  187. return (
  188. <Grid key={process.id} item xs={12} sm={6} md={4}>
  189. <Card
  190. sx={{
  191. minHeight: 160,
  192. maxHeight: 300,
  193. display: "flex",
  194. flexDirection: "column",
  195. border: "1px solid",
  196. borderColor: "success.main",
  197. }}
  198. >
  199. <CardContent
  200. sx={{
  201. pb: 1,
  202. flexGrow: 1, // let content take remaining height
  203. overflow: "auto", // allow scroll when content exceeds
  204. }}
  205. >
  206. <Stack direction="row" justifyContent="space-between" alignItems="center">
  207. <Box sx={{ minWidth: 0 }}>
  208. <Typography variant="subtitle1">
  209. {t("Job Order")}: {jobOrderCode}
  210. </Typography>
  211. </Box>
  212. <Chip size="small" label={t(status)} color={statusColor as any} />
  213. </Stack>
  214. <Typography variant="body2" color="text.secondary">
  215. {t("Item Name")}: {process.itemCode} {process.itemName}
  216. </Typography>
  217. <Typography variant="body2" color="text.secondary">
  218. {t("Production Priority")}: {process.productionPriority}
  219. </Typography>
  220. <Typography variant="body2" color="text.secondary">
  221. {t("Required Qty")}: {process.requiredQty} ({process.uom})
  222. </Typography>
  223. <Typography variant="body2" color="text.secondary">
  224. {t("Production date")}: {process.date ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT) : "-"}
  225. </Typography>
  226. <Typography variant="body2" color="text.secondary">
  227. {t("Assume Time Need")}: {process.timeNeedToComplete} {t("minutes")}
  228. </Typography>
  229. {statusLower !== "pending" && linesWithStatus.length > 0 && (
  230. <Box sx={{ mt: 1 }}>
  231. <Typography variant="body2" fontWeight={600}>
  232. {t("Finished lines")}: {finishedCount} / {totalCount}
  233. </Typography>
  234. {inProgressLines.length > 0 && (
  235. <Box sx={{ mt: 1 }}>
  236. {inProgressLines.map(line => (
  237. <Typography key={line.id} variant="caption" color="text.secondary" display="block">
  238. {t("Operator")}: {line.operatorName || "-"} <br />
  239. {t("Equipment")}: {line.equipmentName || "-"}
  240. </Typography>
  241. ))}
  242. </Box>
  243. )}
  244. </Box>
  245. )}
  246. {statusLower == "pending" && (
  247. <Box sx={{ mt: 1 }}>
  248. <Typography variant="body2" fontWeight={600} color= "white">
  249. {t("t")}
  250. </Typography>
  251. <Box sx={{ mt: 1 }}>
  252. <Typography variant="caption" color="text.secondary" display="block">
  253. {""}
  254. </Typography>
  255. </Box>
  256. </Box>
  257. )}
  258. </CardContent>
  259. <CardActions sx={{ pt: 0.5 }}>
  260. <Button variant="contained" size="small" onClick={() => onSelectProcess(process.jobOrderId, process.id)}>
  261. {t("View Details")}
  262. </Button>
  263. <Button
  264. variant="contained"
  265. size="small"
  266. disabled={process.assignedTo != null || process.matchStatus == "completed"|| process.pickOrderStatus != "completed"}
  267. onClick={() => handleAssignPickOrder(process.pickOrderId, process.jobOrderId, process.id)}
  268. >
  269. {t("Matching Stock")}
  270. </Button>
  271. {statusLower !== "completed" && (
  272. <Button variant="contained" size="small" onClick={() => handleUpdateJo(process)}>
  273. {t("Update Job Order")}
  274. </Button>
  275. )}
  276. {statusLower === "completed" && (
  277. <Button onClick={() => handleViewStockIn(process)}>
  278. {t("view stockin")}
  279. </Button>
  280. )}
  281. <Box sx={{ flex: 1 }} />
  282. </CardActions>
  283. </Card>
  284. </Grid>
  285. );
  286. })}
  287. </Grid>
  288. <QcStockInModal
  289. session={sessionToken}
  290. open={openModal}
  291. onClose={closeNewModal}
  292. inputDetail={modalInfo}
  293. printerCombo={printerCombo}
  294. printSource="productionProcess"
  295. />
  296. {processes.length > 0 && (
  297. <TablePagination
  298. component="div"
  299. count={processes.length}
  300. page={page}
  301. rowsPerPage={PER_PAGE}
  302. onPageChange={(e, p) => setPage(p)}
  303. rowsPerPageOptions={[PER_PAGE]}
  304. />
  305. )}
  306. </Box>
  307. )}
  308. </Box>
  309. );
  310. };
  311. export default ProductProcessList;