"use client"; import { Box, Button, Paper, Stack, Typography, TextField, Table, TableBody, TableCell, TableHead, TableRow, Dialog, DialogTitle, DialogContent, DialogActions, Card, CardContent, Grid, } from "@mui/material"; import { Alert } from "@mui/material"; import QrCodeIcon from '@mui/icons-material/QrCode'; import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import StopIcon from "@mui/icons-material/Stop"; import PauseIcon from "@mui/icons-material/Pause"; import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import { useTranslation } from "react-i18next"; import { JobOrderProcessLineDetailResponse, updateProductProcessLineQty, updateProductProcessLineQrscan, fetchProductProcessLineDetail, UpdateProductProcessLineQtyRequest, saveProductProcessResumeTime, saveProductProcessIssueTime, ProductProcessWithLinesResponse, // ✅ 添加 ProductProcessLineResponse, // ✅ 添加 } from "@/app/api/jo/actions"; import { Operator, Machine } from "@/app/api/jo"; import React, { useCallback, useEffect, useState, useMemo } from "react"; // ✅ 添加 useMemo import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; import { fetchNameList, NameList } from "@/app/api/user/actions"; import BagConsumptionForm from "./BagConsumptionForm"; // ✅ 添加导入 import OverallTimeRemainingCard from "./OverallTimeRemainingCard"; // ✅ 添加导入 import dayjs from "dayjs"; interface ProductionProcessStepExecutionProps { lineId: number | null; onBack: () => void; processData?: ProductProcessWithLinesResponse | null; // ✅ 添加 allLines?: ProductProcessLineResponse[]; // ✅ 添加 jobOrderId?: number; // ✅ 添加 } const ProductionProcessStepExecution: React.FC = ({ lineId, onBack, processData, // ✅ 添加 allLines, // ✅ 添加 jobOrderId, // ✅ 添加 }) => { const { t } = useTranslation( ["common","jo"]); const [lineDetail, setLineDetail] = useState(null); const isCompleted = lineDetail?.status === "Completed" || lineDetail?.status === "Pass"; const [outputData, setOutputData] = useState({ productProcessLineId: lineId ?? 0, outputFromProcessQty: 0, outputFromProcessUom: "", defectQty: 0, defectUom: "", scrapQty: 0, scrapUom: "", byproductName: "", byproductQty: 0, byproductUom: "", defect2Qty: 0, defect2Uom: "", defect3Qty: 0, defect3Uom: "", defectDescription: "", defectDescription2: "", defectDescription3: "" }); const [isManualScanning, setIsManualScanning] = useState(false); const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [scannedOperators, setScannedOperators] = useState([]); const [scannedMachines, setScannedMachines] = useState([]); const [isPaused, setIsPaused] = useState(false); const [showOutputTable, setShowOutputTable] = useState(false); const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-"; const [remainingTime, setRemainingTime] = useState(null); const [isOverTime, setIsOverTime] = useState(false); const [frozenRemainingTime, setFrozenRemainingTime] = useState(null); const [lastPauseTime, setLastPauseTime] = useState(null); const[isOpenReasonModel, setIsOpenReasonModel] = useState(false); const [pauseReason, setPauseReason] = useState(""); // ✅ 添加:判断是否显示 Bag 表单的条件 const shouldShowBagForm = useMemo(() => { if (!processData || !allLines || !lineDetail) return false; // 检查 BOM description 是否为 "FG" const bomDescription = processData.bomDescription; if (bomDescription !== "FG") return false; // 检查是否是最后一个 process line(按 seqNo 排序) const sortedLines = [...allLines].sort((a, b) => (a.seqNo || 0) - (b.seqNo || 0)); const maxSeqNo = sortedLines[sortedLines.length - 1]?.seqNo; const isLastLine = lineDetail.seqNo === maxSeqNo; return isLastLine; }, [processData, allLines, lineDetail]); // ✅ 添加:刷新 line detail 的函数 const handleRefreshLineDetail = useCallback(async () => { if (lineId) { try { const detail = await fetchProductProcessLineDetail(lineId); setLineDetail(detail as any); } catch (error) { console.error("Failed to refresh line detail", error); } } }, [lineId]); useEffect(() => { if (!lineId) { setLineDetail(null); return; } fetchProductProcessLineDetail(lineId) .then((detail) => { setLineDetail(detail as any); console.log("📋 Line Detail loaded:", { id: detail.id, status: detail.status, durationInMinutes: detail.durationInMinutes, startTime: detail.startTime, startTimeType: typeof detail.startTime, hasDuration: !!detail.durationInMinutes, hasStartTime: !!detail.startTime, }); setOutputData(prev => ({ ...prev, productProcessLineId: detail.id, outputFromProcessQty: (detail as any).outputFromProcessQty || 0, outputFromProcessUom: (detail as any).outputFromProcessUom || "", defectQty: detail.defectQty || 0, defectUom: detail.defectUom || "", scrapQty: detail.scrapQty || 0, scrapUom: detail.scrapUom || "", byproductName: detail.byproductName || "", byproductQty: detail.byproductQty || 0, byproductUom: detail.byproductUom || "" })); }) .catch(err => { console.error("Failed to load line detail", err); setLineDetail(null); }); }, [lineId]); useEffect(() => { // Don't show time remaining if completed if (lineDetail?.status === "Completed" || lineDetail?.status === "Pass") { console.log("Line is completed"); setRemainingTime(null); setIsOverTime(false); return; } console.log("🔍 Time Remaining Debug:", { lineId: lineDetail?.id, equipmentId: lineDetail?.equipmentId, equipmentType: lineDetail?.equipmentType, durationInMinutes: lineDetail?.durationInMinutes, startTime: lineDetail?.startTime, startTimeType: typeof lineDetail?.startTime, isStartTimeArray: Array.isArray(lineDetail?.startTime), status: lineDetail?.status, hasDuration: !!lineDetail?.durationInMinutes, hasStartTime: !!lineDetail?.startTime, }); if (!lineDetail?.durationInMinutes || !lineDetail?.startTime) { console.log("❌ Line duration or start time is not valid", { durationInMinutes: lineDetail?.durationInMinutes, startTime: lineDetail?.startTime, equipmentId: lineDetail?.equipmentId, equipmentType: lineDetail?.equipmentType, }); setRemainingTime(null); setIsOverTime(false); return; } let start: Date; if (Array.isArray(lineDetail.startTime)) { console.log("Line start time is an array:", lineDetail.startTime); const [year, month, day, hour = 0, minute = 0, second = 0] = lineDetail.startTime; start = new Date(year, month - 1, day, hour, minute, second); } else { start = new Date(lineDetail.startTime); console.log("Line start time is a string:", lineDetail.startTime); } if (isNaN(start.getTime())) { console.error("Invalid startTime:", lineDetail.startTime); setRemainingTime(null); setIsOverTime(false); return; } const durationMs = lineDetail.durationInMinutes * 60_000; const isPaused = lineDetail.status === "Paused" || lineDetail.productProcessIssueStatus === "Paused"; const parseStopTime = (stopTime: string | number[] | undefined): Date | null => { if (!stopTime) return null; if (Array.isArray(stopTime)) { const [year, month, day, hour = 0, minute = 0, second = 0] = stopTime; return new Date(year, month - 1, day, hour, minute, second); } else { return new Date(stopTime); } }; const update = () => { if (isPaused) { if (!frozenRemainingTime) { const pauseTime = lineDetail.stopTime ? parseStopTime(lineDetail.stopTime) : null; const pauseTimeToUse = pauseTime && !isNaN(pauseTime.getTime()) ? pauseTime : new Date(); const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0; console.log("⏸️ Paused - calculating frozen time:", { stopTime: lineDetail.stopTime, pauseTime: pauseTimeToUse, startTime: start, totalPausedTimeMs: totalPausedTimeMs, }); const elapsed = pauseTimeToUse.getTime() - start.getTime() - totalPausedTimeMs; const remaining = durationMs - elapsed; if (remaining <= 0) { const overTime = Math.abs(remaining); const minutes = Math.floor(overTime / 60000).toString().padStart(2, "0"); const seconds = Math.floor((overTime % 60000) / 1000).toString().padStart(2, "0"); const frozenValue = `-${minutes}:${seconds}`; setFrozenRemainingTime(frozenValue); setRemainingTime(frozenValue); setIsOverTime(true); console.log("⏸️ Frozen time (overtime):", frozenValue); } else { const minutes = Math.floor(remaining / 60000).toString().padStart(2, "0"); const seconds = Math.floor((remaining % 60000) / 1000).toString().padStart(2, "0"); const frozenValue = `${minutes}:${seconds}`; setFrozenRemainingTime(frozenValue); setRemainingTime(frozenValue); setIsOverTime(false); console.log("⏸️ Frozen time:", frozenValue); } } else { setRemainingTime(frozenRemainingTime); console.log("⏸️ Using frozen time:", frozenRemainingTime); } return; } if (frozenRemainingTime && !isPaused) { console.log("▶️ Resumed - clearing frozen time"); setFrozenRemainingTime(null); setLastPauseTime(null); } const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0; const now = new Date(); const elapsed = now.getTime() - start.getTime() - totalPausedTimeMs; const remaining = durationMs - elapsed; console.log("⏱️ Time calculation:", { now: now, start: start, totalPausedTimeMs: totalPausedTimeMs, elapsed: elapsed, remaining: remaining, durationMs: durationMs, }); if (remaining <= 0) { const overTime = Math.abs(remaining); const minutes = Math.floor(overTime / 60000).toString().padStart(2, "0"); const seconds = Math.floor((overTime % 60000) / 1000).toString().padStart(2, "0"); setRemainingTime(`-${minutes}:${seconds}`); setIsOverTime(true); } else { const minutes = Math.floor(remaining / 60000).toString().padStart(2, "0"); const seconds = Math.floor((remaining % 60000) / 1000).toString().padStart(2, "0"); setRemainingTime(`${minutes}:${seconds}`); setIsOverTime(false); } }; update(); if (!isPaused) { const timer = setInterval(update, 1000); return () => clearInterval(timer); } }, [lineDetail?.durationInMinutes, lineDetail?.startTime, lineDetail?.status, lineDetail?.productProcessIssueStatus, lineDetail?.stopTime, frozenRemainingTime]); useEffect(() => { const wasPaused = lineDetail?.status === "Paused" || lineDetail?.productProcessIssueStatus === "Paused"; const isNowInProgress = lineDetail?.status === "InProgress"; if (wasPaused && isNowInProgress && frozenRemainingTime) { setFrozenRemainingTime(null); } }, [lineDetail?.status, lineDetail?.productProcessIssueStatus]); const handleSubmitOutput = async () => { if (!lineDetail?.id) return; try { await updateProductProcessLineQty({ productProcessLineId: lineDetail?.id || 0 as number, byproductName: outputData.byproductName, byproductQty: outputData.byproductQty, byproductUom: outputData.byproductUom, outputFromProcessQty: outputData.outputFromProcessQty, outputFromProcessUom: outputData.outputFromProcessUom, defectQty: outputData.defectQty, defectUom: outputData.defectUom, defect2Qty: outputData.defect2Qty, defect2Uom: outputData.defect2Uom, defect3Qty: outputData.defect3Qty, defect3Uom: outputData.defect3Uom, defectDescription: outputData.defectDescription, defectDescription2: outputData.defectDescription2, defectDescription3: outputData.defectDescription3, scrapQty: outputData.scrapQty, scrapUom: outputData.scrapUom, }); console.log(" Output data submitted successfully"); fetchProductProcessLineDetail(lineDetail.id) .then((detail) => { console.log("Line Detail loaded:", { id: detail.id, status: detail.status, startTime: detail.startTime, durationInMinutes: detail.durationInMinutes, productProcessIssueStatus: detail.productProcessIssueStatus }); setLineDetail(detail as any); setOutputData(prev => ({ ...prev, productProcessLineId: detail.id, outputFromProcessQty: (detail as any).outputFromProcessQty || 0, outputFromProcessUom: (detail as any).outputFromProcessUom || "", defectQty: detail.defectQty || 0, defectUom: detail.defectUom || "", defectDescription: detail.defectDescription || "", defectDescription2: detail.defectDescription2 || "", defectDescription3: detail.defectDescription3 || "", defectQty2: detail.defectQty2 || 0, defectUom2: detail.defectUom2 || "", defectQty3: detail.defectQty3 || 0, defectUom3: detail.defectUom3 || "", scrapQty: detail.scrapQty || 0, scrapUom: detail.scrapUom || "", byproductName: detail.byproductName || "", byproductQty: detail.byproductQty || 0, byproductUom: detail.byproductUom || "" })); }) .catch(err => { console.error("Failed to load line detail", err); setLineDetail(null); }); } catch (error) { console.error("Error submitting output:", error); alert("Failed to submit output data. Please try again."); } }; useEffect(() => { if (isManualScanning && qrValues.length > 0 && lineDetail?.id) { const latestQr = qrValues[qrValues.length - 1]; if (processedQrCodes.has(latestQr)) { return; } setProcessedQrCodes(prev => new Set(prev).add(latestQr)); } }, [qrValues, isManualScanning, lineDetail?.id, processedQrCodes]); const lineAssumeEndTime = useMemo(() => { if (!lineDetail?.startTime || !lineDetail?.durationInMinutes) return null; // 解析 startTime(可能是数组或字符串) let start: dayjs.Dayjs; if (Array.isArray(lineDetail.startTime)) { const [year, month, day, hour = 0, minute = 0, second = 0] = lineDetail.startTime; start = dayjs(new Date(year, month - 1, day, hour, minute, second)); } else if (typeof lineDetail.startTime === 'string') { // 检查是否是 "MM-DD HH:mm" 格式 const mmddHhmmPattern = /^(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2})$/; const match = lineDetail.startTime.match(mmddHhmmPattern); if (match) { const month = parseInt(match[1], 10); const day = parseInt(match[2], 10); const hour = parseInt(match[3], 10); const minute = parseInt(match[4], 10); // 使用当前年份,但如果跨年(startTime 是年末,当前是年初),使用上一年 const now = dayjs(); let year = now.year(); if (month === 12 && day >= 20 && now.month() === 0 && now.date() <= 10) { year = now.year() - 1; } start = dayjs(new Date(year, month - 1, day, hour, minute, 0)); } else { start = dayjs(lineDetail.startTime); } } else { start = dayjs(lineDetail.startTime as any); } if (!start.isValid()) return null; return start.add(lineDetail.durationInMinutes, 'minute'); }, [lineDetail?.startTime, lineDetail?.durationInMinutes]); const lineStartTime = useMemo(() => { if (!lineDetail?.startTime) return null; let start: dayjs.Dayjs; if (Array.isArray(lineDetail.startTime)) { const [year, month, day, hour = 0, minute = 0, second = 0] = lineDetail.startTime; start = dayjs(new Date(year, month - 1, day, hour, minute, second)); } else if (typeof lineDetail.startTime === 'string') { const mmddHhmmPattern = /^(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2})$/; const match = lineDetail.startTime.match(mmddHhmmPattern); if (match) { const month = parseInt(match[1], 10); const day = parseInt(match[2], 10); const hour = parseInt(match[3], 10); const minute = parseInt(match[4], 10); const now = dayjs(); let year = now.year(); if (month === 12 && day >= 20 && now.month() === 0 && now.date() <= 10) { year = now.year() - 1; } start = dayjs(new Date(year, month - 1, day, hour, minute, 0)); } else { start = dayjs(lineDetail.startTime); } } else { start = dayjs(lineDetail.startTime as any); } return start.isValid() ? start : null; }, [lineDetail?.startTime]); const handleOpenReasonModel = () => { setIsOpenReasonModel(true); setPauseReason(""); }; const handleCloseReasonModel = () => { setIsOpenReasonModel(false); setPauseReason(""); }; const handleSaveReason = async () => { if (!pauseReason.trim()) { alert(t("Please enter a reason for pausing")); return; } if (!lineDetail?.id) return; try { await saveProductProcessIssueTime({ productProcessLineId: lineDetail.id, reason: pauseReason.trim() }); setIsOpenReasonModel(false); setPauseReason(""); fetchProductProcessLineDetail(lineDetail.id) .then((detail) => { setLineDetail(detail as any); }) .catch(err => { console.error("Failed to load line detail", err); }); } catch (error) { console.error("Error saving pause reason:", error); alert(t("Failed to pause. Please try again.")); } }; const handleResume = async () => { if (!lineDetail?.productProcessIssueId) { console.error("No productProcessIssueId found"); return; } try { await saveProductProcessResumeTime(lineDetail.productProcessIssueId); console.log("✅ Resume API called successfully"); if (lineDetail?.id) { fetchProductProcessLineDetail(lineDetail.id) .then((detail) => { console.log("✅ Line detail refreshed after resume:", detail); setLineDetail(detail as any); setFrozenRemainingTime(null); setLastPauseTime(null); }) .catch(err => { console.error("❌ Failed to load line detail after resume", err); }); } } catch (error) { console.error("❌ Error resuming:", error); alert(t("Failed to resume. Please try again.")); } }; return ( {processData && ( )} {isCompleted ? ( {lineDetail?.status === "Pass" ? ( {t("Passed Step")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo}) ) : ( {t("Completed Step")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo}) )} {t("Step Information")} {t("Description")}: {lineDetail?.description || "-"} {t("Operator")}: {lineDetail?.operatorName || "-"} {t("Equipment")}: {equipmentName} {t("Status")}: {t(lineDetail?.status || "-")} {t("Production Output Data")} {t("Type")} {t("Quantity")} {t("Unit")} {t("Description")} {t("Output from Process")} {lineDetail?.outputFromProcessQty || 0} {lineDetail?.outputFromProcessUom || "-"} {t("Defect")} {lineDetail.defectQty} {lineDetail.defectUom || "-"} {lineDetail.defectDescription || "-"} {t("Defect")}{t("(3)")} {lineDetail.defectQty3} {lineDetail.defectUom3 || "-"} {lineDetail.defectDescription3 || "-"} {t("Defect")}{t("(2)")} {lineDetail.defectQty2} {lineDetail.defectUom2 || "-"} {lineDetail.defectDescription2 || "-"} {t("Scrap")} {lineDetail.scrapQty} {lineDetail.scrapUom || "-"}
) : ( <> {!showOutputTable && ( {t("Executing")}: {lineDetail?.name} ({t("Seq")}:{lineDetail?.seqNo}) {lineDetail?.description} {t("Operator")}: {lineDetail?.operatorName || "-"} {t("Equipment")}: {equipmentName} {!isCompleted && remainingTime !== null && ( {t("Time Remaining")} {isOverTime ? `${t("Over Time")}: ${remainingTime}` : remainingTime} {/* ✅ 添加:Process Start Time 和 Assume End Time */} {/* ✅ 添加:Process Start Time 和 Assume End Time */} {processData?.startTime && ( {t("Process Start Time")}: {dayjs(processData.startTime).format("MM-DD")} {dayjs(processData.startTime).format("HH:mm")} )} {lineStartTime && ( {t("Step Start Time")}: {lineStartTime.format("MM-DD")} {lineStartTime.format("HH:mm")} {lineAssumeEndTime && ( {t("Assume End Time")}: {lineAssumeEndTime.format("MM-DD")} {lineAssumeEndTime.format("HH:mm")} )} )} {lineDetail?.status === "Paused" && ( {t("Timer Paused")} )} )} { lineDetail?.status === 'InProgress'? ( ) : ( )} )} {/* ========== 产出输入表单 ========== */} {showOutputTable && ( {t("Type")} {t("Quantity")} {t("Unit")} {t(" ")} {t("Output from Process")} setOutputData({ ...outputData, outputFromProcessQty: parseInt(e.target.value) || 0 })} /> setOutputData({ ...outputData, outputFromProcessUom: e.target.value })} /> {t("Description")} {t("Defect")}{t("(1)")} setOutputData({ ...outputData, defectQty: parseInt(e.target.value) || 0 })} /> setOutputData({ ...outputData, defectUom: e.target.value })} /> setOutputData({ ...outputData, defectDescription: e.target.value })} /> {t("Defect")}{t("(2)")} setOutputData({ ...outputData, defect2Qty: parseInt(e.target.value) || 0 })} /> setOutputData({ ...outputData, defect2Uom: e.target.value })} /> setOutputData({ ...outputData, defectDescription2: e.target.value })} /> {t("Defect")}{t("(3)")} setOutputData({ ...outputData, defect3Qty: parseInt(e.target.value) || 0 })} /> setOutputData({ ...outputData, defect3Uom: e.target.value })} /> setOutputData({ ...outputData, defectDescription3: e.target.value })} /> {t("Scrap")} setOutputData({ ...outputData, scrapQty: parseInt(e.target.value) || 0 })} /> setOutputData({ ...outputData, scrapUom: e.target.value })} />
)} {/* ========== Bag Consumption Form ========== */} {((showOutputTable || isCompleted) && shouldShowBagForm && jobOrderId && lineId) && ( )} )} {t("Pause Reason")} setPauseReason(e.target.value)} />
); }; export default ProductionProcessStepExecution;