|
|
@@ -69,7 +69,6 @@ import { SessionWithTokens } from "@/config/authConfig"; |
|
|
import { fetchStockInLineInfo } from "@/app/api/po/actions"; |
|
|
import { fetchStockInLineInfo } from "@/app/api/po/actions"; |
|
|
import GoodPickExecutionForm from "../Jodetail/JobPickExecutionForm"; |
|
|
import GoodPickExecutionForm from "../Jodetail/JobPickExecutionForm"; |
|
|
import FGPickOrderCard from "../Jodetail/FGPickOrderCard"; |
|
|
import FGPickOrderCard from "../Jodetail/FGPickOrderCard"; |
|
|
import LotConfirmationModal from "../Jodetail/LotConfirmationModal"; |
|
|
|
|
|
import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; |
|
|
import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; |
|
|
import LinearProgressWithLabel from "../common/LinearProgressWithLabel"; |
|
|
import LinearProgressWithLabel from "../common/LinearProgressWithLabel"; |
|
|
import ScanStatusAlert from "../common/ScanStatusAlert"; |
|
|
import ScanStatusAlert from "../common/ScanStatusAlert"; |
|
|
@@ -634,10 +633,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
stopScan, |
|
|
stopScan, |
|
|
resetScan, |
|
|
resetScan, |
|
|
} = useQrCodeScannerContext(); |
|
|
} = useQrCodeScannerContext(); |
|
|
const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false); |
|
|
|
|
|
const [lotConfirmationError, setLotConfirmationError] = useState< |
|
|
|
|
|
string | null |
|
|
|
|
|
>(null); |
|
|
|
|
|
const [expectedLotData, setExpectedLotData] = useState<any>(null); |
|
|
const [expectedLotData, setExpectedLotData] = useState<any>(null); |
|
|
const [scannedLotData, setScannedLotData] = useState<any>(null); |
|
|
const [scannedLotData, setScannedLotData] = useState<any>(null); |
|
|
const [isConfirmingLot, setIsConfirmingLot] = useState(false); |
|
|
const [isConfirmingLot, setIsConfirmingLot] = useState(false); |
|
|
@@ -773,9 +768,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
((latestQr: string, qrScanCountAtInvoke?: number) => Promise<void>) | null |
|
|
((latestQr: string, qrScanCountAtInvoke?: number) => Promise<void>) | null |
|
|
>(null); |
|
|
>(null); |
|
|
const resetScanRef = useRef<(() => void) | null>(null); |
|
|
const resetScanRef = useRef<(() => void) | null>(null); |
|
|
const lotConfirmLastQrRef = useRef<string>(""); |
|
|
|
|
|
const lotConfirmSkipNextScanRef = useRef<boolean>(false); |
|
|
|
|
|
const lotConfirmOpenedAtRef = useRef<number>(0); |
|
|
|
|
|
|
|
|
|
|
|
// Manual lot confirmation modal state (test shortcut {2fic}) |
|
|
// Manual lot confirmation modal state (test shortcut {2fic}) |
|
|
const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = |
|
|
const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = |
|
|
@@ -1323,10 +1315,12 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
console.log(" Successfully assigned pick order"); |
|
|
console.log(" Successfully assigned pick order"); |
|
|
if (!assignSkippedByStatus) { |
|
|
if (!assignSkippedByStatus) { |
|
|
try { |
|
|
try { |
|
|
|
|
|
/* |
|
|
const refreshed = |
|
|
const refreshed = |
|
|
await fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench( |
|
|
await fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench( |
|
|
pickOrderId, |
|
|
pickOrderId, |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
const uniqueItemIds = Array.from( |
|
|
const uniqueItemIds = Array.from( |
|
|
new Set( |
|
|
new Set( |
|
|
(refreshed?.pickOrderLines ?? []) |
|
|
(refreshed?.pickOrderLines ?? []) |
|
|
@@ -1339,6 +1333,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
await Promise.all( |
|
|
await Promise.all( |
|
|
uniqueItemIds.map((itemId) => |
|
|
uniqueItemIds.map((itemId) => |
|
|
updateJoPickOrderHandledBy({ |
|
|
updateJoPickOrderHandledBy({ |
|
|
@@ -1348,6 +1343,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
}), |
|
|
}), |
|
|
), |
|
|
), |
|
|
); |
|
|
); |
|
|
|
|
|
*/ |
|
|
} catch (handledBySyncError) { |
|
|
} catch (handledBySyncError) { |
|
|
console.warn( |
|
|
console.warn( |
|
|
"⚠️ Assigned but failed to sync JoPickOrder.handledBy for some lines:", |
|
|
"⚠️ Assigned but failed to sync JoPickOrder.handledBy for some lines:", |
|
|
@@ -1587,72 +1583,12 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
}, |
|
|
}, |
|
|
[combinedLotData, tPick], |
|
|
[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( |
|
|
const clearLotConfirmationState = useCallback( |
|
|
(clearProcessedRefs: boolean = false) => { |
|
|
(clearProcessedRefs: boolean = false) => { |
|
|
setLotConfirmationOpen(false); |
|
|
|
|
|
setLotConfirmationError(null); |
|
|
|
|
|
setExpectedLotData(null); |
|
|
setExpectedLotData(null); |
|
|
setScannedLotData(null); |
|
|
setScannedLotData(null); |
|
|
setSelectedLotForQr(null); |
|
|
setSelectedLotForQr(null); |
|
|
lotConfirmLastQrRef.current = ""; |
|
|
|
|
|
lotConfirmSkipNextScanRef.current = false; |
|
|
|
|
|
lotConfirmOpenedAtRef.current = 0; |
|
|
|
|
|
|
|
|
|
|
|
if (clearProcessedRefs) { |
|
|
if (clearProcessedRefs) { |
|
|
setTimeout(() => { |
|
|
setTimeout(() => { |
|
|
@@ -1683,7 +1619,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
console.log("✅ [LOT CONFIRM] Selected lot for QR:", selectedLotForQr); |
|
|
console.log("✅ [LOT CONFIRM] Selected lot for QR:", selectedLotForQr); |
|
|
|
|
|
|
|
|
setIsConfirmingLot(true); |
|
|
setIsConfirmingLot(true); |
|
|
setLotConfirmationError(null); |
|
|
|
|
|
try { |
|
|
try { |
|
|
let newLotLineId = effectiveScannedLot?.inventoryLotLineId; |
|
|
let newLotLineId = effectiveScannedLot?.inventoryLotLineId; |
|
|
if (!newLotLineId && effectiveScannedLot?.stockInLineId) { |
|
|
if (!newLotLineId && effectiveScannedLot?.stockInLineId) { |
|
|
@@ -1787,7 +1722,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
tPick( |
|
|
tPick( |
|
|
"Lot switch failed; pick line was not marked as checked.", |
|
|
"Lot switch failed; pick line was not marked as checked.", |
|
|
); |
|
|
); |
|
|
setLotConfirmationError(errMsg); |
|
|
|
|
|
setQrScanError(true); |
|
|
setQrScanError(true); |
|
|
setQrScanSuccess(false); |
|
|
setQrScanSuccess(false); |
|
|
setQrScanErrorMsg(errMsg); |
|
|
setQrScanErrorMsg(errMsg); |
|
|
@@ -1831,7 +1765,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
`换批失败:stockInLineId ${ |
|
|
`换批失败:stockInLineId ${ |
|
|
effectiveScannedLot?.stockInLineId ?? "" |
|
|
effectiveScannedLot?.stockInLineId ?? "" |
|
|
} 不存在或无法匹配`; |
|
|
} 不存在或无法匹配`; |
|
|
setLotConfirmationError(errMsg); |
|
|
|
|
|
setQrScanError(true); |
|
|
setQrScanError(true); |
|
|
setQrScanSuccess(false); |
|
|
setQrScanSuccess(false); |
|
|
setQrScanErrorMsg(errMsg); |
|
|
setQrScanErrorMsg(errMsg); |
|
|
@@ -1849,7 +1782,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
const silId = effectiveScannedLot?.stockInLineId; |
|
|
const silId = effectiveScannedLot?.stockInLineId; |
|
|
if (!lotNoToScan) { |
|
|
if (!lotNoToScan) { |
|
|
const errMsg = tPick("Cannot resolve lot number for confirmation."); |
|
|
const errMsg = tPick("Cannot resolve lot number for confirmation."); |
|
|
setLotConfirmationError(errMsg); |
|
|
|
|
|
setQrScanError(true); |
|
|
setQrScanError(true); |
|
|
setQrScanSuccess(false); |
|
|
setQrScanSuccess(false); |
|
|
setQrScanErrorMsg(errMsg); |
|
|
setQrScanErrorMsg(errMsg); |
|
|
@@ -1869,7 +1801,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
const errMsg = |
|
|
const errMsg = |
|
|
(res as { message?: string })?.message || |
|
|
(res as { message?: string })?.message || |
|
|
tPick("Workbench scan-pick failed."); |
|
|
tPick("Workbench scan-pick failed."); |
|
|
setLotConfirmationError(errMsg); |
|
|
|
|
|
setQrScanError(true); |
|
|
setQrScanError(true); |
|
|
setQrScanSuccess(false); |
|
|
setQrScanSuccess(false); |
|
|
setQrScanErrorMsg(errMsg); |
|
|
setQrScanErrorMsg(errMsg); |
|
|
@@ -1942,7 +1873,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
} catch (error) { |
|
|
} catch (error) { |
|
|
console.error("Error confirming lot substitution:", error); |
|
|
console.error("Error confirming lot substitution:", error); |
|
|
const errMsg = tPick("Lot confirmation failed. Please try again."); |
|
|
const errMsg = tPick("Lot confirmation failed. Please try again."); |
|
|
setLotConfirmationError(errMsg); |
|
|
|
|
|
setQrScanError(true); |
|
|
setQrScanError(true); |
|
|
setQrScanSuccess(false); |
|
|
setQrScanSuccess(false); |
|
|
setQrScanErrorMsg(errMsg); |
|
|
setQrScanErrorMsg(errMsg); |
|
|
@@ -1969,84 +1899,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
], |
|
|
], |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
const handleLotConfirmationByRescan = useCallback( |
|
|
|
|
|
async (rawQr: string): Promise<boolean> => { |
|
|
|
|
|
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( |
|
|
const processOutsideQrCode = useCallback( |
|
|
async (latestQr: string, _qrScanCountAtInvoke?: number) => { |
|
|
async (latestQr: string, _qrScanCountAtInvoke?: number) => { |
|
|
const totalStartTime = performance.now(); |
|
|
const totalStartTime = performance.now(); |
|
|
@@ -2993,32 +2845,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
return; |
|
|
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 |
|
|
// Skip processing if manual modal open for same QR |
|
|
if (manualLotConfirmationOpen) { |
|
|
if (manualLotConfirmationOpen) { |
|
|
if (latestQr === lastProcessedQrRef.current) return; |
|
|
if (latestQr === lastProcessedQrRef.current) return; |
|
|
@@ -3065,9 +2891,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
isManualScanning, |
|
|
isManualScanning, |
|
|
isRefreshingData, |
|
|
isRefreshingData, |
|
|
combinedLotData.length, |
|
|
combinedLotData.length, |
|
|
lotConfirmationOpen, |
|
|
|
|
|
manualLotConfirmationOpen, |
|
|
manualLotConfirmationOpen, |
|
|
handleLotConfirmationByRescan, |
|
|
|
|
|
isConfirmingLot, |
|
|
isConfirmingLot, |
|
|
]); |
|
|
]); |
|
|
|
|
|
|
|
|
@@ -3850,13 +3674,40 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
// Pagination data: align DO workbench grouping display |
|
|
// Pagination data: align DO workbench grouping display |
|
|
const paginatedData = useMemo(() => { |
|
|
const paginatedData = useMemo(() => { |
|
|
const sourceData = selectedFloor ? filteredByFloor : combinedLotData; |
|
|
const sourceData = selectedFloor ? filteredByFloor : combinedLotData; |
|
|
|
|
|
|
|
|
|
|
|
/** 同 pick_order_line 內「無庫位 / no-lot」列 extractFloor 為空,若仍用 0 會被排到全表最後,編號欄也變成新群組。繼承該行最大樓層權重並以 polId 相鄰排序。 */ |
|
|
|
|
|
const lineMaxFloorOrder = new Map<number, number>(); |
|
|
|
|
|
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 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 aItem = String(a.itemCode || ""); |
|
|
const bItem = String(b.itemCode || ""); |
|
|
const bItem = String(b.itemCode || ""); |
|
|
if (aItem !== bItem) return aItem.localeCompare(bItem); |
|
|
if (aItem !== bItem) return aItem.localeCompare(bItem); |
|
|
@@ -3865,6 +3716,10 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
const bName = String(b.itemName || ""); |
|
|
const bName = String(b.itemName || ""); |
|
|
if (aName !== bName) return aName.localeCompare(bName); |
|
|
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 aIndex = Number(a.routerIndex ?? 0); |
|
|
const bIndex = Number(b.routerIndex ?? 0); |
|
|
const bIndex = Number(b.routerIndex ?? 0); |
|
|
if (aIndex !== bIndex) return aIndex - bIndex; |
|
|
if (aIndex !== bIndex) return aIndex - bIndex; |
|
|
@@ -4715,20 +4570,18 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
</Stack> |
|
|
</Stack> |
|
|
|
|
|
|
|
|
{/* QR Code Modal */} |
|
|
{/* QR Code Modal */} |
|
|
{!lotConfirmationOpen && ( |
|
|
|
|
|
<QrCodeModal |
|
|
|
|
|
open={qrModalOpen} |
|
|
|
|
|
onClose={() => { |
|
|
|
|
|
setQrModalOpen(false); |
|
|
|
|
|
setSelectedLotForQr(null); |
|
|
|
|
|
// Keep scanner active like GoodPickExecutiondetail. |
|
|
|
|
|
resetScan(); |
|
|
|
|
|
}} |
|
|
|
|
|
lot={selectedLotForQr} |
|
|
|
|
|
combinedLotData={combinedLotData} |
|
|
|
|
|
onQrCodeSubmit={handleQrCodeSubmitFromModal} |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
|
|
|
<QrCodeModal |
|
|
|
|
|
open={qrModalOpen} |
|
|
|
|
|
onClose={() => { |
|
|
|
|
|
setQrModalOpen(false); |
|
|
|
|
|
setSelectedLotForQr(null); |
|
|
|
|
|
// Keep scanner active like GoodPickExecutiondetail. |
|
|
|
|
|
resetScan(); |
|
|
|
|
|
}} |
|
|
|
|
|
lot={selectedLotForQr} |
|
|
|
|
|
combinedLotData={combinedLotData} |
|
|
|
|
|
onQrCodeSubmit={handleQrCodeSubmitFromModal} |
|
|
|
|
|
/> |
|
|
<WorkbenchLotLabelPrintModal |
|
|
<WorkbenchLotLabelPrintModal |
|
|
open={workbenchLotLabelModalOpen} |
|
|
open={workbenchLotLabelModalOpen} |
|
|
onClose={() => { |
|
|
onClose={() => { |
|
|
@@ -4765,54 +4618,39 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { |
|
|
onSubmitQtyChange={handleWorkbenchLotLabelSubmitQtyChange} |
|
|
onSubmitQtyChange={handleWorkbenchLotLabelSubmitQtyChange} |
|
|
reminderText={workbenchLotLabelReminderText ?? undefined} |
|
|
reminderText={workbenchLotLabelReminderText ?? undefined} |
|
|
/> |
|
|
/> |
|
|
{/* Add Lot Confirmation Modal */} |
|
|
|
|
|
{lotConfirmationOpen && expectedLotData && scannedLotData && ( |
|
|
|
|
|
<LotConfirmationModal |
|
|
|
|
|
open={lotConfirmationOpen} |
|
|
|
|
|
onClose={() => { |
|
|
|
|
|
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 */} |
|
|
<ManualLotConfirmationModal |
|
|
<ManualLotConfirmationModal |
|
|
open={manualLotConfirmationOpen} |
|
|
open={manualLotConfirmationOpen} |
|
|
onClose={() => setManualLotConfirmationOpen(false)} |
|
|
onClose={() => setManualLotConfirmationOpen(false)} |
|
|
// Reuse existing handler: expectedLotInput=current lot, scannedLotInput=new lot |
|
|
|
|
|
onConfirm={(currentLotNo, newLotNo) => { |
|
|
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({ |
|
|
setExpectedLotData({ |
|
|
|
|
|
...row, |
|
|
lotNo: currentLotNo, |
|
|
lotNo: currentLotNo, |
|
|
itemCode: "", |
|
|
|
|
|
itemName: "", |
|
|
|
|
|
}); |
|
|
}); |
|
|
setScannedLotData({ |
|
|
setScannedLotData({ |
|
|
lotNo: newLotNo, |
|
|
lotNo: newLotNo, |
|
|
itemCode: "", |
|
|
|
|
|
itemName: "", |
|
|
|
|
|
|
|
|
itemCode: String(row.itemCode ?? ""), |
|
|
|
|
|
itemName: String(row.itemName ?? ""), |
|
|
inventoryLotLineId: null, |
|
|
inventoryLotLineId: null, |
|
|
stockInLineId: null, |
|
|
stockInLineId: null, |
|
|
}); |
|
|
}); |
|
|
setManualLotConfirmationOpen(false); |
|
|
|
|
|
setLotConfirmationOpen(true); |
|
|
|
|
|
|
|
|
window.setTimeout(() => { |
|
|
|
|
|
void handleLotConfirmation(); |
|
|
|
|
|
}, 0); |
|
|
}} |
|
|
}} |
|
|
expectedLot={expectedLotData} |
|
|
expectedLot={expectedLotData} |
|
|
scannedLot={scannedLotData} |
|
|
scannedLot={scannedLotData} |
|
|
|