| @@ -70,6 +70,12 @@ import { useSession } from "next-auth/react"; | |||||
| import { SessionWithTokens } from "@/config/authConfig"; | import { SessionWithTokens } from "@/config/authConfig"; | ||||
| import { fetchStockInLineInfo } from "@/app/api/po/actions"; | import { fetchStockInLineInfo } from "@/app/api/po/actions"; | ||||
| import GoodPickExecutionForm from "@/components/FinishedGoodSearch/GoodPickExecutionForm"; | 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 WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; | ||||
| import FGPickOrderCard from "@/components/FinishedGoodSearch/FGPickOrderCard"; | import FGPickOrderCard from "@/components/FinishedGoodSearch/FGPickOrderCard"; | ||||
| import LinearProgressWithLabel from "@/components/common/LinearProgressWithLabel"; | import LinearProgressWithLabel from "@/components/common/LinearProgressWithLabel"; | ||||
| @@ -423,20 +429,6 @@ function getWorkbenchSourceLotStatusSummary(lot: any): { | |||||
| type PickOrderT = (key: string, options?: Record<string, unknown>) => string; | 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 { | function isExpiredWorkbenchReminderMessage(msg: string): boolean { | ||||
| const trimmed = msg.trim(); | const trimmed = msg.trim(); | ||||
| if (!trimmed) return false; | if (!trimmed) return false; | ||||
| @@ -444,33 +436,6 @@ function isExpiredWorkbenchReminderMessage(msg: string): boolean { | |||||
| return /已過期/.test(trimmed) || /掃描批號已過期/.test(trimmed); | 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( | function buildUnpickableScanRowPatch( | ||||
| scannedLot: any | null | undefined, | scannedLot: any | null | undefined, | ||||
| availability: UnpickableScanAvailability, | availability: UnpickableScanAvailability, | ||||
| @@ -937,7 +902,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| ) { | ) { | ||||
| workbenchFinishNavigateDoneRef.current = true; | workbenchFinishNavigateDoneRef.current = true; | ||||
| const redirectParams = new URLSearchParams(); | const redirectParams = new URLSearchParams(); | ||||
| redirectParams.set("tab", "2"); | |||||
| redirectParams.set("tab", String(WORKBENCH_TAB_FINISHED_GOOD_RECORD_MINE)); | |||||
| redirectParams.set("ticketNo", ticketForRedirect); | redirectParams.set("ticketNo", ticketForRedirect); | ||||
| if (targetDateForRedirect) { | if (targetDateForRedirect) { | ||||
| redirectParams.set("targetDate", targetDateForRedirect); | redirectParams.set("targetDate", targetDateForRedirect); | ||||
| @@ -1434,7 +1399,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| const event = new CustomEvent('pickOrderCompletionStatus', { | const event = new CustomEvent('pickOrderCompletionStatus', { | ||||
| detail: { | detail: { | ||||
| allLotsCompleted, | allLotsCompleted, | ||||
| tabIndex: 2 // DO workbench「Finished Good Record (mine)」分頁索引 | |||||
| tabIndex: WORKBENCH_TAB_FINISHED_GOOD_RECORD_MINE, | |||||
| } | } | ||||
| }); | }); | ||||
| window.dispatchEvent(event); | 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); | recordHandledQrScanCount(qrScanCountAtInvoke); | ||||
| }; | }; | ||||
| @@ -1778,10 +1738,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。` | `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。` | ||||
| ); | ); | ||||
| }); | }); | ||||
| markUnpickableScanSessionHandled( | |||||
| scannedItemId, | |||||
| scannedLot.stockOutLineId, | |||||
| ); | |||||
| markUnpickableScanSessionHandled(); | |||||
| return; | return; | ||||
| } | } | ||||
| @@ -1796,10 +1753,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| scannedLot, | scannedLot, | ||||
| t("This lot is not available, please scan another lot."), | t("This lot is not available, please scan another lot."), | ||||
| ); | ); | ||||
| markUnpickableScanSessionHandled( | |||||
| scannedItemId, | |||||
| scannedLot.stockOutLineId, | |||||
| ); | |||||
| markUnpickableScanSessionHandled(); | |||||
| return; | return; | ||||
| } | } | ||||
| @@ -1814,10 +1768,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| scannedLot, | scannedLot, | ||||
| `Lot is expired (expiry=${scannedLot.expiryDate || "-"})`, | `Lot is expired (expiry=${scannedLot.expiryDate || "-"})`, | ||||
| ); | ); | ||||
| markUnpickableScanSessionHandled( | |||||
| scannedItemId, | |||||
| scannedLot.stockOutLineId, | |||||
| ); | |||||
| markUnpickableScanSessionHandled(); | |||||
| return; | return; | ||||
| } | } | ||||
| } | } | ||||
| @@ -1929,10 +1880,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| null, | null, | ||||
| failMsg, | failMsg, | ||||
| ); | ); | ||||
| markUnpickableScanSessionHandled( | |||||
| scannedItemId, | |||||
| expectedLot.stockOutLineId, | |||||
| ); | |||||
| markUnpickableScanSessionHandled(); | |||||
| return; | return; | ||||
| } | } | ||||
| if (workbenchMode && expectedLot.stockOutLineId != null) { | if (workbenchMode && expectedLot.stockOutLineId != null) { | ||||
| @@ -2109,10 +2057,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| }, | }, | ||||
| failMsg, | failMsg, | ||||
| ); | ); | ||||
| markUnpickableScanSessionHandled( | |||||
| scannedItemId, | |||||
| expectedLot.stockOutLineId, | |||||
| ); | |||||
| markUnpickableScanSessionHandled(); | |||||
| return; | return; | ||||
| } | } | ||||
| if (workbenchMode && expectedLot.stockOutLineId != null) { | if (workbenchMode && expectedLot.stockOutLineId != null) { | ||||
| @@ -2300,10 +2245,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| exactMatch | exactMatch | ||||
| ) { | ) { | ||||
| openUnpickableScanLotLabelModal(exactMatch, exactMatch, failMsg); | openUnpickableScanLotLabelModal(exactMatch, exactMatch, failMsg); | ||||
| markUnpickableScanSessionHandled( | |||||
| scannedItemId, | |||||
| exactMatch.stockOutLineId, | |||||
| ); | |||||
| markUnpickableScanSessionHandled(); | |||||
| return; | return; | ||||
| } | } | ||||
| if (workbenchMode && exactMatch.stockOutLineId != null) { | if (workbenchMode && exactMatch.stockOutLineId != null) { | ||||
| @@ -2428,10 +2370,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| }, | }, | ||||
| failMsg, | failMsg, | ||||
| ); | ); | ||||
| markUnpickableScanSessionHandled( | |||||
| scannedItemId, | |||||
| expectedLot.stockOutLineId, | |||||
| ); | |||||
| markUnpickableScanSessionHandled(); | |||||
| return; | return; | ||||
| } | } | ||||
| if (workbenchMode && expectedLot.stockOutLineId != null) { | if (workbenchMode && expectedLot.stockOutLineId != null) { | ||||