FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

1098 строки
43 KiB

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