diff --git a/src/components/JoWorkbench/newJobPickExecution.tsx b/src/components/JoWorkbench/newJobPickExecution.tsx index 92fa403..f4f8bd4 100644 --- a/src/components/JoWorkbench/newJobPickExecution.tsx +++ b/src/components/JoWorkbench/newJobPickExecution.tsx @@ -69,7 +69,6 @@ import { SessionWithTokens } from "@/config/authConfig"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; import GoodPickExecutionForm from "../Jodetail/JobPickExecutionForm"; import FGPickOrderCard from "../Jodetail/FGPickOrderCard"; -import LotConfirmationModal from "../Jodetail/LotConfirmationModal"; import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; import LinearProgressWithLabel from "../common/LinearProgressWithLabel"; import ScanStatusAlert from "../common/ScanStatusAlert"; @@ -634,10 +633,6 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { stopScan, resetScan, } = useQrCodeScannerContext(); - const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false); - const [lotConfirmationError, setLotConfirmationError] = useState< - string | null - >(null); const [expectedLotData, setExpectedLotData] = useState(null); const [scannedLotData, setScannedLotData] = useState(null); const [isConfirmingLot, setIsConfirmingLot] = useState(false); @@ -773,9 +768,6 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { ((latestQr: string, qrScanCountAtInvoke?: number) => Promise) | null >(null); const resetScanRef = useRef<(() => void) | null>(null); - const lotConfirmLastQrRef = useRef(""); - const lotConfirmSkipNextScanRef = useRef(false); - const lotConfirmOpenedAtRef = useRef(0); // Manual lot confirmation modal state (test shortcut {2fic}) const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = @@ -1323,10 +1315,12 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { console.log(" Successfully assigned pick order"); if (!assignSkippedByStatus) { try { + /* const refreshed = await fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench( pickOrderId, ); + const uniqueItemIds = Array.from( new Set( (refreshed?.pickOrderLines ?? []) @@ -1339,6 +1333,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { ), ), ); + await Promise.all( uniqueItemIds.map((itemId) => updateJoPickOrderHandledBy({ @@ -1348,6 +1343,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { }), ), ); + */ } catch (handledBySyncError) { console.warn( "⚠️ Assigned but failed to sync JoPickOrder.handledBy for some lines:", @@ -1587,72 +1583,12 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { }, [combinedLotData, tPick], ); - const handleLotMismatch = useCallback( - (expectedLot: any, scannedLot: any) => { - console.log("⚠️ [LOT MISMATCH] Lot mismatch detected:", { - expectedLot, - scannedLot, - }); - console.log( - "⚠️ [LOT MISMATCH] Opening confirmation modal - NO lot will be marked as scanned until user confirms", - ); - - // ✅ schedule modal open in next tick (avoid flushSync warnings on some builds) - // ✅ IMPORTANT: This function ONLY opens the modal. It does NOT process any lot. - setTimeout(() => { - setExpectedLotData(expectedLot); - setScannedLotData({ - ...scannedLot, - lotNo: scannedLot.lotNo || null, - }); - lotConfirmSkipNextScanRef.current = true; - lotConfirmOpenedAtRef.current = Date.now(); - setLotConfirmationOpen(true); - console.log( - "⚠️ [LOT MISMATCH] Modal opened - waiting for user confirmation", - ); - }, 0); - - // ✅ Fetch lotNo in background for display purposes (cached) - // ✅ This is ONLY for display - it does NOT process any lot - if (!scannedLot.lotNo && scannedLot.stockInLineId) { - console.log( - `⚠️ [LOT MISMATCH] Fetching lotNo for display (stockInLineId: ${scannedLot.stockInLineId})`, - ); - fetchStockInLineInfoCached(scannedLot.stockInLineId) - .then((info) => { - console.log( - `⚠️ [LOT MISMATCH] Fetched lotNo for display: ${info.lotNo}`, - ); - startTransition(() => { - setScannedLotData((prev: any) => ({ - ...prev, - lotNo: info.lotNo || null, - })); - }); - }) - .catch((error) => { - console.error( - `❌ [LOT MISMATCH] Error fetching lotNo for display (stockInLineId may not exist):`, - error, - ); - // ignore display fetch errors - this does NOT affect processing - }); - } - }, - [fetchStockInLineInfoCached], - ); const clearLotConfirmationState = useCallback( (clearProcessedRefs: boolean = false) => { - setLotConfirmationOpen(false); - setLotConfirmationError(null); setExpectedLotData(null); setScannedLotData(null); setSelectedLotForQr(null); - lotConfirmLastQrRef.current = ""; - lotConfirmSkipNextScanRef.current = false; - lotConfirmOpenedAtRef.current = 0; if (clearProcessedRefs) { setTimeout(() => { @@ -1683,7 +1619,6 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { console.log("✅ [LOT CONFIRM] Selected lot for QR:", selectedLotForQr); setIsConfirmingLot(true); - setLotConfirmationError(null); try { let newLotLineId = effectiveScannedLot?.inventoryLotLineId; if (!newLotLineId && effectiveScannedLot?.stockInLineId) { @@ -1787,7 +1722,6 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { tPick( "Lot switch failed; pick line was not marked as checked.", ); - setLotConfirmationError(errMsg); setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg(errMsg); @@ -1831,7 +1765,6 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { `换批失败:stockInLineId ${ effectiveScannedLot?.stockInLineId ?? "" } 不存在或无法匹配`; - setLotConfirmationError(errMsg); setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg(errMsg); @@ -1849,7 +1782,6 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const silId = effectiveScannedLot?.stockInLineId; if (!lotNoToScan) { const errMsg = tPick("Cannot resolve lot number for confirmation."); - setLotConfirmationError(errMsg); setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg(errMsg); @@ -1869,7 +1801,6 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const errMsg = (res as { message?: string })?.message || tPick("Workbench scan-pick failed."); - setLotConfirmationError(errMsg); setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg(errMsg); @@ -1942,7 +1873,6 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { } catch (error) { console.error("Error confirming lot substitution:", error); const errMsg = tPick("Lot confirmation failed. Please try again."); - setLotConfirmationError(errMsg); setQrScanError(true); setQrScanSuccess(false); setQrScanErrorMsg(errMsg); @@ -1969,84 +1899,6 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { ], ); - const handleLotConfirmationByRescan = useCallback( - async (rawQr: string): Promise => { - if ( - !lotConfirmationOpen || - !selectedLotForQr || - !expectedLotData || - !scannedLotData - ) { - return false; - } - let payload: any = null; - try { - payload = JSON.parse(rawQr); - } catch { - payload = null; - } - const expectedStockInLineId = Number(selectedLotForQr.stockInLineId); - const mismatchedStockInLineId = Number(scannedLotData?.stockInLineId); - if (payload?.stockInLineId && payload?.itemId) { - const rescannedStockInLineId = Number(payload.stockInLineId); - if ( - Number.isFinite(expectedStockInLineId) && - rescannedStockInLineId === expectedStockInLineId - ) { - clearLotConfirmationState(false); - if (processOutsideQrCodeRef.current) { - await processOutsideQrCodeRef.current(JSON.stringify(payload)); - } - return true; - } - if ( - Number.isFinite(mismatchedStockInLineId) && - rescannedStockInLineId === mismatchedStockInLineId - ) { - await handleLotConfirmation(); - return true; - } - await handleLotConfirmation({ - lotNo: null, - itemCode: expectedLotData?.itemCode, - itemName: expectedLotData?.itemName, - inventoryLotLineId: null, - stockInLineId: rescannedStockInLineId, - }); - return true; - } else { - const scannedText = rawQr?.trim(); - const expectedLotNo = expectedLotData?.lotNo?.trim(); - const mismatchedLotNo = scannedLotData?.lotNo?.trim(); - if (expectedLotNo && scannedText === expectedLotNo) { - clearLotConfirmationState(false); - if (processOutsideQrCodeRef.current) { - await processOutsideQrCodeRef.current( - JSON.stringify({ - itemId: selectedLotForQr.itemId, - stockInLineId: selectedLotForQr.stockInLineId, - }), - ); - } - return true; - } - if (mismatchedLotNo && scannedText === mismatchedLotNo) { - await handleLotConfirmation(); - return true; - } - } - return false; - }, - [ - lotConfirmationOpen, - selectedLotForQr, - expectedLotData, - scannedLotData, - handleLotConfirmation, - clearLotConfirmationState, - ], - ); - const processOutsideQrCode = useCallback( async (latestQr: string, _qrScanCountAtInvoke?: number) => { const totalStartTime = performance.now(); @@ -2993,32 +2845,6 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { return; } - if (lotConfirmationOpen) { - if (isConfirmingLot) return; - if (lotConfirmSkipNextScanRef.current) { - lotConfirmSkipNextScanRef.current = false; - lotConfirmLastQrRef.current = latestQr || ""; - return; - } - if (!latestQr) return; - const sameQr = latestQr === lotConfirmLastQrRef.current; - const justOpened = - lotConfirmOpenedAtRef.current > 0 && - Date.now() - lotConfirmOpenedAtRef.current < 800; - if (sameQr && justOpened) return; - lotConfirmLastQrRef.current = latestQr; - void (async () => { - try { - const handled = await handleLotConfirmationByRescan(latestQr); - if (handled && resetScanRef.current) { - resetScanRef.current(); - } - } catch (e) { - console.error("Lot confirmation rescan failed:", e); - } - })(); - return; - } // Skip processing if manual modal open for same QR if (manualLotConfirmationOpen) { if (latestQr === lastProcessedQrRef.current) return; @@ -3065,9 +2891,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { isManualScanning, isRefreshingData, combinedLotData.length, - lotConfirmationOpen, manualLotConfirmationOpen, - handleLotConfirmationByRescan, isConfirmingLot, ]); @@ -3850,13 +3674,40 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { // Pagination data: align DO workbench grouping display const paginatedData = useMemo(() => { const sourceData = selectedFloor ? filteredByFloor : combinedLotData; + + /** 同 pick_order_line 內「無庫位 / no-lot」列 extractFloor 為空,若仍用 0 會被排到全表最後,編號欄也變成新群組。繼承該行最大樓層權重並以 polId 相鄰排序。 */ + const lineMaxFloorOrder = new Map(); + for (const lot of sourceData) { + const polId = Number(lot.pickOrderLineId); + if (!Number.isFinite(polId) || polId <= 0) continue; + const o = floorSortOrder(extractFloor(lot)); + const prev = lineMaxFloorOrder.get(polId) ?? 0; + if (o > prev) lineMaxFloorOrder.set(polId, o); + } + const effectiveFloorOrder = (lot: any): number => { + const own = floorSortOrder(extractFloor(lot)); + if (own > 0) return own; + const polId = Number(lot.pickOrderLineId); + if (Number.isFinite(polId) && polId > 0) { + const inherited = lineMaxFloorOrder.get(polId); + if (inherited != null && inherited > 0) return inherited; + } + return 0; + }; + const isNoLotTailRow = (lot: any) => + lot.noLot === true || lot.lotId == null || lot.lotId === undefined; + const sortedData = [...sourceData].sort((a, b) => { - const floorA = extractFloor(a); - const floorB = extractFloor(b); - const orderA = floorSortOrder(floorA); - const orderB = floorSortOrder(floorB); - if (orderA !== orderB) return orderB - orderA; // 4F, 3F, 2F - // Same floor: group same item together first (DO-like visual grouping) + const efA = effectiveFloorOrder(a); + const efB = effectiveFloorOrder(b); + if (efA !== efB) return efB - efA; // 4F, 3F, 2F(含繼承樓層的缺口列) + + const polA = Number(a.pickOrderLineId); + const polB = Number(b.pickOrderLineId); + const hasPolA = Number.isFinite(polA) && polA > 0; + const hasPolB = Number.isFinite(polB) && polB > 0; + if (hasPolA && hasPolB && polA !== polB) return polA - polB; + const aItem = String(a.itemCode || ""); const bItem = String(b.itemCode || ""); if (aItem !== bItem) return aItem.localeCompare(bItem); @@ -3865,6 +3716,10 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const bName = String(b.itemName || ""); if (aName !== bName) return aName.localeCompare(bName); + const tailA = isNoLotTailRow(a) ? 1 : 0; + const tailB = isNoLotTailRow(b) ? 1 : 0; + if (tailA !== tailB) return tailA - tailB; + const aIndex = Number(a.routerIndex ?? 0); const bIndex = Number(b.routerIndex ?? 0); if (aIndex !== bIndex) return aIndex - bIndex; @@ -4715,20 +4570,18 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { {/* QR Code Modal */} - {!lotConfirmationOpen && ( - { - setQrModalOpen(false); - setSelectedLotForQr(null); - // Keep scanner active like GoodPickExecutiondetail. - resetScan(); - }} - lot={selectedLotForQr} - combinedLotData={combinedLotData} - onQrCodeSubmit={handleQrCodeSubmitFromModal} - /> - )} + { + setQrModalOpen(false); + setSelectedLotForQr(null); + // Keep scanner active like GoodPickExecutiondetail. + resetScan(); + }} + lot={selectedLotForQr} + combinedLotData={combinedLotData} + onQrCodeSubmit={handleQrCodeSubmitFromModal} + /> { @@ -4765,54 +4618,39 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { onSubmitQtyChange={handleWorkbenchLotLabelSubmitQtyChange} reminderText={workbenchLotLabelReminderText ?? undefined} /> - {/* Add Lot Confirmation Modal */} - {lotConfirmationOpen && expectedLotData && scannedLotData && ( - { - console.log( - `⏱️ [LOT CONFIRM MODAL] Closing modal, reset scanner and release raw-QR dedupe`, - ); - if (resetScanRef.current) { - resetScanRef.current(); - } - clearLotConfirmationState(false); - setTimeout(() => { - lastProcessedQrRef.current = ""; - processedQrCodesRef.current.clear(); - }, 250); - }} - onConfirm={handleLotConfirmation} - expectedLot={expectedLotData} - scannedLot={scannedLotData} - isLoading={isConfirmingLot} - errorMessage={lotConfirmationError} - /> - )} - - {/* Manual Lot Confirmation Modal (test shortcut {2fic}) */} + {/* Manual Lot Confirmation Modal (test shortcut {2fic}); JO Workbench 不使用 LotConfirmationModal,直接走 handleLotConfirmation */} setManualLotConfirmationOpen(false)} - // Reuse existing handler: expectedLotInput=current lot, scannedLotInput=new lot onConfirm={(currentLotNo, newLotNo) => { - // Use existing manual flow from handleManualLotConfirmation in other screens: - // Here we route through updateStockOutLineStatusByQRCodeAndLotNo via handleManualLotConfirmation-like inline logic. - // For now: open LotConfirmationModal path by setting expected/scanned and letting user confirm substitution. + setManualLotConfirmationOpen(false); + const row = selectedLotForQr; + if ( + !row?.stockOutLineId || + !row?.pickOrderLineId || + row.itemId == null + ) { + alert( + t( + "Open 挑號 QR 碼 on a pick line first, then scan {2fic} to use manual lot substitution.", + ), + ); + return; + } setExpectedLotData({ + ...row, lotNo: currentLotNo, - itemCode: "", - itemName: "", }); setScannedLotData({ lotNo: newLotNo, - itemCode: "", - itemName: "", + itemCode: String(row.itemCode ?? ""), + itemName: String(row.itemName ?? ""), inventoryLotLineId: null, stockInLineId: null, }); - setManualLotConfirmationOpen(false); - setLotConfirmationOpen(true); + window.setTimeout(() => { + void handleLotConfirmation(); + }, 0); }} expectedLot={expectedLotData} scannedLot={scannedLotData} diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index ed7c798..e59eae9 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -385,6 +385,7 @@ "Printed Successfully.": "成功列印", "Submit All Scanned": "提交所有已掃描項目", "Submitting...": "提交中...", + "is unavable. Please check around have available QR code or not.": "此批號不可用,請檢查周圍是否有可用的 QR 碼。", "COMPLETED": "已完成", "success": "成功", "Total (Verified + Bad + Missing) must equal Required quantity": "驗證數量 + 不良數量 + 缺失數量必須等於需求數量", @@ -605,5 +606,6 @@ "BOM Type": "BOM 類型", "BOM Description": "BOM 說明", "Floor": "樓層", - "Finish": "完成" + "Finish": "完成", + "Open 挑號 QR 碼 on a pick line first, then scan {2fic} to use manual lot substitution.": "請先點選一列並開啟「挑號 QR 碼」,再掃描 {2fic} 以使用手動換批。" }