FPSMS-frontend
Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

1076 linhas
42 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. 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;