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

ProductionProcessStepExecution.tsx 42 KiB

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