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.

ProductionProcessStepExecution.tsx 40 KiB

1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 day ago
1 month ago
1 day ago
1 month ago
1 day ago
1 month ago
1 day ago
1 month ago
1 day ago
1 month ago
1 month ago
1 month ago
1 day ago
1 month ago
1 month ago
1 day ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 day ago
1 month ago
1 month ago
1 day ago
1 month ago
1 day ago
1 month ago
1 day ago
1 week ago
1 month ago
1 week ago
1 week ago
1 week ago
1 month ago
1 month ago
1 week ago
1 month ago
1 week ago
1 week ago
1 week ago
1 week ago
1 month ago
1 week ago
1 week ago
1 week ago
1 week ago
1 month ago
1 month ago
1 week ago
1 day ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 day ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 day ago
1 day ago
1 month ago
1 day ago
1 month ago
1 month ago
1 day ago
1 month ago
1 month ago
1 day ago
1 month ago
1 day ago
1 month ago
1 day ago
1 month ago
1 week ago
1 month ago
1 week ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 week ago
1 month ago
1 day ago
1 month ago
1 month ago
1 month ago
1 day ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 day ago
1 month ago
1 month ago
1 month ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031
  1. "use client";
  2. import {
  3. Box,
  4. Button,
  5. Paper,
  6. Stack,
  7. Typography,
  8. TextField,
  9. Table,
  10. TableBody,
  11. TableCell,
  12. TableHead,
  13. TableRow,
  14. Dialog,
  15. DialogTitle,
  16. DialogContent,
  17. DialogActions,
  18. Card,
  19. CardContent,
  20. Grid,
  21. } from "@mui/material";
  22. import { Alert } from "@mui/material";
  23. import QrCodeIcon from '@mui/icons-material/QrCode';
  24. import CheckCircleIcon from "@mui/icons-material/CheckCircle";
  25. import StopIcon from "@mui/icons-material/Stop";
  26. import PauseIcon from "@mui/icons-material/Pause";
  27. import PlayArrowIcon from "@mui/icons-material/PlayArrow";
  28. import { useTranslation } from "react-i18next";
  29. import {
  30. JobOrderProcessLineDetailResponse,
  31. updateProductProcessLineQty,
  32. updateProductProcessLineQrscan,
  33. fetchProductProcessLineDetail,
  34. UpdateProductProcessLineQtyRequest,
  35. saveProductProcessResumeTime,
  36. saveProductProcessIssueTime,
  37. ProductProcessWithLinesResponse, // ✅ 添加
  38. ProductProcessLineResponse, // ✅ 添加
  39. } from "@/app/api/jo/actions";
  40. import { Operator, Machine } from "@/app/api/jo";
  41. import React, { useCallback, useEffect, useState, useMemo } from "react"; // ✅ 添加 useMemo
  42. import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
  43. import { fetchNameList, NameList } from "@/app/api/user/actions";
  44. import BagConsumptionForm from "./BagConsumptionForm"; // ✅ 添加导入
  45. import OverallTimeRemainingCard from "./OverallTimeRemainingCard"; // ✅ 添加导入
  46. import dayjs from "dayjs";
  47. interface ProductionProcessStepExecutionProps {
  48. lineId: number | null;
  49. onBack: () => void;
  50. processData?: ProductProcessWithLinesResponse | null; // ✅ 添加
  51. allLines?: ProductProcessLineResponse[]; // ✅ 添加
  52. jobOrderId?: number; // ✅ 添加
  53. }
  54. const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({
  55. lineId,
  56. onBack,
  57. processData, // ✅ 添加
  58. allLines, // ✅ 添加
  59. jobOrderId, // ✅ 添加
  60. }) => {
  61. const { t } = useTranslation( ["common","jo"]);
  62. const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null);
  63. const isCompleted = lineDetail?.status === "Completed" || lineDetail?.status === "Pass";
  64. const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & {
  65. byproductName: string;
  66. byproductQty: number;
  67. byproductUom: string;
  68. }>({
  69. productProcessLineId: lineId ?? 0,
  70. outputFromProcessQty: 0,
  71. outputFromProcessUom: "",
  72. defectQty: 0,
  73. defectUom: "",
  74. scrapQty: 0,
  75. scrapUom: "",
  76. byproductName: "",
  77. byproductQty: 0,
  78. byproductUom: "",
  79. defect2Qty: 0,
  80. defect2Uom: "",
  81. defect3Qty: 0,
  82. defect3Uom: "",
  83. defectDescription: "",
  84. defectDescription2: "",
  85. defectDescription3: ""
  86. });
  87. const [isManualScanning, setIsManualScanning] = useState(false);
  88. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  89. const [scannedOperators, setScannedOperators] = useState<Operator[]>([]);
  90. const [scannedMachines, setScannedMachines] = useState<Machine[]>([]);
  91. const [isPaused, setIsPaused] = useState(false);
  92. const [showOutputTable, setShowOutputTable] = useState(false);
  93. const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  94. const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-";
  95. const [remainingTime, setRemainingTime] = useState<string | null>(null);
  96. const [isOverTime, setIsOverTime] = useState(false);
  97. const [frozenRemainingTime, setFrozenRemainingTime] = useState<string | null>(null);
  98. const [lastPauseTime, setLastPauseTime] = useState<Date | null>(null);
  99. const[isOpenReasonModel, setIsOpenReasonModel] = useState(false);
  100. const [pauseReason, setPauseReason] = useState("");
  101. // ✅ 添加:判断是否显示 Bag 表单的条件
  102. const shouldShowBagForm = useMemo(() => {
  103. if (!processData || !allLines || !lineDetail) return false;
  104. // 检查 BOM description 是否为 "FG"
  105. const bomDescription = processData.bomDescription;
  106. if (bomDescription !== "FG") return false;
  107. // 检查是否是最后一个 process line(按 seqNo 排序)
  108. const sortedLines = [...allLines].sort((a, b) => (a.seqNo || 0) - (b.seqNo || 0));
  109. const maxSeqNo = sortedLines[sortedLines.length - 1]?.seqNo;
  110. const isLastLine = lineDetail.seqNo === maxSeqNo;
  111. return isLastLine;
  112. }, [processData, allLines, lineDetail]);
  113. // ✅ 添加:刷新 line detail 的函数
  114. const handleRefreshLineDetail = useCallback(async () => {
  115. if (lineId) {
  116. try {
  117. const detail = await fetchProductProcessLineDetail(lineId);
  118. setLineDetail(detail as any);
  119. } catch (error) {
  120. console.error("Failed to refresh line detail", error);
  121. }
  122. }
  123. }, [lineId]);
  124. useEffect(() => {
  125. if (!lineId) {
  126. setLineDetail(null);
  127. return;
  128. }
  129. fetchProductProcessLineDetail(lineId)
  130. .then((detail) => {
  131. setLineDetail(detail as any);
  132. console.log("📋 Line Detail loaded:", {
  133. id: detail.id,
  134. status: detail.status,
  135. durationInMinutes: detail.durationInMinutes,
  136. startTime: detail.startTime,
  137. startTimeType: typeof detail.startTime,
  138. hasDuration: !!detail.durationInMinutes,
  139. hasStartTime: !!detail.startTime,
  140. });
  141. setOutputData(prev => ({
  142. ...prev,
  143. productProcessLineId: detail.id,
  144. outputFromProcessQty: (detail as any).outputFromProcessQty || 0,
  145. outputFromProcessUom: (detail as any).outputFromProcessUom || "",
  146. defectQty: detail.defectQty || 0,
  147. defectUom: detail.defectUom || "",
  148. scrapQty: detail.scrapQty || 0,
  149. scrapUom: detail.scrapUom || "",
  150. byproductName: detail.byproductName || "",
  151. byproductQty: detail.byproductQty || 0,
  152. byproductUom: detail.byproductUom || ""
  153. }));
  154. })
  155. .catch(err => {
  156. console.error("Failed to load line detail", err);
  157. setLineDetail(null);
  158. });
  159. }, [lineId]);
  160. useEffect(() => {
  161. // Don't show time remaining if completed
  162. if (lineDetail?.status === "Completed" || lineDetail?.status === "Pass") {
  163. console.log("Line is completed");
  164. setRemainingTime(null);
  165. setIsOverTime(false);
  166. return;
  167. }
  168. console.log("🔍 Time Remaining Debug:", {
  169. lineId: lineDetail?.id,
  170. equipmentId: lineDetail?.equipmentId,
  171. equipmentType: lineDetail?.equipmentType,
  172. durationInMinutes: lineDetail?.durationInMinutes,
  173. startTime: lineDetail?.startTime,
  174. startTimeType: typeof lineDetail?.startTime,
  175. isStartTimeArray: Array.isArray(lineDetail?.startTime),
  176. status: lineDetail?.status,
  177. hasDuration: !!lineDetail?.durationInMinutes,
  178. hasStartTime: !!lineDetail?.startTime,
  179. });
  180. if (!lineDetail?.durationInMinutes || !lineDetail?.startTime) {
  181. console.log("❌ Line duration or start time is not valid", {
  182. durationInMinutes: lineDetail?.durationInMinutes,
  183. startTime: lineDetail?.startTime,
  184. equipmentId: lineDetail?.equipmentId,
  185. equipmentType: lineDetail?.equipmentType,
  186. });
  187. setRemainingTime(null);
  188. setIsOverTime(false);
  189. return;
  190. }
  191. let start: Date;
  192. if (Array.isArray(lineDetail.startTime)) {
  193. console.log("Line start time is an array:", lineDetail.startTime);
  194. const [year, month, day, hour = 0, minute = 0, second = 0] = lineDetail.startTime;
  195. start = new Date(year, month - 1, day, hour, minute, second);
  196. } else {
  197. start = new Date(lineDetail.startTime);
  198. console.log("Line start time is a string:", lineDetail.startTime);
  199. }
  200. if (isNaN(start.getTime())) {
  201. console.error("Invalid startTime:", lineDetail.startTime);
  202. setRemainingTime(null);
  203. setIsOverTime(false);
  204. return;
  205. }
  206. const durationMs = lineDetail.durationInMinutes * 60_000;
  207. const isPaused = lineDetail.status === "Paused" || lineDetail.productProcessIssueStatus === "Paused";
  208. const parseStopTime = (stopTime: string | number[] | undefined): Date | null => {
  209. if (!stopTime) return null;
  210. if (Array.isArray(stopTime)) {
  211. const [year, month, day, hour = 0, minute = 0, second = 0] = stopTime;
  212. return new Date(year, month - 1, day, hour, minute, second);
  213. } else {
  214. return new Date(stopTime);
  215. }
  216. };
  217. const update = () => {
  218. if (isPaused) {
  219. if (!frozenRemainingTime) {
  220. const pauseTime = lineDetail.stopTime
  221. ? parseStopTime(lineDetail.stopTime)
  222. : null;
  223. const pauseTimeToUse = pauseTime && !isNaN(pauseTime.getTime())
  224. ? pauseTime
  225. : new Date();
  226. const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0;
  227. console.log("⏸️ Paused - calculating frozen time:", {
  228. stopTime: lineDetail.stopTime,
  229. pauseTime: pauseTimeToUse,
  230. startTime: start,
  231. totalPausedTimeMs: totalPausedTimeMs,
  232. });
  233. const elapsed = pauseTimeToUse.getTime() - start.getTime() - totalPausedTimeMs;
  234. const remaining = durationMs - elapsed;
  235. if (remaining <= 0) {
  236. const overTime = Math.abs(remaining);
  237. const minutes = Math.floor(overTime / 60000).toString().padStart(2, "0");
  238. const seconds = Math.floor((overTime % 60000) / 1000).toString().padStart(2, "0");
  239. const frozenValue = `-${minutes}:${seconds}`;
  240. setFrozenRemainingTime(frozenValue);
  241. setRemainingTime(frozenValue);
  242. setIsOverTime(true);
  243. console.log("⏸️ Frozen time (overtime):", frozenValue);
  244. } else {
  245. const minutes = Math.floor(remaining / 60000).toString().padStart(2, "0");
  246. const seconds = Math.floor((remaining % 60000) / 1000).toString().padStart(2, "0");
  247. const frozenValue = `${minutes}:${seconds}`;
  248. setFrozenRemainingTime(frozenValue);
  249. setRemainingTime(frozenValue);
  250. setIsOverTime(false);
  251. console.log("⏸️ Frozen time:", frozenValue);
  252. }
  253. } else {
  254. setRemainingTime(frozenRemainingTime);
  255. console.log("⏸️ Using frozen time:", frozenRemainingTime);
  256. }
  257. return;
  258. }
  259. if (frozenRemainingTime && !isPaused) {
  260. console.log("▶️ Resumed - clearing frozen time");
  261. setFrozenRemainingTime(null);
  262. setLastPauseTime(null);
  263. }
  264. const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0;
  265. const now = new Date();
  266. const elapsed = now.getTime() - start.getTime() - totalPausedTimeMs;
  267. const remaining = durationMs - elapsed;
  268. console.log("⏱️ Time calculation:", {
  269. now: now,
  270. start: start,
  271. totalPausedTimeMs: totalPausedTimeMs,
  272. elapsed: elapsed,
  273. remaining: remaining,
  274. durationMs: durationMs,
  275. });
  276. if (remaining <= 0) {
  277. const overTime = Math.abs(remaining);
  278. const minutes = Math.floor(overTime / 60000).toString().padStart(2, "0");
  279. const seconds = Math.floor((overTime % 60000) / 1000).toString().padStart(2, "0");
  280. setRemainingTime(`-${minutes}:${seconds}`);
  281. setIsOverTime(true);
  282. } else {
  283. const minutes = Math.floor(remaining / 60000).toString().padStart(2, "0");
  284. const seconds = Math.floor((remaining % 60000) / 1000).toString().padStart(2, "0");
  285. setRemainingTime(`${minutes}:${seconds}`);
  286. setIsOverTime(false);
  287. }
  288. };
  289. update();
  290. if (!isPaused) {
  291. const timer = setInterval(update, 1000);
  292. return () => clearInterval(timer);
  293. }
  294. }, [lineDetail?.durationInMinutes, lineDetail?.startTime, lineDetail?.status, lineDetail?.productProcessIssueStatus, lineDetail?.stopTime, frozenRemainingTime]);
  295. useEffect(() => {
  296. const wasPaused = lineDetail?.status === "Paused" || lineDetail?.productProcessIssueStatus === "Paused";
  297. const isNowInProgress = lineDetail?.status === "InProgress";
  298. if (wasPaused && isNowInProgress && frozenRemainingTime) {
  299. setFrozenRemainingTime(null);
  300. }
  301. }, [lineDetail?.status, lineDetail?.productProcessIssueStatus]);
  302. const handleSubmitOutput = async () => {
  303. if (!lineDetail?.id) return;
  304. try {
  305. await updateProductProcessLineQty({
  306. productProcessLineId: lineDetail?.id || 0 as number,
  307. byproductName: outputData.byproductName,
  308. byproductQty: outputData.byproductQty,
  309. byproductUom: outputData.byproductUom,
  310. outputFromProcessQty: outputData.outputFromProcessQty,
  311. outputFromProcessUom: outputData.outputFromProcessUom,
  312. defectQty: outputData.defectQty,
  313. defectUom: outputData.defectUom,
  314. defect2Qty: outputData.defect2Qty,
  315. defect2Uom: outputData.defect2Uom,
  316. defect3Qty: outputData.defect3Qty,
  317. defect3Uom: outputData.defect3Uom,
  318. defectDescription: outputData.defectDescription,
  319. defectDescription2: outputData.defectDescription2,
  320. defectDescription3: outputData.defectDescription3,
  321. scrapQty: outputData.scrapQty,
  322. scrapUom: outputData.scrapUom,
  323. });
  324. console.log(" Output data submitted successfully");
  325. fetchProductProcessLineDetail(lineDetail.id)
  326. .then((detail) => {
  327. console.log("Line Detail loaded:", {
  328. id: detail.id,
  329. status: detail.status,
  330. startTime: detail.startTime,
  331. durationInMinutes: detail.durationInMinutes,
  332. productProcessIssueStatus: detail.productProcessIssueStatus
  333. });
  334. setLineDetail(detail as any);
  335. setOutputData(prev => ({
  336. ...prev,
  337. productProcessLineId: detail.id,
  338. outputFromProcessQty: (detail as any).outputFromProcessQty || 0,
  339. outputFromProcessUom: (detail as any).outputFromProcessUom || "",
  340. defectQty: detail.defectQty || 0,
  341. defectUom: detail.defectUom || "",
  342. defectDescription: detail.defectDescription || "",
  343. defectDescription2: detail.defectDescription2 || "",
  344. defectDescription3: detail.defectDescription3 || "",
  345. defectQty2: detail.defectQty2 || 0,
  346. defectUom2: detail.defectUom2 || "",
  347. defectQty3: detail.defectQty3 || 0,
  348. defectUom3: detail.defectUom3 || "",
  349. scrapQty: detail.scrapQty || 0,
  350. scrapUom: detail.scrapUom || "",
  351. byproductName: detail.byproductName || "",
  352. byproductQty: detail.byproductQty || 0,
  353. byproductUom: detail.byproductUom || ""
  354. }));
  355. })
  356. .catch(err => {
  357. console.error("Failed to load line detail", err);
  358. setLineDetail(null);
  359. });
  360. } catch (error) {
  361. console.error("Error submitting output:", error);
  362. alert("Failed to submit output data. Please try again.");
  363. }
  364. };
  365. useEffect(() => {
  366. if (isManualScanning && qrValues.length > 0 && lineDetail?.id) {
  367. const latestQr = qrValues[qrValues.length - 1];
  368. if (processedQrCodes.has(latestQr)) {
  369. return;
  370. }
  371. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  372. }
  373. }, [qrValues, isManualScanning, lineDetail?.id, processedQrCodes]);
  374. const lineAssumeEndTime = useMemo(() => {
  375. if (!lineDetail?.startTime || !lineDetail?.durationInMinutes) return null;
  376. // 解析 startTime(可能是数组或字符串)
  377. let start: dayjs.Dayjs;
  378. if (Array.isArray(lineDetail.startTime)) {
  379. const [year, month, day, hour = 0, minute = 0, second = 0] = lineDetail.startTime;
  380. start = dayjs(new Date(year, month - 1, day, hour, minute, second));
  381. } else if (typeof lineDetail.startTime === 'string') {
  382. // 检查是否是 "MM-DD HH:mm" 格式
  383. const mmddHhmmPattern = /^(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2})$/;
  384. const match = lineDetail.startTime.match(mmddHhmmPattern);
  385. if (match) {
  386. const month = parseInt(match[1], 10);
  387. const day = parseInt(match[2], 10);
  388. const hour = parseInt(match[3], 10);
  389. const minute = parseInt(match[4], 10);
  390. // 使用当前年份,但如果跨年(startTime 是年末,当前是年初),使用上一年
  391. const now = dayjs();
  392. let year = now.year();
  393. if (month === 12 && day >= 20 && now.month() === 0 && now.date() <= 10) {
  394. year = now.year() - 1;
  395. }
  396. start = dayjs(new Date(year, month - 1, day, hour, minute, 0));
  397. } else {
  398. start = dayjs(lineDetail.startTime);
  399. }
  400. } else {
  401. start = dayjs(lineDetail.startTime as any);
  402. }
  403. if (!start.isValid()) return null;
  404. return start.add(lineDetail.durationInMinutes, 'minute');
  405. }, [lineDetail?.startTime, lineDetail?.durationInMinutes]);
  406. const lineStartTime = useMemo(() => {
  407. if (!lineDetail?.startTime) return null;
  408. let start: dayjs.Dayjs;
  409. if (Array.isArray(lineDetail.startTime)) {
  410. const [year, month, day, hour = 0, minute = 0, second = 0] = lineDetail.startTime;
  411. start = dayjs(new Date(year, month - 1, day, hour, minute, second));
  412. } else if (typeof lineDetail.startTime === 'string') {
  413. const mmddHhmmPattern = /^(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2})$/;
  414. const match = lineDetail.startTime.match(mmddHhmmPattern);
  415. if (match) {
  416. const month = parseInt(match[1], 10);
  417. const day = parseInt(match[2], 10);
  418. const hour = parseInt(match[3], 10);
  419. const minute = parseInt(match[4], 10);
  420. const now = dayjs();
  421. let year = now.year();
  422. if (month === 12 && day >= 20 && now.month() === 0 && now.date() <= 10) {
  423. year = now.year() - 1;
  424. }
  425. start = dayjs(new Date(year, month - 1, day, hour, minute, 0));
  426. } else {
  427. start = dayjs(lineDetail.startTime);
  428. }
  429. } else {
  430. start = dayjs(lineDetail.startTime as any);
  431. }
  432. return start.isValid() ? start : null;
  433. }, [lineDetail?.startTime]);
  434. const handleOpenReasonModel = () => {
  435. setIsOpenReasonModel(true);
  436. setPauseReason("");
  437. };
  438. const handleCloseReasonModel = () => {
  439. setIsOpenReasonModel(false);
  440. setPauseReason("");
  441. };
  442. const handleSaveReason = async () => {
  443. if (!pauseReason.trim()) {
  444. alert(t("Please enter a reason for pausing"));
  445. return;
  446. }
  447. if (!lineDetail?.id) return;
  448. try {
  449. await saveProductProcessIssueTime({
  450. productProcessLineId: lineDetail.id,
  451. reason: pauseReason.trim()
  452. });
  453. setIsOpenReasonModel(false);
  454. setPauseReason("");
  455. fetchProductProcessLineDetail(lineDetail.id)
  456. .then((detail) => {
  457. setLineDetail(detail as any);
  458. })
  459. .catch(err => {
  460. console.error("Failed to load line detail", err);
  461. });
  462. } catch (error) {
  463. console.error("Error saving pause reason:", error);
  464. alert(t("Failed to pause. Please try again."));
  465. }
  466. };
  467. const handleResume = async () => {
  468. if (!lineDetail?.productProcessIssueId) {
  469. console.error("No productProcessIssueId found");
  470. return;
  471. }
  472. try {
  473. await saveProductProcessResumeTime(lineDetail.productProcessIssueId);
  474. console.log("✅ Resume API called successfully");
  475. if (lineDetail?.id) {
  476. fetchProductProcessLineDetail(lineDetail.id)
  477. .then((detail) => {
  478. console.log("✅ Line detail refreshed after resume:", detail);
  479. setLineDetail(detail as any);
  480. setFrozenRemainingTime(null);
  481. setLastPauseTime(null);
  482. })
  483. .catch(err => {
  484. console.error("❌ Failed to load line detail after resume", err);
  485. });
  486. }
  487. } catch (error) {
  488. console.error("❌ Error resuming:", error);
  489. alert(t("Failed to resume. Please try again."));
  490. }
  491. };
  492. return (
  493. <Box>
  494. <Box sx={{ mb: 2 }}>
  495. <Button variant="outlined" onClick={onBack}>
  496. {t("Back to List")}
  497. </Button>
  498. </Box>
  499. {processData && (
  500. <OverallTimeRemainingCard processData={processData} />
  501. )}
  502. {isCompleted ? (
  503. <Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}>
  504. <CardContent>
  505. {lineDetail?.status === "Pass" ? (
  506. <Typography variant="h5" color="success.main" gutterBottom fontWeight="bold">
  507. {t("Passed Step")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo})
  508. </Typography>
  509. ) : (
  510. <Typography variant="h5" color="success.main" gutterBottom fontWeight="bold">
  511. {t("Completed Step")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo})
  512. </Typography>
  513. )}
  514. <Typography variant="h6" gutterBottom sx={{ mt: 2 }}>
  515. {t("Step Information")}
  516. </Typography>
  517. <Grid container spacing={2} sx={{ mb: 3 }}>
  518. <Grid item xs={12} md={6}>
  519. <Typography variant="body2" color="text.secondary" sx={{ fontSize: '1.25rem' }}>
  520. <strong>{t("Description")}:</strong> {lineDetail?.description || "-"}
  521. </Typography>
  522. </Grid>
  523. <Grid item xs={12} md={6}>
  524. <Typography variant="body2" color="text.secondary" sx={{ fontSize: '1.25rem' }}>
  525. <strong>{t("Operator")}:</strong> {lineDetail?.operatorName || "-"}
  526. </Typography>
  527. </Grid>
  528. <Grid item xs={12} md={6}>
  529. <Typography variant="body2" color="text.secondary" sx={{ fontSize: '1.25rem' }}>
  530. <strong>{t("Equipment")}:</strong> {equipmentName}
  531. </Typography>
  532. </Grid>
  533. <Grid item xs={12} md={6}>
  534. <Typography variant="body2" color="text.secondary" sx={{ fontSize: '1.25rem' }}>
  535. <strong>{t("Status")}:</strong> {t(lineDetail?.status || "-")}
  536. </Typography>
  537. </Grid>
  538. </Grid>
  539. <Typography variant="h6" gutterBottom sx={{ mt: 2 }}>
  540. {t("Production Output Data")}
  541. </Typography>
  542. <Table size="small" sx={{ mt: 2 }}>
  543. <TableHead>
  544. <TableRow>
  545. <TableCell width="25%"><strong>{t("Type")}</strong></TableCell>
  546. <TableCell width="25%"><strong>{t("Quantity")}</strong></TableCell>
  547. <TableCell width="25%"><strong>{t("Unit")}</strong></TableCell>
  548. <TableCell width="25%"><strong>{t("Description")}</strong></TableCell>
  549. </TableRow>
  550. </TableHead>
  551. <TableBody>
  552. <TableRow>
  553. <TableCell>
  554. <Typography fontWeight={500}>{t("Output from Process")}</Typography>
  555. </TableCell>
  556. <TableCell>
  557. <Typography>{lineDetail?.outputFromProcessQty || 0}</Typography>
  558. </TableCell>
  559. <TableCell>
  560. <Typography>{lineDetail?.outputFromProcessUom || "-"}</Typography>
  561. </TableCell>
  562. </TableRow>
  563. <TableRow sx={{ bgcolor: 'warning.50' }}>
  564. <TableCell>
  565. <Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography>
  566. </TableCell>
  567. <TableCell>
  568. <Typography>{lineDetail.defectQty}</Typography>
  569. </TableCell>
  570. <TableCell>
  571. <Typography>{lineDetail.defectUom || "-"}</Typography>
  572. </TableCell>
  573. <TableCell>
  574. <Typography>{lineDetail.defectDescription || "-"}</Typography>
  575. </TableCell>
  576. </TableRow>
  577. <TableRow sx={{ bgcolor: 'warning.50' }}>
  578. <TableCell>
  579. <Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(3)")}</Typography>
  580. </TableCell>
  581. <TableCell>
  582. <Typography>{lineDetail.defectQty3}</Typography>
  583. </TableCell>
  584. <TableCell>
  585. <Typography>{lineDetail.defectUom3 || "-"}</Typography>
  586. </TableCell>
  587. <TableCell>
  588. <Typography>{lineDetail.defectDescription3 || "-"}</Typography>
  589. </TableCell>
  590. </TableRow>
  591. <TableRow sx={{ bgcolor: 'warning.50' }}>
  592. <TableCell>
  593. <Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(2)")}</Typography>
  594. </TableCell>
  595. <TableCell>
  596. <Typography>{lineDetail.defectQty2}</Typography>
  597. </TableCell>
  598. <TableCell>
  599. <Typography>{lineDetail.defectUom2 || "-"}</Typography>
  600. </TableCell>
  601. <TableCell>
  602. <Typography>{lineDetail.defectDescription2 || "-"}</Typography>
  603. </TableCell>
  604. </TableRow>
  605. <TableRow sx={{ bgcolor: 'error.50' }}>
  606. <TableCell>
  607. <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography>
  608. </TableCell>
  609. <TableCell>
  610. <Typography>{lineDetail.scrapQty}</Typography>
  611. </TableCell>
  612. <TableCell>
  613. <Typography>{lineDetail.scrapUom || "-"}</Typography>
  614. </TableCell>
  615. </TableRow>
  616. </TableBody>
  617. </Table>
  618. </CardContent>
  619. </Card>
  620. ) : (
  621. <>
  622. {!showOutputTable && (
  623. <Grid container spacing={2} sx={{ mb: 3 }}>
  624. <Grid item xs={12} >
  625. <Card sx={{ bgcolor: 'primary.50', border: '2px solid', borderColor: 'primary.main', height: '100%' }}>
  626. <CardContent>
  627. <Typography variant="h6" color="primary.main" gutterBottom>
  628. {t("Executing")}: {lineDetail?.name} ({t("Seq")}:{lineDetail?.seqNo})
  629. </Typography>
  630. <Typography variant="body2" color="text.secondary">
  631. {lineDetail?.description}
  632. </Typography>
  633. <Typography variant="body2" color="text.secondary">
  634. {t("Operator")}: {lineDetail?.operatorName || "-"}
  635. </Typography>
  636. <Typography variant="body2" color="text.secondary">
  637. {t("Equipment")}: {equipmentName}
  638. </Typography>
  639. {!isCompleted && remainingTime !== null && (
  640. <Box sx={{ mt: 2, mb: 2, p: 2, bgcolor: isOverTime ? 'error.50' : 'info.50', borderRadius: 1, border: '1px solid', borderColor: isOverTime ? 'error.main' : 'info.main' }}>
  641. <Typography variant="body2" color="text.secondary" gutterBottom>
  642. {t("Time Remaining")}
  643. </Typography>
  644. <Typography
  645. variant="h5"
  646. fontWeight="bold"
  647. color={isOverTime ? 'error.main' : 'info.main'}
  648. >
  649. {isOverTime ? `${t("Over Time")}: ${remainingTime}` : remainingTime}
  650. </Typography>
  651. {/* ✅ 添加:Process Start Time 和 Assume End Time */}
  652. {/* ✅ 添加:Process Start Time 和 Assume End Time */}
  653. {processData?.startTime && (
  654. <Box sx={{ mt: 2, pt: 2, borderTop: '1px solid', borderColor: 'divider' }}>
  655. <Typography variant="body2" color="text.secondary" gutterBottom>
  656. <strong>{t("Process Start Time")}:</strong> {dayjs(processData.startTime).format("MM-DD")} {dayjs(processData.startTime).format("HH:mm")}
  657. </Typography>
  658. </Box>
  659. )}
  660. {lineStartTime && (
  661. <Box sx={{ mt: 2, pt: 2, borderTop: '1px solid', borderColor: 'divider' }}>
  662. <Typography variant="body2" color="text.secondary" gutterBottom>
  663. <strong>{t("Step Start Time")}:</strong> {lineStartTime.format("MM-DD")} {lineStartTime.format("HH:mm")}
  664. </Typography>
  665. {lineAssumeEndTime && (
  666. <Typography variant="body2" color="text.secondary">
  667. <strong>{t("Assume End Time")}:</strong> {lineAssumeEndTime.format("MM-DD")} {lineAssumeEndTime.format("HH:mm")}
  668. </Typography>
  669. )}
  670. </Box>
  671. )}
  672. {lineDetail?.status === "Paused" && (
  673. <Typography variant="caption" color="warning.main" sx={{ mt: 0.5, display: 'block' }}>
  674. {t("Timer Paused")}
  675. </Typography>
  676. )}
  677. </Box>
  678. )}
  679. <Stack direction="row" spacing={2} justifyContent="center" sx={{ mt: 2 }}>
  680. { lineDetail?.status === 'InProgress'? (
  681. <Button
  682. variant="contained"
  683. color="warning"
  684. startIcon={<PauseIcon />}
  685. onClick={() => handleOpenReasonModel()}
  686. >
  687. {t("Pause")}
  688. </Button>
  689. ) : (
  690. <Button
  691. variant="contained"
  692. color="success"
  693. startIcon={<PlayArrowIcon />}
  694. onClick={handleResume}
  695. >
  696. {t("Continue")}
  697. </Button>
  698. )}
  699. <Button
  700. sx={{ mt: 2, alignSelf: "flex-end" }}
  701. variant="outlined"
  702. disabled={lineDetail?.status === 'Paused'}
  703. onClick={() => setShowOutputTable(true)}
  704. >
  705. {t("Order Complete")}
  706. </Button>
  707. </Stack>
  708. </CardContent>
  709. </Card>
  710. </Grid>
  711. </Grid>
  712. )}
  713. {/* ========== 产出输入表单 ========== */}
  714. {showOutputTable && (
  715. <Box>
  716. <Paper sx={{ p: 3, bgcolor: 'grey.50' }}>
  717. <Table size="small">
  718. <TableHead>
  719. <TableRow>
  720. <TableCell width="25%" align="center">{t("Type")}</TableCell>
  721. <TableCell width="25%" align="center">{t("Quantity")}</TableCell>
  722. <TableCell width="25%" align="center">{t("Unit")}</TableCell>
  723. <TableCell width="25%" align="center">{t(" ")}</TableCell>
  724. </TableRow>
  725. </TableHead>
  726. <TableBody>
  727. <TableRow>
  728. <TableCell>
  729. <Typography fontWeight={500}>{t("Output from Process")}</Typography>
  730. </TableCell>
  731. <TableCell>
  732. <TextField
  733. type="number"
  734. fullWidth
  735. size="small"
  736. value={outputData.outputFromProcessQty}
  737. onChange={(e) => setOutputData({
  738. ...outputData,
  739. outputFromProcessQty: parseInt(e.target.value) || 0
  740. })}
  741. />
  742. </TableCell>
  743. <TableCell>
  744. <TextField
  745. fullWidth
  746. size="small"
  747. value={outputData.outputFromProcessUom}
  748. onChange={(e) => setOutputData({
  749. ...outputData,
  750. outputFromProcessUom: e.target.value
  751. })}
  752. />
  753. </TableCell>
  754. <TableCell>
  755. <Typography fontSize={15} align="center"> <strong>{t("Description")}</strong></Typography>
  756. </TableCell>
  757. </TableRow>
  758. <TableRow sx={{ bgcolor: 'warning.50' }}>
  759. <TableCell>
  760. <Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(1)")}</Typography>
  761. </TableCell>
  762. <TableCell>
  763. <TextField
  764. type="number"
  765. fullWidth
  766. size="small"
  767. value={outputData.defectQty}
  768. onChange={(e) => setOutputData({
  769. ...outputData,
  770. defectQty: parseInt(e.target.value) || 0
  771. })}
  772. />
  773. </TableCell>
  774. <TableCell>
  775. <TextField
  776. fullWidth
  777. size="small"
  778. value={outputData.defectUom}
  779. onChange={(e) => setOutputData({
  780. ...outputData,
  781. defectUom: e.target.value
  782. })}
  783. />
  784. </TableCell>
  785. <TableCell>
  786. <TextField
  787. fullWidth
  788. size="small"
  789. onChange={(e) => setOutputData({
  790. ...outputData,
  791. defectDescription: e.target.value
  792. })}
  793. />
  794. </TableCell>
  795. </TableRow>
  796. <TableRow sx={{ bgcolor: 'warning.50' }}>
  797. <TableCell>
  798. <Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(2)")}</Typography>
  799. </TableCell>
  800. <TableCell>
  801. <TextField
  802. type="number"
  803. fullWidth
  804. size="small"
  805. value={outputData.defect2Qty}
  806. onChange={(e) => setOutputData({
  807. ...outputData,
  808. defect2Qty: parseInt(e.target.value) || 0
  809. })}
  810. />
  811. </TableCell>
  812. <TableCell>
  813. <TextField
  814. fullWidth
  815. size="small"
  816. value={outputData.defect2Uom}
  817. onChange={(e) => setOutputData({
  818. ...outputData,
  819. defect2Uom: e.target.value
  820. })}
  821. />
  822. </TableCell>
  823. <TableCell>
  824. <TextField
  825. fullWidth
  826. size="small"
  827. onChange={(e) => setOutputData({
  828. ...outputData,
  829. defectDescription2: e.target.value
  830. })}
  831. />
  832. </TableCell>
  833. </TableRow>
  834. <TableRow sx={{ bgcolor: 'warning.50' }}>
  835. <TableCell>
  836. <Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(3)")}</Typography>
  837. </TableCell>
  838. <TableCell>
  839. <TextField
  840. type="number"
  841. fullWidth
  842. size="small"
  843. value={outputData.defect3Qty}
  844. onChange={(e) => setOutputData({
  845. ...outputData,
  846. defect3Qty: parseInt(e.target.value) || 0
  847. })}
  848. />
  849. </TableCell>
  850. <TableCell>
  851. <TextField
  852. fullWidth
  853. size="small"
  854. value={outputData.defect3Uom}
  855. onChange={(e) => setOutputData({
  856. ...outputData,
  857. defect3Uom: e.target.value
  858. })}
  859. />
  860. </TableCell>
  861. <TableCell>
  862. <TextField
  863. fullWidth
  864. size="small"
  865. onChange={(e) => setOutputData({
  866. ...outputData,
  867. defectDescription3: e.target.value
  868. })}
  869. />
  870. </TableCell>
  871. </TableRow>
  872. <TableRow sx={{ bgcolor: 'error.50' }}>
  873. <TableCell>
  874. <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography>
  875. </TableCell>
  876. <TableCell>
  877. <TextField
  878. type="number"
  879. fullWidth
  880. size="small"
  881. value={outputData.scrapQty}
  882. onChange={(e) => setOutputData({
  883. ...outputData,
  884. scrapQty: parseInt(e.target.value) || 0
  885. })}
  886. />
  887. </TableCell>
  888. <TableCell>
  889. <TextField
  890. fullWidth
  891. size="small"
  892. value={outputData.scrapUom}
  893. onChange={(e) => setOutputData({
  894. ...outputData,
  895. scrapUom: e.target.value
  896. })}
  897. />
  898. </TableCell>
  899. </TableRow>
  900. </TableBody>
  901. </Table>
  902. <Box sx={{ mt: 3, display: 'flex', gap: 2 }}>
  903. <Button
  904. variant="outlined"
  905. onClick={() => setShowOutputTable(false)}
  906. >
  907. {t("Cancel")}
  908. </Button>
  909. <Button
  910. variant="contained"
  911. startIcon={<CheckCircleIcon />}
  912. onClick={handleSubmitOutput}
  913. >
  914. {t("Complete Step")}
  915. </Button>
  916. </Box>
  917. </Paper>
  918. </Box>
  919. )}
  920. {/* ========== Bag Consumption Form ========== */}
  921. {((showOutputTable || isCompleted) && shouldShowBagForm && jobOrderId && lineId) && (
  922. <BagConsumptionForm
  923. jobOrderId={jobOrderId}
  924. lineId={lineId}
  925. bomDescription={processData?.bomDescription}
  926. isLastLine={shouldShowBagForm}
  927. onRefresh={handleRefreshLineDetail}
  928. />
  929. )}
  930. </>
  931. )}
  932. <Dialog
  933. open={isOpenReasonModel}
  934. onClose={handleCloseReasonModel}
  935. maxWidth="sm"
  936. fullWidth
  937. >
  938. <DialogTitle>{t("Pause Reason")}</DialogTitle>
  939. <DialogContent>
  940. <TextField
  941. autoFocus
  942. margin="dense"
  943. label={t("Reason")}
  944. fullWidth
  945. multiline
  946. rows={4}
  947. value={pauseReason}
  948. onChange={(e) => setPauseReason(e.target.value)}
  949. />
  950. </DialogContent>
  951. <DialogActions>
  952. <Button onClick={handleCloseReasonModel}>
  953. {t("Cancel")}
  954. </Button>
  955. <Button
  956. onClick={handleSaveReason}
  957. variant="contained"
  958. disabled={!pauseReason.trim()}
  959. >
  960. {t("Confirm")}
  961. </Button>
  962. </DialogActions>
  963. </Dialog>
  964. </Box>
  965. );
  966. };
  967. export default ProductionProcessStepExecution;