FPSMS-frontend
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 

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