FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

ProductionProcessStepExecution.tsx 40 KiB

1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1週間前
1ヶ月前
1週間前
1週間前
1週間前
1ヶ月前
1ヶ月前
1週間前
1ヶ月前
1週間前
1週間前
1週間前
1週間前
1ヶ月前
1週間前
1週間前
1週間前
1週間前
1ヶ月前
1ヶ月前
1週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1週間前
1ヶ月前
1週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1週間前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
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;