"use client"; import React, { useCallback, useEffect, useState, useRef } from "react"; import EditIcon from "@mui/icons-material/Edit"; import AddIcon from '@mui/icons-material/Add'; import DeleteIcon from '@mui/icons-material/Delete'; import Fab from '@mui/material/Fab'; import { Box, Button, Paper, Stack, Typography, TextField, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Chip, Card, CardContent, CircularProgress, Dialog, DialogTitle, DialogContent, DialogActions, IconButton } from "@mui/material"; import QrCodeIcon from '@mui/icons-material/QrCode'; import { useTranslation } from "react-i18next"; import { Operator, Machine } from "@/app/api/jo"; import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import { // updateProductProcessLineQrscan, newUpdateProductProcessLineQrscan, fetchProductProcessLineDetail, JobOrderProcessLineDetailResponse, ProductProcessLineInfoResponse, startProductProcessLine, fetchProductProcessesByJobOrderId, ProductProcessWithLinesResponse, // 添加 ProductProcessLineResponse, passProductProcessLine, newProductProcessLine, updateProductProcessLineProcessingTimeSetupTimeChangeoverTime, UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest, deleteProductProcessLine, } from "@/app/api/jo/actions"; import { updateProductProcessLineStatus } from "@/app/api/jo/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; import ProductionProcessStepExecution from "./ProductionProcessStepExecution"; import ProductionOutputFormPage from "./ProductionOutputFormPage"; import ProcessSummaryHeader from "./ProcessSummaryHeader"; interface ProductProcessDetailProps { jobOrderId: number; onBack: () => void; fromJosave?: boolean; } const ProductionProcessDetail: React.FC = ({ jobOrderId, onBack, fromJosave, }) => { console.log(" ProductionProcessDetail RENDER", { jobOrderId, fromJosave }); const { t } = useTranslation("common"); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); const [showOutputPage, setShowOutputPage] = useState(false); // 基本信息 const [processData, setProcessData] = useState(null); // 修改类型 const [lines, setLines] = useState([]); // 修改类型 const [loading, setLoading] = useState(false); const linesRef = useRef([]); const onBackRef = useRef(onBack); const fetchProcessDetailRef = useRef<() => Promise>(); // 选中的 line 和执行状态 const [selectedLineId, setSelectedLineId] = useState(null); const [isExecutingLine, setIsExecutingLine] = useState(false); const [isAutoSubmitting, setIsAutoSubmitting] = useState(false); // 扫描器状态 const [isManualScanning, setIsManualScanning] = useState(false); const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [scannedOperatorId, setScannedOperatorId] = useState(null); const [scannedEquipmentId, setScannedEquipmentId] = useState(null); // const [scannedEquipmentTypeSubTypeEquipmentNo, setScannedEquipmentTypeSubTypeEquipmentNo] = useState(null); const [scannedStaffNo, setScannedStaffNo] = useState(null); // const [scannedEquipmentDetailId, setScannedEquipmentDetailId] = useState(null); const [scannedEquipmentCode, setScannedEquipmentCode] = useState(null); const [scanningLineId, setScanningLineId] = useState(null); const [lineDetailForScan, setLineDetailForScan] = useState(null); const [showScanDialog, setShowScanDialog] = useState(false); const autoSubmitTimerRef = useRef(null); const [openTimeDialog, setOpenTimeDialog] = useState(false); const [editingLineId, setEditingLineId] = useState(null); const [timeValues, setTimeValues] = useState({ durationInMinutes: 0, prepTimeInMinutes: 0, postProdTimeInMinutes: 0, }); const [outputData, setOutputData] = useState({ byproductName: "", byproductQty: "", byproductUom: "", scrapQty: "", scrapUom: "", defectQty: "", defectUom: "", outputFromProcessQty: "", outputFromProcessUom: "", }); // 处理 QR 码扫描 // 处理 QR 码扫描 const handleBackFromStep = async () => { await fetchProcessDetail(); // 重新拉取最新的 process/lines setIsExecutingLine(false); setSelectedLineId(null); setShowOutputPage(false); }; useEffect(() => { onBackRef.current = onBack; }, [onBack]); // 获取 process 和 lines 数据 const fetchProcessDetail = useCallback(async () => { console.log(" fetchProcessDetail CALLED", { jobOrderId, timestamp: new Date().toISOString() }); setLoading(true); try { console.log(` Loading process detail for JobOrderId: ${jobOrderId}`); const processesWithLines = await fetchProductProcessesByJobOrderId(jobOrderId); if (!processesWithLines || processesWithLines.length === 0) { throw new Error("No processes found for this job order"); } const currentProcess = processesWithLines[0]; setProcessData(currentProcess); const lines = currentProcess.productProcessLines || []; setLines(lines); linesRef.current = lines; console.log(" Process data loaded:", currentProcess); console.log(" Lines loaded:", lines); } catch (error) { console.error(" Error loading process detail:", error); onBackRef.current(); } finally { setLoading(false); } }, [jobOrderId]); const handleOpenTimeDialog = useCallback((lineId: number) => { console.log("🔓 handleOpenTimeDialog CALLED", { lineId, timestamp: new Date().toISOString() }); // 直接使用 linesRef.current,避免触发 setLines const line = linesRef.current.find(l => l.id === lineId); if (line) { console.log(" Found line:", line); setEditingLineId(lineId); setTimeValues({ durationInMinutes: line.durationInMinutes || 0, prepTimeInMinutes: line.prepTimeInMinutes || 0, postProdTimeInMinutes: line.postProdTimeInMinutes || 0, }); setOpenTimeDialog(true); console.log(" Dialog opened"); } else { console.warn(" Line not found:", lineId); } }, []); useEffect(() => { fetchProcessDetailRef.current = fetchProcessDetail; }, [fetchProcessDetail]); const handleCloseTimeDialog = useCallback(() => { console.log("🔒 handleCloseTimeDialog CALLED", { timestamp: new Date().toISOString() }); setOpenTimeDialog(false); setEditingLineId(null); setTimeValues({ durationInMinutes: 0, prepTimeInMinutes: 0, postProdTimeInMinutes: 0, }); console.log(" Dialog closed"); }, []); const handleConfirmTimeUpdate = useCallback(async () => { console.log("💾 handleConfirmTimeUpdate CALLED", { editingLineId, timeValues, timestamp: new Date().toISOString() }); if (!editingLineId) return; try { const request: UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest = { productProcessLineId: editingLineId, processingTime: timeValues.durationInMinutes, setupTime: timeValues.prepTimeInMinutes, changeoverTime: timeValues.postProdTimeInMinutes, }; await updateProductProcessLineProcessingTimeSetupTimeChangeoverTime(editingLineId, request); await fetchProcessDetail(); handleCloseTimeDialog(); } catch (error) { console.error("Error updating time:", error); alert(t("update failed")); } }, [editingLineId, timeValues, fetchProcessDetail, handleCloseTimeDialog, t]); useEffect(() => { console.log("🔄 useEffect [jobOrderId] TRIGGERED", { jobOrderId, timestamp: new Date().toISOString() }); if (fetchProcessDetailRef.current) { fetchProcessDetailRef.current(); } }, [jobOrderId]); // 添加监听 openTimeDialog 变化的 useEffect useEffect(() => { console.log(" openTimeDialog changed:", { openTimeDialog, timestamp: new Date().toISOString() }); }, [openTimeDialog]); // 添加监听 timeValues 变化的 useEffect useEffect(() => { console.log(" timeValues changed:", { timeValues, timestamp: new Date().toISOString() }); }, [timeValues]); // 添加监听 lines 变化的 useEffect useEffect(() => { console.log(" lines changed:", { count: lines.length, lines, timestamp: new Date().toISOString() }); }, [lines]); // 添加监听 editingLineId 变化的 useEffect useEffect(() => { console.log(" editingLineId changed:", { editingLineId, timestamp: new Date().toISOString() }); }, [editingLineId]); const handlePassLine = useCallback(async (lineId: number) => { try { await passProductProcessLine(lineId); // 刷新数据 await fetchProcessDetail(); } catch (error) { console.error("Error passing line:", error); alert(t("Failed to pass line. Please try again.")); } }, [fetchProcessDetail, t]); const handleCreateNewLine = useCallback(async (lineId: number) => { try { await newProductProcessLine(lineId); // 刷新数据 await fetchProcessDetail(); } catch (error) { console.error("Error creating new line:", error); alert(t("Failed to create new line. Please try again.")); } }, [fetchProcessDetail, t]); const handleDeleteLine = useCallback(async (lineId: number) => { if (!confirm(t("Are you sure you want to delete this process?"))) { return; } try { await deleteProductProcessLine(lineId); // 刷新数据 await fetchProcessDetail(); } catch (error) { console.error("Error deleting line:", error); alert(t("Failed to delete line. Please try again.")); } }, [fetchProcessDetail, t]); const processQrCode = useCallback((qrValue: string, lineId: number) => { // 设备快捷格式:{2fitesteXXX} - XXX 直接作为设备代码 // 格式:{2fitesteXXX} = equipmentCode: "XXX" // 例如:{2fiteste包裝機類-真空八爪魚機-1號} = equipmentCode: "包裝機類-真空八爪魚機-1號" if (qrValue.match(/\{2fiteste(.+)\}/)) { const match = qrValue.match(/\{2fiteste(.+)\}/); const equipmentCode = match![1]; setScannedEquipmentCode(equipmentCode); console.log(`Set equipmentCode from shortcut: ${equipmentCode}`); return; } // 员工编号格式:{2fitestu任何内容} - 直接作为 staffNo // 例如:{2fitestu123} = staffNo: "123" // 例如:{2fitestustaff001} = staffNo: "staff001" if (qrValue.match(/\{2fitestu(.+)\}/)) { const match = qrValue.match(/\{2fitestu(.+)\}/); const staffNo = match![1]; setScannedStaffNo(staffNo); return; } // 正常 QR 扫描器扫描格式 const trimmedValue = qrValue.trim(); // 检查 staffNo 格式:"staffNo: STAFF001" 或 "staffNo:STAFF001" const staffNoMatch = trimmedValue.match(/^staffNo:\s*(.+)$/i); if (staffNoMatch) { const staffNo = staffNoMatch[1].trim(); setScannedStaffNo(staffNo); return; } // 检查 equipmentCode 格式 const equipmentCodeMatch = trimmedValue.match(/^(?:equipmentTypeSubTypeEquipmentNo|EquipmentType-SubType-EquipmentNo|equipmentCode):\s*(.+)$/i); if (equipmentCodeMatch) { const equipmentCode = equipmentCodeMatch[1].trim(); setScannedEquipmentCode(equipmentCode); return; } // 其他格式处理(JSON、普通文本等) try { const qrData = JSON.parse(qrValue); if (qrData.staffNo) { setScannedStaffNo(String(qrData.staffNo)); } if (qrData.equipmentTypeSubTypeEquipmentNo || qrData.equipmentCode) { setScannedEquipmentCode( String(qrData.equipmentTypeSubTypeEquipmentNo ?? qrData.equipmentCode) ); } } catch { // 普通文本格式 - 尝试判断是 staffNo 还是 equipmentCode if (trimmedValue.length > 0) { if (trimmedValue.toUpperCase().startsWith("STAFF") || /^\d+$/.test(trimmedValue)) { // 可能是员工编号 setScannedStaffNo(trimmedValue); } else if (trimmedValue.includes("-")) { // 可能包含 "-" 的是设备代码(如 "包裝機類-真空八爪魚機-1號") setScannedEquipmentCode(trimmedValue); } } } }, [lines]); // 处理 QR 码扫描效果 useEffect(() => { if (isManualScanning && qrValues.length > 0 && scanningLineId) { const latestQr = qrValues[qrValues.length - 1]; if (processedQrCodes.has(latestQr)) { return; } setProcessedQrCodes(prev => new Set(prev).add(latestQr)); processQrCode(latestQr, scanningLineId); } }, [qrValues, isManualScanning, scanningLineId, processedQrCodes, processQrCode]); const submitScanAndStart = useCallback(async (lineId: number) => { console.log("submitScanAndStart called with:", { lineId, scannedStaffNo, // scannedEquipmentTypeSubTypeEquipmentNo, scannedEquipmentCode, }); if (!scannedStaffNo) { console.log("No staffNo, cannot submit"); setIsAutoSubmitting(false); return false; } try { const lineDetail = lineDetailForScan || await fetchProductProcessLineDetail(lineId); // 统一使用一个最终的 equipmentCode(优先用 scannedEquipmentCode,其次用 scannedEquipmentTypeSubTypeEquipmentNo) const effectiveEquipmentCode = scannedEquipmentCode ?? null; console.log("Submitting scan data with equipmentCode:", { productProcessLineId: lineId, staffNo: scannedStaffNo, equipmentCode: effectiveEquipmentCode, }); const response = await newUpdateProductProcessLineQrscan({ productProcessLineId: lineId, equipmentCode: effectiveEquipmentCode ?? "", staffNo: scannedStaffNo, }); console.log("Scan submit response:", response); if (response && response.type === "error") { console.error("Scan validation failed:", response.message); alert(t(response.message) || t("Validation failed. Please check your input.")); setIsAutoSubmitting(false); if (autoSubmitTimerRef.current) { clearTimeout(autoSubmitTimerRef.current); autoSubmitTimerRef.current = null; } return false; } console.log("Validation passed, starting line..."); handleStopScan(); setShowScanDialog(false); setIsAutoSubmitting(false); await handleStartLine(lineId); setSelectedLineId(lineId); setIsExecutingLine(true); await fetchProcessDetail(); return true; } catch (error) { console.error("Error submitting scan:", error); alert("Failed to submit scan data. Please try again."); setIsAutoSubmitting(false); return false; } }, [ scannedStaffNo, scannedEquipmentCode, lineDetailForScan, t, fetchProcessDetail, ]); const handleSubmitScanAndStart = useCallback(async (lineId: number) => { console.log("handleSubmitScanAndStart called with lineId:", lineId); if (!scannedStaffNo) { //alert(t("Please scan operator code first")); return; } // 如果正在自动提交,等待一下 if (isAutoSubmitting) { console.log("Already auto-submitting, skipping manual submit"); return; } await submitScanAndStart(lineId); }, [scannedOperatorId, isAutoSubmitting, submitScanAndStart, t]); // 开始扫描 const handleStartScan = useCallback((lineId: number) => { if (autoSubmitTimerRef.current) { clearTimeout(autoSubmitTimerRef.current); autoSubmitTimerRef.current = null; } setScanningLineId(lineId); setIsManualScanning(true); setProcessedQrCodes(new Set()); setScannedOperatorId(null); setScannedEquipmentId(null); setScannedStaffNo(null); // Add this setScannedEquipmentCode(null); setIsAutoSubmitting(false); // 添加:重置自动提交状态 setLineDetailForScan(null); // 获取 line detail 以获取 bomProcessEquipmentId fetchProductProcessLineDetail(lineId) .then(setLineDetailForScan) .catch(err => { console.error("Failed to load line detail", err); // 不阻止扫描继续,line detail 不是必需的 }); startScan(); }, [startScan]); // 停止扫描 const handleStopScan = useCallback(() => { console.log("🛑 Stopping scan"); // 清除定时器 if (autoSubmitTimerRef.current) { clearTimeout(autoSubmitTimerRef.current); autoSubmitTimerRef.current = null; } setIsManualScanning(false); setIsAutoSubmitting(false); setScannedStaffNo(null); // Add this setScannedEquipmentCode(null); stopScan(); resetScan(); }, [stopScan, resetScan]); // 开始执行某个 line(原有逻辑,现在在验证通过后调用) const handleStartLine = async (lineId: number) => { try { await startProductProcessLine(lineId); } catch (error) { console.error("Error starting line:", error); //alert("Failed to start line. Please try again."); } }; // 提交扫描结果并验证 /* useEffect(() => { console.log("Auto-submit check:", { scanningLineId, scannedStaffNo, scannedEquipmentCode, isAutoSubmitting, isManualScanning, }); // Update condition to check for either equipmentTypeSubTypeEquipmentNo OR equipmentDetailId if ( scanningLineId && scannedStaffNo !== null && (scannedEquipmentCode !== null) && !isAutoSubmitting && isManualScanning ) { console.log("Auto-submitting triggered!"); setIsAutoSubmitting(true); // 清除之前的定时器(如果有) if (autoSubmitTimerRef.current) { clearTimeout(autoSubmitTimerRef.current); } // 延迟一点时间,让用户看到两个都扫描完成了 autoSubmitTimerRef.current = setTimeout(() => { console.log("Executing auto-submit..."); submitScanAndStart(scanningLineId); autoSubmitTimerRef.current = null; }, 500); } // 清理函数:只在组件卸载或条件不再满足时清除定时器 return () => { // 注意:这里不立即清除定时器,因为我们需要它执行 // 只在组件卸载时清除 }; }, [scanningLineId, scannedStaffNo, scannedEquipmentCode, isAutoSubmitting, isManualScanning, submitScanAndStart]); */ useEffect(() => { return () => { if (autoSubmitTimerRef.current) { clearTimeout(autoSubmitTimerRef.current); } }; }, []); const handleStartLineWithScan = async (lineId: number) => { console.log("🚀 Starting line with scan for lineId:", lineId); // 确保状态完全重置 setIsAutoSubmitting(false); setScannedOperatorId(null); setScannedEquipmentId(null); setProcessedQrCodes(new Set()); setScannedStaffNo(null); setScannedEquipmentCode(null); setProcessedQrCodes(new Set()); // 清除之前的定时器 if (autoSubmitTimerRef.current) { clearTimeout(autoSubmitTimerRef.current); autoSubmitTimerRef.current = null; } setScanningLineId(lineId); setShowScanDialog(true); handleStartScan(lineId); }; const selectedLine = lines.find(l => l.id === selectedLineId); // 添加组件卸载日志 useEffect(() => { return () => { console.log("🗑️ ProductionProcessDetail UNMOUNTING"); }; }, []); if (loading) { return ( ); } return ( {/* ========== 第二部分:Process Lines ========== */} {t("Production Process Steps")} {!isExecutingLine ? ( /* ========== 步骤列表视图 ========== */ {t(" ")} {t("Seq")} {t("Step Name")} {t("Description")} {t("EquipmentType-EquipmentName-Code")} {t("Operator")} {t("Assume End Time")} {t("Time Information(mins)")} {t("Status")} {!fromJosave&&({t("Action")})} {lines.map((line) => { const status = (line as any).status || ''; const statusLower = status.toLowerCase(); const equipmentName = line.equipment_name || "-"; const isPlanning = processData?.jobOrderStatus === "planning"; const isCompleted = statusLower === 'completed'; const isInProgress = statusLower === 'inprogress' || statusLower === 'in progress'; const isPaused = statusLower === 'paused'; const isPending = statusLower === 'pending' || status === ''; const isPass = statusLower === 'pass'; const isPassDisabled = isCompleted || isPass; return ( {isPlanning && ( handleCreateNewLine(line.id)} sx={{ width: 32, height: 32, minHeight: 32, boxShadow: 1, '&:hover': { boxShadow: 3 }, }} > )} {isPlanning && line.isOringinal !== true && ( handleDeleteLine(line.id)} sx={{ padding: 0.5 }} > )} {line.seqNo} {line.name} {line.description || "-"} {line.equipmentDetailCode||equipmentName} {line.operatorName} {line.startTime && line.durationInMinutes ? dayjs(line.startTime) .add(line.durationInMinutes, 'minute') .format('MM-DD HH:mm') : '-'} {t("Processing Time")}: {line.durationInMinutes || 0}{t("mins")} {processData?.jobOrderStatus === "planning" && ( { console.log("🖱️ Edit button clicked for line:", line.id); handleOpenTimeDialog(line.id); }} sx={{ padding: 0.5 }} > )} {t("Setup Time")}: {line.prepTimeInMinutes || 0} {t("mins")} {t("Changeover Time")}: {line.postProdTimeInMinutes || 0} {t("mins")} {isCompleted ? ( { setSelectedLineId(line.id); setShowOutputPage(false); setIsExecutingLine(true); await fetchProcessDetail(); }} /> ) : isInProgress ? ( { setSelectedLineId(line.id); setShowOutputPage(false); setIsExecutingLine(true); await fetchProcessDetail(); }} /> ) : isPending ? ( ) : isPaused ? ( ) : isPass ? ( ) : ( ) } {!fromJosave&&( {statusLower === 'pending' ? ( <> ) : statusLower === 'in_progress' || statusLower === 'in progress' || statusLower === 'paused' ? ( <> ) : ( <> )} )} ); })}
) : ( /* ========== 步骤执行视图 ========== */ )}
{/* QR 扫描对话框 */} { handleStopScan(); setShowScanDialog(false); }} maxWidth="sm" fullWidth > {t("Scan Operator & Equipment")} {scannedStaffNo ? `${t("Staff No")}: ${scannedStaffNo}` : t("Please scan staff no") } {scannedEquipmentCode ? `${t("Equipment Code")}: ${scannedEquipmentCode}` : t("Please scan equipment code") } {t("Update Time Information")} { console.log("⌨️ Processing Time onChange:", { value: e.target.value, openTimeDialog, editingLineId, timestamp: new Date().toISOString() }); const value = e.target.value === '' ? 0 : parseInt(e.target.value) || 0; setTimeValues(prev => ({ ...prev, durationInMinutes: Math.max(0, value) })); }} inputProps={{ min: 0, step: 1 }} /> { console.log("⌨️ Setup Time onChange:", { value: e.target.value, openTimeDialog, editingLineId, timestamp: new Date().toISOString() }); const value = e.target.value === '' ? 0 : parseInt(e.target.value) || 0; setTimeValues(prev => ({ ...prev, prepTimeInMinutes: Math.max(0, value) })); }} inputProps={{ min: 0, step: 1 }} /> { console.log("⌨️ Changeover Time onChange:", { value: e.target.value, openTimeDialog, editingLineId, timestamp: new Date().toISOString() }); const value = e.target.value === '' ? 0 : parseInt(e.target.value) || 0; setTimeValues(prev => ({ ...prev, postProdTimeInMinutes: Math.max(0, value) })); }} inputProps={{ min: 0, step: 1 }} />
); }; export default ProductionProcessDetail;