| @@ -70,6 +70,12 @@ import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import { fetchStockInLineInfo } from "@/app/api/po/actions"; | |||
| import GoodPickExecutionForm from "@/components/FinishedGoodSearch/GoodPickExecutionForm"; | |||
| import { WORKBENCH_TAB_FINISHED_GOOD_RECORD_MINE } from "./workbenchTabConstants"; | |||
| import { | |||
| inferUnpickableScanAvailability, | |||
| translateWorkbenchRejectMessage, | |||
| type UnpickableScanAvailability, | |||
| } from "@/utils/workbenchPickLotUtils"; | |||
| import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; | |||
| import FGPickOrderCard from "@/components/FinishedGoodSearch/FGPickOrderCard"; | |||
| import LinearProgressWithLabel from "@/components/common/LinearProgressWithLabel"; | |||
| @@ -423,20 +429,6 @@ function getWorkbenchSourceLotStatusSummary(lot: any): { | |||
| type PickOrderT = (key: string, options?: Record<string, unknown>) => string; | |||
| function translateWorkbenchRejectMessage(raw: string, t: PickOrderT): string { | |||
| const msg = raw.trim(); | |||
| if (!msg) return msg; | |||
| const expiredMatch = msg.match(/^Lot is expired \(expiry=([^)]+)\)\.?$/i); | |||
| if (expiredMatch) { | |||
| return t("Lot is expired (expiry={{expiry}})", { | |||
| expiry: expiredMatch[1], | |||
| }); | |||
| } | |||
| return t(msg); | |||
| } | |||
| function isExpiredWorkbenchReminderMessage(msg: string): boolean { | |||
| const trimmed = msg.trim(); | |||
| if (!trimmed) return false; | |||
| @@ -444,33 +436,6 @@ function isExpiredWorkbenchReminderMessage(msg: string): boolean { | |||
| return /已過期/.test(trimmed) || /掃描批號已過期/.test(trimmed); | |||
| } | |||
| type UnpickableScanAvailability = "expired" | "status_unavailable"; | |||
| function inferUnpickableScanAvailability( | |||
| failMsg: string | null | undefined, | |||
| ): UnpickableScanAvailability | null { | |||
| const m = String(failMsg ?? "").trim().toLowerCase(); | |||
| if (!m) return null; | |||
| if ( | |||
| m.includes("expired") || | |||
| m.includes("过期") || | |||
| m.includes("已過期") || | |||
| /^lot is expired/.test(m) | |||
| ) { | |||
| return "expired"; | |||
| } | |||
| if ( | |||
| m.includes("unavailable") || | |||
| m.includes("not available") || | |||
| m.includes("not yet putaway") || | |||
| m.includes("不可用") || | |||
| m.includes("未上架") | |||
| ) { | |||
| return "status_unavailable"; | |||
| } | |||
| return null; | |||
| } | |||
| function buildUnpickableScanRowPatch( | |||
| scannedLot: any | null | undefined, | |||
| availability: UnpickableScanAvailability, | |||
| @@ -937,7 +902,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| ) { | |||
| workbenchFinishNavigateDoneRef.current = true; | |||
| const redirectParams = new URLSearchParams(); | |||
| redirectParams.set("tab", "2"); | |||
| redirectParams.set("tab", String(WORKBENCH_TAB_FINISHED_GOOD_RECORD_MINE)); | |||
| redirectParams.set("ticketNo", ticketForRedirect); | |||
| if (targetDateForRedirect) { | |||
| redirectParams.set("targetDate", targetDateForRedirect); | |||
| @@ -1434,7 +1399,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| const event = new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted, | |||
| tabIndex: 2 // DO workbench「Finished Good Record (mine)」分頁索引 | |||
| tabIndex: WORKBENCH_TAB_FINISHED_GOOD_RECORD_MINE, | |||
| } | |||
| }); | |||
| window.dispatchEvent(event); | |||
| @@ -1669,16 +1634,11 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| } | |||
| }; | |||
| /** Stop QR effect re-entry after unpickable modal (expired/unavailable API fail). */ | |||
| const markUnpickableScanSessionHandled = ( | |||
| itemId: number, | |||
| stockOutLineId: number | null | undefined, | |||
| ) => { | |||
| if (stockOutLineId != null) { | |||
| setProcessedQrCombinations((prev) => | |||
| markProcessedStockOutLine(prev, itemId, stockOutLineId), | |||
| ); | |||
| } | |||
| /** | |||
| * Stop QR effect re-entry after unpickable modal (expired/unavailable API fail). | |||
| * Do NOT mark stock-out line as processed — pick was not completed; user may scan another lot. | |||
| */ | |||
| const markUnpickableScanSessionHandled = () => { | |||
| recordHandledQrScanCount(qrScanCountAtInvoke); | |||
| }; | |||
| @@ -1778,10 +1738,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。` | |||
| ); | |||
| }); | |||
| markUnpickableScanSessionHandled( | |||
| scannedItemId, | |||
| scannedLot.stockOutLineId, | |||
| ); | |||
| markUnpickableScanSessionHandled(); | |||
| return; | |||
| } | |||
| @@ -1796,10 +1753,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| scannedLot, | |||
| t("This lot is not available, please scan another lot."), | |||
| ); | |||
| markUnpickableScanSessionHandled( | |||
| scannedItemId, | |||
| scannedLot.stockOutLineId, | |||
| ); | |||
| markUnpickableScanSessionHandled(); | |||
| return; | |||
| } | |||
| @@ -1814,10 +1768,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| scannedLot, | |||
| `Lot is expired (expiry=${scannedLot.expiryDate || "-"})`, | |||
| ); | |||
| markUnpickableScanSessionHandled( | |||
| scannedItemId, | |||
| scannedLot.stockOutLineId, | |||
| ); | |||
| markUnpickableScanSessionHandled(); | |||
| return; | |||
| } | |||
| } | |||
| @@ -1929,10 +1880,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| null, | |||
| failMsg, | |||
| ); | |||
| markUnpickableScanSessionHandled( | |||
| scannedItemId, | |||
| expectedLot.stockOutLineId, | |||
| ); | |||
| markUnpickableScanSessionHandled(); | |||
| return; | |||
| } | |||
| if (workbenchMode && expectedLot.stockOutLineId != null) { | |||
| @@ -2109,10 +2057,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| }, | |||
| failMsg, | |||
| ); | |||
| markUnpickableScanSessionHandled( | |||
| scannedItemId, | |||
| expectedLot.stockOutLineId, | |||
| ); | |||
| markUnpickableScanSessionHandled(); | |||
| return; | |||
| } | |||
| if (workbenchMode && expectedLot.stockOutLineId != null) { | |||
| @@ -2300,10 +2245,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| exactMatch | |||
| ) { | |||
| openUnpickableScanLotLabelModal(exactMatch, exactMatch, failMsg); | |||
| markUnpickableScanSessionHandled( | |||
| scannedItemId, | |||
| exactMatch.stockOutLineId, | |||
| ); | |||
| markUnpickableScanSessionHandled(); | |||
| return; | |||
| } | |||
| if (workbenchMode && exactMatch.stockOutLineId != null) { | |||
| @@ -2428,10 +2370,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| }, | |||
| failMsg, | |||
| ); | |||
| markUnpickableScanSessionHandled( | |||
| scannedItemId, | |||
| expectedLot.stockOutLineId, | |||
| ); | |||
| markUnpickableScanSessionHandled(); | |||
| return; | |||
| } | |||
| if (workbenchMode && expectedLot.stockOutLineId != null) { | |||