From 2575dbc0990655acdbdd1754b2d8e4b027173347 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Sun, 14 Dec 2025 22:35:28 +0800 Subject: [PATCH] update 1-9 --- src/app/api/bom/index.ts | 1 + src/app/api/jo/actions.ts | 14 +- src/components/JoSearch/JoCreateFormModal.tsx | 53 +- src/components/JoSearch/JoSearch.tsx | 3 +- src/components/JoSearch/JoSearchWrapper.tsx | 6 + .../Jodetail/JobPickExecutionsecondscan.tsx | 952 +++++++++--------- .../PickOrderSearch/PickExecution.tsx | 2 +- .../ProcessSummaryHeader.tsx | 3 +- .../ProductionProcessDetail.tsx | 17 +- .../ProductionProcessJobOrderDetail.tsx | 21 +- .../ProductionProcessList.tsx | 53 +- .../ProductionProcessPage.tsx | 22 +- src/i18n/zh/common.json | 5 +- src/i18n/zh/jo.json | 5 +- 14 files changed, 644 insertions(+), 513 deletions(-) diff --git a/src/app/api/bom/index.ts b/src/app/api/bom/index.ts index 3e4ec62..9e8e3ff 100644 --- a/src/app/api/bom/index.ts +++ b/src/app/api/bom/index.ts @@ -7,6 +7,7 @@ export interface BomCombo { value: number; label: string; outputQty: number; + outputQtyUom: string; } export const preloadBomCombo = (() => { diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 4b782d1..86d0de4 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -248,6 +248,8 @@ export interface ProductProcessWithLinesResponse { isDark: string; isDense: number; isFloat: string; + timeSequence: number; + complexity: number; scrapRate: number; allergicSubstance: string; itemId: number; @@ -315,6 +317,8 @@ export interface AllJoborderProductProcessInfoResponse { endTime?: string; date: string; bomId?: number; + assignedTo: number; + pickOrderId: number; itemName: string; requiredQty: number; jobOrderId: number; @@ -834,7 +838,15 @@ export const assignJobOrderPickOrder = async (pickOrderId: number, userId: numbe } ); }; - +export const unAssignJobOrderPickOrder = async (pickOrderId: number) => { + return serverFetchJson( + `${BASE_API_URL}/jo/unassign-job-order-pick-order/${pickOrderId}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + } + ); +}; // 获取 Job Order 分层数据 export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => { return serverFetchJson( diff --git a/src/components/JoSearch/JoCreateFormModal.tsx b/src/components/JoSearch/JoCreateFormModal.tsx index 99a1b6f..62c508c 100644 --- a/src/components/JoSearch/JoCreateFormModal.tsx +++ b/src/components/JoSearch/JoCreateFormModal.tsx @@ -3,7 +3,7 @@ import { JoDetail } from "@/app/api/jo"; import { SaveJo, manualCreateJo } from "@/app/api/jo/actions"; import { OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dateStringToDayjs, dayjsToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil"; import { Check } from "@mui/icons-material"; -import { Autocomplete, Box, Button, Card, Grid, Modal, Stack, TextField, Typography ,FormControl, InputLabel, Select, MenuItem} from "@mui/material"; +import { Autocomplete, Box, Button, Card, Grid, Modal, Stack, TextField, Typography ,FormControl, InputLabel, Select, MenuItem,InputAdornment} from "@mui/material"; import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs, { Dayjs } from "dayjs"; @@ -66,7 +66,7 @@ const JoCreateFormModal: React.FC = ({ msg(t("update success")); onModalClose(); } - }, []) + }, [onSearch, onModalClose, t]) const onSubmitError = useCallback>((error) => { console.log(error) @@ -166,25 +166,36 @@ const JoCreateFormModal: React.FC = ({ required: "Req. Qty. required!", validate: (value) => value > 0 }} - render={({ field, fieldState: { error } }) => ( - { - const val = e.target.value === "" ? undefined : Number(e.target.value); - field.onChange(val); - }} - /> - )} + render={({ field, fieldState: { error } }) => { + const selectedBom = bomCombo.find(bom => bom.id === formProps.watch("bomId")); + const uom = selectedBom?.outputQtyUom || ""; + + return ( + { + const val = e.target.value === "" ? undefined : Number(e.target.value); + field.onChange(val); + }} + InputProps={{ + endAdornment: uom ? ( + + + {uom} + + + ) : null + }} + /> + ); + }} /> diff --git a/src/components/JoSearch/JoSearch.tsx b/src/components/JoSearch/JoSearch.tsx index e5e4a41..6c577de 100644 --- a/src/components/JoSearch/JoSearch.tsx +++ b/src/components/JoSearch/JoSearch.tsx @@ -400,7 +400,8 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT bomCombo={bomCombo} onClose={onCloseCreateJoModal} onSearch={() => { - + setInputs(defaultInputs); + setPagingController(defaultPagingController); }} /> diff --git a/src/components/JoSearch/JoSearchWrapper.tsx b/src/components/JoSearch/JoSearchWrapper.tsx index 256255b..68e894b 100644 --- a/src/components/JoSearch/JoSearchWrapper.tsx +++ b/src/components/JoSearch/JoSearchWrapper.tsx @@ -10,9 +10,15 @@ interface SubComponents { } const JoSearchWrapper: React.FC & SubComponents = async () => { + const today = new Date(); + const todayStr = today.toISOString().split('T')[0]; + const defaultInputs: SearchJoResultRequest = { code: "", itemName: "", + planStart: `${todayStr}T00:00`, + planStartTo: `${todayStr}T23:59:59`, + } const [ diff --git a/src/components/Jodetail/JobPickExecutionsecondscan.tsx b/src/components/Jodetail/JobPickExecutionsecondscan.tsx index 688e3b4..deb22ce 100644 --- a/src/components/Jodetail/JobPickExecutionsecondscan.tsx +++ b/src/components/Jodetail/JobPickExecutionsecondscan.tsx @@ -23,16 +23,15 @@ import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider'; import { useCallback, useEffect, useState, useRef, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/navigation"; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; // 修改:使用 Job Order API import { fetchCompletedJobOrderPickOrders, - fetchUnassignedJobOrderPickOrders, - assignJobOrderPickOrder, updateSecondQrScanStatus, submitSecondScanQuantity, - recordSecondScanIssue - + recordSecondScanIssue, + unAssignJobOrderPickOrder } from "@/app/api/jo/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; import { @@ -48,10 +47,10 @@ import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; import GoodPickExecutionForm from "./JobmatchForm"; -import FGPickOrderCard from "./FGPickOrderCard"; - +import { BASE_API_URL } from "@/config/api"; interface Props { filterArgs: Record; + onBack?: () => void; // 添加返回回调 } // QR Code Modal Component (from GoodPickExecution) @@ -65,17 +64,15 @@ const QrCodeModal: React.FC<{ const { t } = useTranslation("jo"); const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); const [manualInput, setManualInput] = useState(''); - const [manualInputSubmitted, setManualInputSubmitted] = useState(false); const [manualInputError, setManualInputError] = useState(false); const [isProcessingQr, setIsProcessingQr] = useState(false); const [qrScanFailed, setQrScanFailed] = useState(false); const [qrScanSuccess, setQrScanSuccess] = useState(false); - const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [scannedQrResult, setScannedQrResult] = useState(''); const { data: session } = useSession() as { data: SessionWithTokens | null }; - const currentUserId = session?.id ? parseInt(session.id) : + // Process scanned QR codes useEffect(() => { if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) { @@ -309,29 +306,25 @@ const QrCodeModal: React.FC<{ ); }; -const JobPickExecution: React.FC = ({ filterArgs }) => { +const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { const { t } = useTranslation("jo"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; - + const authTokenRef = useRef(null); // 修改:使用 Job Order 数据结构 const [jobOrderData, setJobOrderData] = useState(null); const [combinedLotData, setCombinedLotData] = useState([]); const [combinedDataLoading, setCombinedDataLoading] = useState(false); const [originalCombinedData, setOriginalCombinedData] = useState([]); - // 添加未分配订单状态 - const [unassignedOrders, setUnassignedOrders] = useState([]); - const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); - const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); const [qrScanInput, setQrScanInput] = useState(''); const [qrScanError, setQrScanError] = useState(false); const [qrScanSuccess, setQrScanSuccess] = useState(false); - + const unassignExecutedRef = useRef(false); const [pickQtyData, setPickQtyData] = useState>({}); const [searchQuery, setSearchQuery] = useState>({}); const [isSubmittingAll, setIsSubmittingAll] = useState(false); @@ -343,7 +336,6 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { const [usernameList, setUsernameList] = useState([]); const initializationRef = useRef(false); - const autoAssignRef = useRef(false); const formProps = useForm(); const errors = formProps.formState.errors; @@ -356,56 +348,62 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState(null); // Add these missing state variables - const [isManualScanning, setIsManualScanning] = useState(false); - const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); - const [lastProcessedQr, setLastProcessedQr] = useState(''); - const [isRefreshingData, setIsRefreshingData] = useState(false); + const [isManualScanning, setIsManualScanning] = useState(false); + const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); + const [lastProcessedQr, setLastProcessedQr] = useState(''); + const [isRefreshingData, setIsRefreshingData] = useState(false); + const [currentPickOrderId, setCurrentPickOrderId] = useState(null); + + // 添加:unassign 函数 + const handleUnassign = useCallback(async (pickOrderId: number | null) => { + if (!pickOrderId || !currentUserId) { + console.log("No pickOrderId or userId to unassign"); + return; + } - // 修改:加载未分配的 Job Order 订单 - const loadUnassignedOrders = useCallback(async () => { - setIsLoadingUnassigned(true); try { - const orders = await fetchUnassignedJobOrderPickOrders(); - setUnassignedOrders(orders); + console.log(`🔄 Unassigning pick order: ${pickOrderId} for user: ${currentUserId}`); + await unAssignJobOrderPickOrder(pickOrderId); + console.log(`✅ Successfully unassigned pick order: ${pickOrderId}`); } catch (error) { - console.error("Error loading unassigned orders:", error); - } finally { - setIsLoadingUnassigned(false); + console.error(`❌ Error unassigning pick order ${pickOrderId}:`, error); + // 即使 unassign 失败,也继续执行,不阻塞用户操作 } - }, []); - - // 修改:分配订单给当前用户 - const handleAssignOrder = useCallback(async (pickOrderId: number) => { - if (!currentUserId) { - console.error("Missing user id in session"); + }, [currentUserId]); + useEffect(() => { + if (session && (session as any).accessToken) { + authTokenRef.current = (session as any).accessToken; + } + }, [session]); + + const handleUnassignSync = useCallback((pickOrderId: number | null) => { + if (!pickOrderId || !currentUserId) { return; } - try { - const result = await assignJobOrderPickOrder(pickOrderId, currentUserId); - if (result.message === "Successfully assigned") { - console.log(" Successfully assigned pick order"); - // 刷新数据 - window.dispatchEvent(new CustomEvent('pickOrderAssigned')); - // 重新加载未分配订单列表 - loadUnassignedOrders(); - } else { - console.warn("⚠️ Assignment failed:", result.message); - alert(`Assignment failed: ${result.message}`); - } - } catch (error) { - console.error("❌ Error assigning order:", error); - alert("Error occurred during assignment"); + const url = `${BASE_API_URL}/jo/unassign-job-order-pick-order/${pickOrderId}`; + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (authTokenRef.current) { + headers['Authorization'] = `Bearer ${authTokenRef.current}`; } - }, [currentUserId, loadUnassignedOrders]); - - - // Handle QR code button click - const handleQrCodeClick = (pickOrderId: number) => { - console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); - // TODO: Implement QR code functionality - }; - + + fetch(url, { + method: 'POST', + headers: headers, + keepalive: true, + body: JSON.stringify({}) + }).catch((error) => { + console.error(`❌ Error in sync unassign:`, error); + // 备用方案:sendBeacon(但无法发送自定义 headers) + if (navigator.sendBeacon) { + const blob = new Blob([JSON.stringify({})], { type: 'application/json' }); + navigator.sendBeacon(url, blob); + } + }); + }, [currentUserId]); // 修改:使用 Job Order API 获取数据 const fetchJobOrderData = useCallback(async (userId?: number) => { setCombinedDataLoading(true); @@ -419,6 +417,7 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { setJobOrderData(null); setCombinedLotData([]); setOriginalCombinedData([]); + setCurrentPickOrderId(null); return; } window.dispatchEvent(new CustomEvent('jobOrderDataStatus', { @@ -431,10 +430,14 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { const jobOrderData = await fetchCompletedJobOrderPickOrders(userIdToUse); console.log(" Job Order data:", jobOrderData); console.log(" Pick Order Code from API:", jobOrderData.pickOrder?.code); - console.log(" Expected Pick Order Code: P-20251003-001"); setJobOrderData(jobOrderData); + // 保存 pickOrderId + if (jobOrderData?.pickOrder?.id) { + setCurrentPickOrderId(jobOrderData.pickOrder.id); + } + // Transform hierarchical data to flat structure for the table const flatLotData: any[] = []; @@ -508,22 +511,29 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { detail: { allLotsCompleted: allCompleted, - tabIndex: 1 - } - })); - // 发送完成状态事件,包含标签页信息 - window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { - detail: { - allLotsCompleted: allCompleted, - tabIndex: 0 // 明确指定这是来自标签页 0 的事件 + tabIndex: 0 } })); - } catch (error) { + } catch (error: any) { console.error("❌ Error fetching job order data:", error); + + // 检查是否是 token 错误或认证错误 + const isAuthError = error?.status === 401 || + error?.status === 403 || + error?.message?.toLowerCase().includes('unauthorized') || + error?.message?.toLowerCase().includes('token'); + + if (isAuthError) { + console.log("🔒 Authentication error detected, unassigning pick order"); + // Token 错误时也要 unassign + await handleUnassign(currentPickOrderId); + } + setJobOrderData(null); setCombinedLotData([]); setOriginalCombinedData([]); + setCurrentPickOrderId(null); // 如果加载失败,禁用打印按钮 window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { @@ -535,10 +545,71 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { } finally { setCombinedDataLoading(false); } - }, [currentUserId]); + }, [currentUserId, currentPickOrderId, handleUnassign]); + + + useEffect(() => { + unassignExecutedRef.current = false; + + const executeUnassign = () => { + if (unassignExecutedRef.current || !currentPickOrderId || !currentUserId) { + return; + } + unassignExecutedRef.current = true; + console.log("🔄 Executing unassign:", currentPickOrderId); + handleUnassignSync(currentPickOrderId); + }; + + const handleVisibilityChange = () => { + if (document.visibilityState === 'hidden') { + // 页面隐藏时(可能是刷新或登出),执行 unassign + executeUnassign(); + } + }; + + const handleBeforeUnload = () => { + // beforeunload 中无法执行异步操作,使用同步版本 + if (currentPickOrderId && currentUserId) { + console.log("🚪 Page unloading, attempting to unassign pick order:", currentPickOrderId); + executeUnassign(); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + window.removeEventListener('beforeunload', handleBeforeUnload); + + // 组件卸载时执行 unassign(但只在未执行过的情况下) + if (!unassignExecutedRef.current && currentPickOrderId && currentUserId) { + executeUnassign(); + } + + // Cleanup QR scanner + if (isManualScanning) { + console.log("🧹 Second scan component unmounting, stopping QR scanner..."); + stopScan(); + resetScan(); + } + }; + }, [currentPickOrderId, currentUserId, isManualScanning, handleUnassignSync, stopScan, resetScan]); + + // 添加:处理返回按钮 + const handleBack = useCallback(async () => { + if (currentPickOrderId && currentUserId) { + console.log("🔄 Back button clicked, unassigning pick order:", currentPickOrderId); + await handleUnassign(currentPickOrderId); + } + if (onBack) { + onBack(); + } + }, [currentPickOrderId, currentUserId, handleUnassign, onBack]); + const handleSubmitAllScanned = useCallback(async () => { const scannedLots = combinedLotData.filter(lot => - lot.matchStatus === 'scanned' // Only submit items that are scanned but not yet submitted + lot.matchStatus === 'scanned' ); if (scannedLots.length === 0) { @@ -550,7 +621,6 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`); try { - // Submit all items in parallel using Promise.all const submitPromises = scannedLots.map(async (lot) => { const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; @@ -570,13 +640,11 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { return { success: result.code === "SUCCESS", itemCode: lot.itemCode }; }); - // Wait for all submissions to complete const results = await Promise.all(submitPromises); const successCount = results.filter(r => r.success).length; console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`); - // Refresh data once after all submissions await fetchJobOrderData(); if (successCount > 0) { @@ -584,52 +652,42 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { setTimeout(() => setQrScanSuccess(false), 2000); } - } catch (error) { + } catch (error: any) { console.error("Error submitting all scanned items:", error); + + const isAuthError = error?.status === 401 || + error?.status === 403 || + error?.message?.toLowerCase().includes('unauthorized') || + error?.message?.toLowerCase().includes('token'); + + if (isAuthError) { + console.log("🔒 Authentication error in submit, unassigning pick order"); + await handleUnassign(currentPickOrderId); + } + setQrScanError(true); } finally { setIsSubmittingAll(false); } - }, [combinedLotData, fetchJobOrderData]); + }, [combinedLotData, fetchJobOrderData, currentPickOrderId, handleUnassign]); - // Calculate scanned items count const scannedItemsCount = useMemo(() => { return combinedLotData.filter(lot => lot.matchStatus === 'scanned').length; }, [combinedLotData]); - // 修改:初始化时加载数据 + // 修改:初始化时加载数据(移除 loadUnassignedOrders) useEffect(() => { if (session && currentUserId && !initializationRef.current) { console.log(" Session loaded, initializing job order..."); initializationRef.current = true; - // 加载 Job Order 数据 fetchJobOrderData(); - // 加载未分配订单 - loadUnassignedOrders(); } - }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]); + }, [session, currentUserId, fetchJobOrderData]); - // Add event listener for manual assignment - useEffect(() => { - const handlePickOrderAssigned = () => { - console.log("🔄 Pick order assigned event received, refreshing data..."); - fetchJobOrderData(); - }; - - window.addEventListener('pickOrderAssigned', handlePickOrderAssigned); - - return () => { - window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned); - }; - }, [fetchJobOrderData]); - - // Handle QR code submission for matched lot (external scanning) const handleQrCodeSubmit = useCallback(async (lotNo: string) => { console.log(` Processing Second QR Code for lot: ${lotNo}`); - // Check if this lot was already processed recently - const lotKey = `${lotNo}_${Date.now()}`; if (processedQrCodes.has(lotNo)) { console.log(`⏭️ Lot ${lotNo} already processed, skipping...`); return; @@ -652,24 +710,21 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { let successCount = 0; for (const matchingLot of matchingLots) { - // Check if this specific item was already processed const itemKey = `${matchingLot.pickOrderId}_${matchingLot.itemId}`; if (processedQrCodes.has(itemKey)) { console.log(`⏭️ Item ${matchingLot.itemId} already processed, skipping...`); continue; } - // Use the new second scan API const result = await updateSecondQrScanStatus( matchingLot.pickOrderId, matchingLot.itemId, currentUserId || 0, - matchingLot.requiredQty || 1 // 传递实际的 required quantity + matchingLot.requiredQty || 1 ); if (result.code === "SUCCESS") { successCount++; - // Mark this item as processed setProcessedQrCodes(prev => new Set(prev).add(itemKey)); console.log(` Second QR scan status updated for item ${matchingLot.itemId}`); } else { @@ -681,11 +736,9 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { setQrScanSuccess(true); setQrScanError(false); - // Set refreshing flag briefly to prevent duplicate processing setIsRefreshingData(true); - await fetchJobOrderData(); // Refresh data + await fetchJobOrderData(); - // Clear refresh flag and success message after a short delay setTimeout(() => { setQrScanSuccess(false); setIsRefreshingData(false); @@ -694,38 +747,43 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { setQrScanError(true); setQrScanSuccess(false); } - } catch (error) { + } catch (error: any) { console.error("❌ Error processing second QR code:", error); + + const isAuthError = error?.status === 401 || + error?.status === 403 || + error?.message?.toLowerCase().includes('unauthorized') || + error?.message?.toLowerCase().includes('token'); + + if (isAuthError) { + console.log("🔒 Authentication error in QR submit, unassigning pick order"); + await handleUnassign(currentPickOrderId); + } + setQrScanError(true); setQrScanSuccess(false); } - }, [combinedLotData, fetchJobOrderData, processedQrCodes]); - - + }, [combinedLotData, fetchJobOrderData, processedQrCodes, currentUserId, currentPickOrderId, handleUnassign]); + useEffect(() => { - // Add isManualScanning and isRefreshingData checks if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) { return; } const latestQr = qrValues[qrValues.length - 1]; - // Check if this QR was already processed recently if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { console.log("⏭️ QR code already processed, skipping..."); return; } - // Mark as processed setProcessedQrCodes(prev => new Set(prev).add(latestQr)); setLastProcessedQr(latestQr); - // Extract lot number from QR code let lotNo = ''; try { const qrData = JSON.parse(latestQr); if (qrData.stockInLineId && qrData.itemId) { - // For JSON QR codes, we need to fetch the lot number fetchStockInLineInfo(qrData.stockInLineId) .then((stockInLineInfo) => { console.log("Outside QR scan - Stock in line info:", stockInLineInfo); @@ -738,24 +796,20 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { .catch((error) => { console.error("Outside QR scan - Error fetching stock in line info:", error); }); - return; // Exit early for JSON QR codes + return; } } catch (error) { - // Not JSON format, treat as direct lot number lotNo = latestQr.replace(/[{}]/g, ''); } - // For direct lot number QR codes if (lotNo) { console.log(`Outside QR scan detected (direct): ${lotNo}`); handleQrCodeSubmit(lotNo); } }, [qrValues, combinedLotData, handleQrCodeSubmit, processedQrCodes, lastProcessedQr, isManualScanning, isRefreshingData]); - // ADD THIS: Cleanup effect useEffect(() => { return () => { - // Cleanup when component unmounts (e.g., when switching tabs) if (isManualScanning) { console.log("🧹 Second scan component unmounting, stopping QR scanner..."); stopScan(); @@ -763,13 +817,13 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { } }; }, [isManualScanning, stopScan, resetScan]); + const handleManualInputSubmit = useCallback(() => { if (qrScanInput.trim() !== '') { handleQrCodeSubmit(qrScanInput.trim()); } }, [qrScanInput, handleQrCodeSubmit]); - // Handle QR code submission from modal (internal scanning) const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => { if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) { console.log(` QR Code verified for lot: ${lotNo}`); @@ -777,7 +831,6 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { const requiredQty = selectedLotForQr.requiredQty; const lotId = selectedLotForQr.lotId; - // Create stock out line const stockOutLineData: CreateStockOutLine = { consoCode: selectedLotForQr.pickOrderConsoCode, pickOrderLineId: selectedLotForQr.pickOrderLineId, @@ -786,14 +839,9 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { }; try { - - - - // Close modal setQrModalOpen(false); setSelectedLotForQr(null); - // Set pick quantity const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`; setTimeout(() => { setPickQtyData(prev => ({ @@ -803,7 +851,6 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); }, 500); - // Refresh data await fetchJobOrderData(); } catch (error) { console.error("Error creating stock out line:", error); @@ -811,9 +858,6 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { } }, [selectedLotForQr, fetchJobOrderData]); - // Outside QR scanning - process QR codes from outside the page automatically - - const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { if (value === '' || value === null || value === undefined) { setPickQtyData(prev => ({ @@ -839,15 +883,8 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { })); }, []); - const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle'); - const [autoAssignMessage, setAutoAssignMessage] = useState(''); - - - - const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => { try { - // Use the new second scan submit API const result = await submitSecondScanQuantity( lot.pickOrderId, lot.itemId, @@ -855,14 +892,13 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { qty: submitQty, isMissing: false, isBad: false, - reason: undefined // Fix TypeScript error + reason: undefined } ); - if (result.code === "SUCCESS") { console.log(` Second scan quantity submitted: ${submitQty}`); - await fetchJobOrderData(); // Refresh data + await fetchJobOrderData(); } else { console.error(`❌ Failed to submit second scan quantity: ${result.message}`); } @@ -870,14 +906,10 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { console.error("Error submitting second scan quantity:", error); } }, [fetchJobOrderData]); - // Handle reject lot - // Handle pick execution form const handlePickExecutionForm = useCallback((lot: any) => { console.log("=== Pick Execution Form ==="); console.log("Lot data:", lot); - console.log("lot.pickOrderCode:", lot.pickOrderCode); // 添加 - console.log("lot.pickOrderId:", lot.pickOrderId); if (!lot) { console.warn("No lot data provided for pick execution form"); @@ -903,9 +935,9 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { selectedLotForExecutionForm.pickOrderId, selectedLotForExecutionForm.itemId, { - qty: data.actualPickQty, // verified qty - missQty: data.missQty || 0, // 添加:实际的 miss qty - badItemQty: data.badItemQty || 0, // 添加:实际的 bad item qty + qty: data.actualPickQty, + missQty: data.missQty || 0, + badItemQty: data.badItemQty || 0, isMissing: data.missQty > 0, isBad: data.badItemQty > 0, reason: data.issueRemark || '', @@ -928,33 +960,31 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { } catch (error) { console.error("Error submitting pick execution form:", error); } - }, [currentUserId, selectedLotForExecutionForm, fetchJobOrderData,]); + }, [currentUserId, selectedLotForExecutionForm, fetchJobOrderData]); - // Calculate remaining required quantity const calculateRemainingRequiredQty = useCallback((lot: any) => { const requiredQty = lot.requiredQty || 0; const stockOutLineQty = lot.stockOutLineQty || 0; return Math.max(0, requiredQty - stockOutLineQty); }, []); - // Search criteria const searchCriteria: Criterion[] = [ { label: t("Pick Order Code"), paramName: "pickOrderCode", type: "text", }, - { - label: t("Item Code"), - paramName: "itemCode", - type: "text", - }, - { - label: t("Item Name"), - paramName: "itemName", - type: "text", - }, - { + { + label: t("Item Code"), + paramName: "itemCode", + type: "text", + }, + { + label: t("Item Name"), + paramName: "itemName", + type: "text", + }, + { label: t("Lot No"), paramName: "lotNo", type: "text", @@ -971,10 +1001,10 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { const pickOrderCodeMatch = !query.pickOrderCode || lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); - const itemCodeMatch = !query.itemCode || + const itemCodeMatch = !query.itemCode || lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase()); - const itemNameMatch = !query.itemName || + const itemNameMatch = !query.itemName || lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase()); const lotNoMatch = !query.lotNo || @@ -1009,24 +1039,19 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { }); }, []); - // Pagination data with sorting by routerIndex -const paginatedData = useMemo(() => { - // Sort by routerIndex first, then by other criteria + const paginatedData = useMemo(() => { const sortedData = [...combinedLotData].sort((a, b) => { const aIndex = a.routerIndex || 0; const bIndex = b.routerIndex || 0; - // Primary sort: by routerIndex if (aIndex !== bIndex) { return aIndex - bIndex; } - // Secondary sort: by pickOrderCode if routerIndex is the same if (a.pickOrderCode !== b.pickOrderCode) { return a.pickOrderCode.localeCompare(b.pickOrderCode); } - // Tertiary sort: by lotNo if everything else is the same return (a.lotNo || '').localeCompare(b.lotNo || ''); }); @@ -1035,7 +1060,6 @@ const paginatedData = useMemo(() => { return sortedData.slice(startIndex, endIndex); }, [combinedLotData, paginationController]); - // Add these functions for manual scanning const handleStartScan = useCallback(() => { console.log(" Starting manual QR scan..."); setIsManualScanning(true); @@ -1054,6 +1078,7 @@ const paginatedData = useMemo(() => { stopScan(); resetScan(); }, [stopScan, resetScan]); + useEffect(() => { if (isManualScanning && combinedLotData.length === 0) { console.log("⏹️ No data available, auto-stopping QR scan..."); @@ -1061,17 +1086,6 @@ const paginatedData = useMemo(() => { } }, [combinedLotData.length, isManualScanning, handleStopScan]); - // Cleanup effect - useEffect(() => { - return () => { - // Cleanup when component unmounts (e.g., when switching tabs) - if (isManualScanning) { - console.log("🧹 Second scan component unmounting, stopping QR scanner..."); - stopScan(); - resetScan(); - } - }; - }, [isManualScanning, stopScan, resetScan]); const getStatusMessage = useCallback((lot: any) => { switch (lot.stockOutLineStatus?.toLowerCase()) { case 'pending': @@ -1092,320 +1106,322 @@ const paginatedData = useMemo(() => { }, [t]); return ( + ( + lot.matchStatus !== 'completed' && + lot.lotAvailability !== 'rejected' + )} + > + + + {/* 添加返回按钮 */} + {onBack && ( + + + + )} - ( - lot.matchStatus !== 'completed' && - lot.lotAvailability !== 'rejected' - )} - > - - - {/* Job Order Header */} - {jobOrderData && ( + {/* Job Order Header */} + {jobOrderData && ( - - {t("Job Order")}: {jobOrderData.pickOrder?.jobOrder?.code || '-'} + + {t("Job Order")}: {jobOrderData.pickOrder?.jobOrder?.code || '-'} - {t("Pick Order Code")}: {jobOrderData.pickOrder?.code || '-'} + {t("Pick Order Code")}: {jobOrderData.pickOrder?.code || '-'} - {t("Target Date")}: {jobOrderData.pickOrder?.targetDate || '-'} + {t("Target Date")}: {jobOrderData.pickOrder?.targetDate || '-'} - - )} - - + )} - {/* Combined Lot Table */} - - - - - - {!isManualScanning ? ( + {/* Combined Lot Table */} + + + + {!isManualScanning ? ( + + ) : ( + + )} + - ) : ( - - )} - - {/* ADD THIS: Submit All Scanned Button */} - + - - {qrScanError && !qrScanSuccess && ( - - {t("QR code does not match any item in current orders.")} - - )} - {qrScanSuccess && ( - - {t("QR code verified.")} - - )} - - - - - - {t("Index")} - {t("Route")} - {t("Item Code")} - {t("Item Name")} - {t("Lot No")} - {t("Lot Required Pick Qty")} - {t("Scan Result")} - {t("Submit Required Pick Qty")} - - - - {paginatedData.length === 0 ? ( + {qrScanError && !qrScanSuccess && ( + + {t("QR code does not match any item in current orders.")} + + )} + {qrScanSuccess && ( + + {t("QR code verified.")} + + )} + + +
+ - - - {t("No data available")} - - + {t("Index")} + {t("Route")} + {t("Item Code")} + {t("Item Name")} + {t("Lot No")} + {t("Lot Required Pick Qty")} + {t("Scan Result")} + {t("Submit Required Pick Qty")} - ) : ( - paginatedData.map((lot, index) => ( - - - - {index + 1} - - - - - {lot.routerRoute || '-'} + + + {paginatedData.length === 0 ? ( + + + + {t("No data available")} - {lot.itemCode} - {lot.itemName+'('+lot.uomDesc+')'} - - - - {lot.lotNo} - - - - - {(() => { - const requiredQty = lot.requiredQty || 0; - return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; - })()} - - - - {lot.matchStatus?.toLowerCase() === 'scanned' || - lot.matchStatus?.toLowerCase() === 'completed' ? ( - - - - ) : ( - - {t(" ")} - - )} - - - - - - - - - - - - - )) - )} - -
-
- - - `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` - } - /> -
-
+ }} + > + + + {index + 1} + + + + + {lot.routerRoute || '-'} + + + {lot.itemCode} + {lot.itemName+'('+lot.uomDesc+')'} + + + + {lot.lotNo} + + + + + {(() => { + const requiredQty = lot.requiredQty || 0; + return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; + })()} + + + + {lot.matchStatus?.toLowerCase() === 'scanned' || + lot.matchStatus?.toLowerCase() === 'completed' ? ( + + + + ) : ( + + {t(" ")} + + )} + - {/* QR Code Modal */} - { - setQrModalOpen(false); - setSelectedLotForQr(null); - stopScan(); - resetScan(); - }} - lot={selectedLotForQr} - combinedLotData={combinedLotData} - onQrCodeSubmit={handleQrCodeSubmitFromModal} - /> + + + + + + + + + + + )) + )} + + + + + + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` + } + /> + +
- {/* Pick Execution Form Modal */} - {pickExecutionFormOpen && selectedLotForExecutionForm && ( - { - setPickExecutionFormOpen(false); - setSelectedLotForExecutionForm(null); - }} - onSubmit={handlePickExecutionFormSubmit} - selectedLot={selectedLotForExecutionForm} - selectedPickOrderLine={{ - id: selectedLotForExecutionForm.pickOrderLineId, - itemId: selectedLotForExecutionForm.itemId, - itemCode: selectedLotForExecutionForm.itemCode, - itemName: selectedLotForExecutionForm.itemName, - pickOrderCode: selectedLotForExecutionForm.pickOrderCode, - // Add missing required properties from GetPickOrderLineInfo interface - availableQty: selectedLotForExecutionForm.availableQty || 0, - requiredQty: selectedLotForExecutionForm.requiredQty || 0, - uomCode: selectedLotForExecutionForm.uomCode || '', - uomDesc: selectedLotForExecutionForm.uomDesc || '', - pickedQty: selectedLotForExecutionForm.actualPickQty || 0, // Use pickedQty instead of actualPickQty - suggestedList: [] // Add required suggestedList property + setQrModalOpen(false); + setSelectedLotForQr(null); + stopScan(); + resetScan(); }} - pickOrderId={selectedLotForExecutionForm.pickOrderId} - pickOrderCreateDate={new Date()} + lot={selectedLotForQr} + combinedLotData={combinedLotData} + onQrCodeSubmit={handleQrCodeSubmitFromModal} /> - )} -
+ + {/* Pick Execution Form Modal */} + {pickExecutionFormOpen && selectedLotForExecutionForm && ( + { + setPickExecutionFormOpen(false); + setSelectedLotForExecutionForm(null); + }} + onSubmit={handlePickExecutionFormSubmit} + selectedLot={selectedLotForExecutionForm} + selectedPickOrderLine={{ + id: selectedLotForExecutionForm.pickOrderLineId, + itemId: selectedLotForExecutionForm.itemId, + itemCode: selectedLotForExecutionForm.itemCode, + itemName: selectedLotForExecutionForm.itemName, + pickOrderCode: selectedLotForExecutionForm.pickOrderCode, + availableQty: selectedLotForExecutionForm.availableQty || 0, + requiredQty: selectedLotForExecutionForm.requiredQty || 0, + uomDesc: selectedLotForExecutionForm.uomDesc || '', + uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '', + pickedQty: selectedLotForExecutionForm.actualPickQty || 0, + suggestedList: [], + noLotLines: [] + }} + pickOrderId={selectedLotForExecutionForm.pickOrderId} + pickOrderCreateDate={new Date()} + /> + )} +
); }; -export default JobPickExecution \ No newline at end of file +export default JobPickExecution; \ No newline at end of file diff --git a/src/components/PickOrderSearch/PickExecution.tsx b/src/components/PickOrderSearch/PickExecution.tsx index 31b1f7d..b3dbcc3 100644 --- a/src/components/PickOrderSearch/PickExecution.tsx +++ b/src/components/PickOrderSearch/PickExecution.tsx @@ -1007,7 +1007,7 @@ const handleIssueNoLotStockOutLine = useCallback(async (stockOutLineId: number) showInputBody={showInputBody} onIssueNoLotStockOutLine={handleIssueNoLotStockOutLine} setShowInputBody={setShowInputBody} - selectedLotForInput={selectedLotForInput} + //selectedLotForInput={selectedLotForInput} generateInputBody={generateInputBody} // Add missing props totalPickedByAllPickOrders={0} // You can calculate this from lotData if needed diff --git a/src/components/ProductionProcess/ProcessSummaryHeader.tsx b/src/components/ProductionProcess/ProcessSummaryHeader.tsx index 412f7cf..8412ac9 100644 --- a/src/components/ProductionProcess/ProcessSummaryHeader.tsx +++ b/src/components/ProductionProcess/ProcessSummaryHeader.tsx @@ -10,6 +10,7 @@ interface Props { itemName?: string; jobType?: string; outputQty?: number | string; + outputQtyUom?: string; date?: string; }; } @@ -32,7 +33,7 @@ const ProcessSummaryHeader: React.FC = ({ processData }) => { - {t("Qty")}: {processData?.outputQty} + {t("Qty")}: {processData?.outputQty} ({processData?.outputQtyUom ?? ""}) {t("Production Date")}: {processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""} diff --git a/src/components/ProductionProcess/ProductionProcessDetail.tsx b/src/components/ProductionProcess/ProductionProcessDetail.tsx index 7b38b0b..6723fc4 100644 --- a/src/components/ProductionProcess/ProductionProcessDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessDetail.tsx @@ -599,12 +599,14 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { {t("Time Information(mins)")} + {/* {t("Processing Time")}- {t("Setup Time")} - {t("Changeover Time")} + */} {t("Status")} @@ -637,7 +639,20 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { {line.prepTimeInMinutes} {line.postProdTimeInMinutes} */} - {line.durationInMinutes} - {line.prepTimeInMinutes} - {line.postProdTimeInMinutes} + + + + {t("Processing Time")}: {line.durationInMinutes}{t("mins")} + + + {t("Setup Time")}: {line.prepTimeInMinutes} {t("mins")} + + + + {t("Changeover Time")}: {line.postProdTimeInMinutes} {t("mins")} + + + {isCompleted ? ( {
@@ -311,10 +312,10 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { headerName: t("Seq"), flex: 0.2, align: "left", - headerAlign: "center", + headerAlign: "left", type: "number", renderCell: (params) => { - return {params.value}; + return {params.value}; }, }, { @@ -322,9 +323,9 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { headerName: t("Remark"), flex: 1, align: "left", - headerAlign: "center", + headerAlign: "left", renderCell: (params) => { - return {params.value || ""}; + return {params.value || ""}; }, }, ]; @@ -521,9 +522,9 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { - {!fromJosave && ( + {/* {!fromJosave && ( - )} + )} */} @@ -545,8 +546,8 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { )} {tabIndex === 3 && } - {tabIndex === 4 && } - + {/* {tabIndex === 4 && } */} + void; + onSelectMatchingStock: (jobOrderId: number|undefined, productProcessId: number|undefined) => void; printerCombo: PrinterCombo[]; } const PER_PAGE = 6; -const ProductProcessList: React.FC = ({ onSelectProcess, printerCombo }) => { +const ProductProcessList: React.FC = ({ onSelectProcess, printerCombo ,onSelectMatchingStock}) => { const { t } = useTranslation( ["common", "production","purchaseOrder"]); const { data: session } = useSession() as { data: SessionWithTokens | null }; const sessionToken = session as SessionWithTokens | null; @@ -45,6 +48,44 @@ const ProductProcessList: React.FC = ({ onSelectProcess const [page, setPage] = useState(0); const [openModal, setOpenModal] = useState(false); const [modalInfo, setModalInfo] = useState(); + const currentUserId = session?.id ? parseInt(session.id) : undefined; + + const handleAssignPickOrder = useCallback(async (pickOrderId: number, jobOrderId?: number, productProcessId?: number) => { + if (!currentUserId) { + alert(t("无法获取用户ID")); + return; + } + + try { + console.log("🔄 Assigning pick order:", pickOrderId, "to user:", currentUserId); + + // 调用分配 API 并读取响应 + const assignResult = await assignJobOrderPickOrder(pickOrderId, currentUserId); + + console.log("📦 Assign result:", assignResult); + + // 检查分配是否成功 + if (assignResult.message === "Successfully assigned") { + console.log("✅ Successfully assigned pick order"); + console.log("✅ Pick order ID:", assignResult.id); + console.log("✅ Pick order code:", assignResult.code); + + // 分配成功后,导航到 second scan 页面 + if (onSelectMatchingStock && jobOrderId) { + onSelectMatchingStock(jobOrderId, productProcessId); + } else { + alert(t("分配成功")); + } + } else { + // 分配失败 + console.error("❌ Assignment failed:", assignResult.message); + alert(t(`分配失败: ${assignResult.message || "未知错误"}`)); + } + } catch (error: any) { + console.error("❌ Error assigning pick order:", error); + alert(t(`分配时出错: ${error?.message || "未知错误"}。请稍后重试。`)); + } + }, [currentUserId, t, onSelectMatchingStock]); const handleViewStockIn = useCallback((process: AllJoborderProductProcessInfoResponse) => { if (!process.stockInLineId) { alert(t("Invalid Stock In Line Id")); @@ -224,6 +265,14 @@ const ProductProcessList: React.FC = ({ onSelectProcess + {statusLower !== "completed" && (