| @@ -101,6 +101,66 @@ function pickExpectedLotForSubstitution(activeSuggestedLots: any[]): any | null | |||||
| return activeSuggestedLots[0]; | return activeSuggestedLots[0]; | ||||
| } | } | ||||
| /** Parse workbench QR / test shortcut into itemId + stockInLineId. */ | |||||
| function parseWorkbenchScanQrPayload( | |||||
| raw: string, | |||||
| ): { itemId: number; stockInLineId: number } | null { | |||||
| const text = String(raw || "").trim(); | |||||
| if (!text) return null; | |||||
| if ( | |||||
| (text.startsWith("{2fitest") || text.startsWith("{2fittest")) && | |||||
| text.endsWith("}") | |||||
| ) { | |||||
| let content = ""; | |||||
| if (text.startsWith("{2fittest")) { | |||||
| content = text.substring(9, text.length - 1); | |||||
| } else { | |||||
| content = text.substring(8, text.length - 1); | |||||
| } | |||||
| const parts = content.split(","); | |||||
| if (parts.length === 2) { | |||||
| const itemId = parseInt(parts[0].trim(), 10); | |||||
| const stockInLineId = parseInt(parts[1].trim(), 10); | |||||
| if ( | |||||
| Number.isFinite(itemId) && | |||||
| itemId > 0 && | |||||
| Number.isFinite(stockInLineId) && | |||||
| stockInLineId > 0 | |||||
| ) { | |||||
| return { itemId, stockInLineId }; | |||||
| } | |||||
| } | |||||
| return null; | |||||
| } | |||||
| try { | |||||
| const obj = JSON.parse(text); | |||||
| const itemId = Number(obj?.itemId); | |||||
| const stockInLineId = Number(obj?.stockInLineId); | |||||
| if ( | |||||
| Number.isFinite(itemId) && | |||||
| itemId > 0 && | |||||
| Number.isFinite(stockInLineId) && | |||||
| stockInLineId > 0 | |||||
| ) { | |||||
| return { itemId, stockInLineId }; | |||||
| } | |||||
| } catch { | |||||
| /* not JSON */ | |||||
| } | |||||
| return null; | |||||
| } | |||||
| /** True when another pending/partial SOL still suggests this stockInLineId (same lot across lines). */ | |||||
| function activeLotsStillNeedStockInLine( | |||||
| activeLotsForItem: any[] | undefined, | |||||
| stockInLineId: number, | |||||
| ): boolean { | |||||
| if (!activeLotsForItem?.length) return false; | |||||
| return activeLotsForItem.some( | |||||
| (lot) => Number(lot?.stockInLineId) === Number(stockInLineId), | |||||
| ); | |||||
| } | |||||
| const ManualLotConfirmationModal: React.FC<{ | const ManualLotConfirmationModal: React.FC<{ | ||||
| open: boolean; | open: boolean; | ||||
| onClose: () => void; | onClose: () => void; | ||||
| @@ -601,6 +661,12 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); | |||||
| // Use refs for processed QR tracking to avoid useEffect dependency issues and delays | // Use refs for processed QR tracking to avoid useEffect dependency issues and delays | ||||
| const processedQrCodesRef = useRef<Set<string>>(new Set()); | const processedQrCodesRef = useRef<Set<string>>(new Set()); | ||||
| const lastProcessedQrRef = useRef<string>(''); | const lastProcessedQrRef = useRef<string>(''); | ||||
| /** Prevent double-submit while a scan-pick API is in flight (scanner bounce). */ | |||||
| const qrProcessInFlightRef = useRef(false); | |||||
| /** Latest lot indexes for QR re-scan allow checks outside processOutsideQrCode. */ | |||||
| const lotDataIndexesRef = useRef<{ | |||||
| activeLotsByItemId: Map<number, any[]>; | |||||
| } | null>(null); | |||||
| // Store callbacks in refs to avoid useEffect dependency issues | // Store callbacks in refs to avoid useEffect dependency issues | ||||
| const processOutsideQrCodeRef = useRef< | const processOutsideQrCodeRef = useRef< | ||||
| @@ -1399,6 +1465,10 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId }; | return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId }; | ||||
| }, [combinedLotData.length, combinedLotData]); | }, [combinedLotData.length, combinedLotData]); | ||||
| lotDataIndexesRef.current = { | |||||
| activeLotsByItemId: lotDataIndexes.activeLotsByItemId, | |||||
| }; | |||||
| // Store resetScan in ref for immediate access (update on every render) | // Store resetScan in ref for immediate access (update on every render) | ||||
| resetScanRef.current = resetScan; | resetScanRef.current = resetScan; | ||||
| @@ -1447,10 +1517,19 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| const scannedItemId = qrData.itemId; | const scannedItemId = qrData.itemId; | ||||
| const scannedStockInLineId = qrData.stockInLineId; | const scannedStockInLineId = qrData.stockInLineId; | ||||
| // ✅ Check if this combination was already processed | |||||
| // ✅ Skip only when this lot was already processed AND no other active line still needs it. | |||||
| // Same item + same stockInLineId across different pick-order lines must be re-scannable. | |||||
| const duplicateCheckStartTime = performance.now(); | const duplicateCheckStartTime = performance.now(); | ||||
| const itemProcessedSet = processedQrCombinations.get(scannedItemId); | const itemProcessedSet = processedQrCombinations.get(scannedItemId); | ||||
| if (itemProcessedSet?.has(scannedStockInLineId)) { | |||||
| const activeSuggestedLotsEarly = | |||||
| indexes.activeLotsByItemId.get(scannedItemId) || []; | |||||
| if ( | |||||
| itemProcessedSet?.has(scannedStockInLineId) && | |||||
| !activeLotsStillNeedStockInLine( | |||||
| activeSuggestedLotsEarly, | |||||
| scannedStockInLineId, | |||||
| ) | |||||
| ) { | |||||
| const duplicateCheckTime = performance.now() - duplicateCheckStartTime; | const duplicateCheckTime = performance.now() - duplicateCheckStartTime; | ||||
| console.log(` [SKIP] Already processed combination: itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId} (check time: ${duplicateCheckTime.toFixed(2)}ms)`); | console.log(` [SKIP] Already processed combination: itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId} (check time: ${duplicateCheckTime.toFixed(2)}ms)`); | ||||
| return; | return; | ||||
| @@ -1460,7 +1539,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| // ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed) | // ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed) | ||||
| const lookupStartTime = performance.now(); | const lookupStartTime = performance.now(); | ||||
| const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || []; | |||||
| const activeSuggestedLots = activeSuggestedLotsEarly; | |||||
| // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected | // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected | ||||
| const allLotsForItem = indexes.byItemId.get(scannedItemId) || []; | const allLotsForItem = indexes.byItemId.get(scannedItemId) || []; | ||||
| const lookupTime = performance.now() - lookupStartTime; | const lookupTime = performance.now() - lookupStartTime; | ||||
| @@ -2007,7 +2086,10 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| // Workbench 策略:不彈窗,直接切換到掃到的批次並提交一次掃描 | // Workbench 策略:不彈窗,直接切換到掃到的批次並提交一次掃描 | ||||
| const mismatchCheckStartTime = performance.now(); | const mismatchCheckStartTime = performance.now(); | ||||
| const itemProcessedSet2 = processedQrCombinations.get(scannedItemId); | const itemProcessedSet2 = processedQrCombinations.get(scannedItemId); | ||||
| if (itemProcessedSet2?.has(scannedStockInLineId)) { | |||||
| if ( | |||||
| itemProcessedSet2?.has(scannedStockInLineId) && | |||||
| !activeLotsStillNeedStockInLine(activeSuggestedLots, scannedStockInLineId) | |||||
| ) { | |||||
| const mismatchCheckTime = performance.now() - mismatchCheckStartTime; | const mismatchCheckTime = performance.now() - mismatchCheckStartTime; | ||||
| console.log( | console.log( | ||||
| ` [SKIP] Already processed this exact combination (check time: ${mismatchCheckTime.toFixed(2)}ms)`, | ` [SKIP] Already processed this exact combination (check time: ${mismatchCheckTime.toFixed(2)}ms)`, | ||||
| @@ -2272,6 +2354,11 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| // ✅ Process immediately (bypass QR scanner delay) | // ✅ Process immediately (bypass QR scanner delay) | ||||
| if (processOutsideQrCodeRef.current) { | if (processOutsideQrCodeRef.current) { | ||||
| if (qrProcessInFlightRef.current) { | |||||
| console.log(` [TEST QR] Skipping — scan-pick already in flight`); | |||||
| return; | |||||
| } | |||||
| qrProcessInFlightRef.current = true; | |||||
| processOutsideQrCodeRef.current(simulatedQr, qrValues.length).then(() => { | processOutsideQrCodeRef.current(simulatedQr, qrValues.length).then(() => { | ||||
| const testTime = performance.now() - testStartTime; | const testTime = performance.now() - testStartTime; | ||||
| console.log(` [TEST QR] Total processing time: ${testTime.toFixed(2)}ms (${(testTime / 1000).toFixed(3)}s)`); | console.log(` [TEST QR] Total processing time: ${testTime.toFixed(2)}ms (${(testTime / 1000).toFixed(3)}s)`); | ||||
| @@ -2279,6 +2366,27 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| }).catch((error) => { | }).catch((error) => { | ||||
| const testTime = performance.now() - testStartTime; | const testTime = performance.now() - testStartTime; | ||||
| console.error(`❌ [TEST QR] Error after ${testTime.toFixed(2)}ms:`, error); | console.error(`❌ [TEST QR] Error after ${testTime.toFixed(2)}ms:`, error); | ||||
| }).finally(async () => { | |||||
| await new Promise((r) => setTimeout(r, 50)); | |||||
| try { | |||||
| const activeForItem = | |||||
| lotDataIndexesRef.current?.activeLotsByItemId.get(itemId); | |||||
| if ( | |||||
| activeLotsStillNeedStockInLine(activeForItem, stockInLineId) | |||||
| ) { | |||||
| processedQrCodesRef.current.delete(latestQr); | |||||
| if (lastProcessedQrRef.current === latestQr) { | |||||
| lastProcessedQrRef.current = ""; | |||||
| } | |||||
| setProcessedQrCodes(new Set(processedQrCodesRef.current)); | |||||
| setLastProcessedQr(""); | |||||
| console.log( | |||||
| ` [TEST QR] Cleared processed test QR so same lot can be scanned for remaining line(s)`, | |||||
| ); | |||||
| } | |||||
| } finally { | |||||
| qrProcessInFlightRef.current = false; | |||||
| } | |||||
| }); | }); | ||||
| } | } | ||||
| @@ -2315,11 +2423,30 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| console.log(` [QR DETECTION] Detection time: ${new Date().toISOString()}`); | console.log(` [QR DETECTION] Detection time: ${new Date().toISOString()}`); | ||||
| console.log(` [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`); | console.log(` [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`); | ||||
| // Skip if already processed (use refs to avoid dependency issues and delays) | |||||
| // Skip if already processed — unless another active line still needs the same lot (cross-POL same lot). | |||||
| const checkProcessedStartTime = performance.now(); | const checkProcessedStartTime = performance.now(); | ||||
| if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) { | |||||
| const checkTime = performance.now() - checkProcessedStartTime; | |||||
| console.log(` [QR PROCESS] Already processed check time: ${checkTime.toFixed(2)}ms`); | |||||
| const alreadySeenQr = | |||||
| processedQrCodesRef.current.has(latestQr) || | |||||
| lastProcessedQrRef.current === latestQr; | |||||
| if (alreadySeenQr) { | |||||
| const payload = parseWorkbenchScanQrPayload(latestQr); | |||||
| const activeForItem = payload | |||||
| ? lotDataIndexesRef.current?.activeLotsByItemId.get(payload.itemId) | |||||
| : undefined; | |||||
| const allowSameLotRescan = | |||||
| !!payload && | |||||
| activeLotsStillNeedStockInLine(activeForItem, payload.stockInLineId); | |||||
| if (!allowSameLotRescan) { | |||||
| const checkTime = performance.now() - checkProcessedStartTime; | |||||
| console.log(` [QR PROCESS] Already processed check time: ${checkTime.toFixed(2)}ms`); | |||||
| return; | |||||
| } | |||||
| console.log( | |||||
| ` [QR PROCESS] Allowing re-scan of same lot QR for remaining active pick line(s)`, | |||||
| ); | |||||
| } | |||||
| if (qrProcessInFlightRef.current) { | |||||
| console.log(` [QR PROCESS] Skipping — scan-pick already in flight`); | |||||
| return; | return; | ||||
| } | } | ||||
| const checkTime = performance.now() - checkProcessedStartTime; | const checkTime = performance.now() - checkProcessedStartTime; | ||||
| @@ -2389,17 +2516,46 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| // Use ref to avoid dependency issues | // Use ref to avoid dependency issues | ||||
| const processCallStartTime = performance.now(); | const processCallStartTime = performance.now(); | ||||
| if (processOutsideQrCodeRef.current) { | if (processOutsideQrCodeRef.current) { | ||||
| processOutsideQrCodeRef.current(latestQr, qrValues.length).then(() => { | |||||
| const processCallTime = performance.now() - processCallStartTime; | |||||
| const totalProcessingTime = performance.now() - processingStartTime; | |||||
| console.log(` [QR PROCESS] processOutsideQrCode call time: ${processCallTime.toFixed(2)}ms`); | |||||
| console.log(` [QR PROCESS] Total processing time: ${totalProcessingTime.toFixed(2)}ms (${(totalProcessingTime / 1000).toFixed(3)}s)`); | |||||
| }).catch((error) => { | |||||
| const processCallTime = performance.now() - processCallStartTime; | |||||
| const totalProcessingTime = performance.now() - processingStartTime; | |||||
| console.error(`❌ [QR PROCESS] processOutsideQrCode error after ${processCallTime.toFixed(2)}ms:`, error); | |||||
| console.error(`❌ [QR PROCESS] Total processing time before error: ${totalProcessingTime.toFixed(2)}ms`); | |||||
| }); | |||||
| qrProcessInFlightRef.current = true; | |||||
| processOutsideQrCodeRef.current(latestQr, qrValues.length) | |||||
| .then(() => { | |||||
| const processCallTime = performance.now() - processCallStartTime; | |||||
| const totalProcessingTime = performance.now() - processingStartTime; | |||||
| console.log(` [QR PROCESS] processOutsideQrCode call time: ${processCallTime.toFixed(2)}ms`); | |||||
| console.log(` [QR PROCESS] Total processing time: ${totalProcessingTime.toFixed(2)}ms (${(totalProcessingTime / 1000).toFixed(3)}s)`); | |||||
| }) | |||||
| .catch((error) => { | |||||
| const processCallTime = performance.now() - processCallStartTime; | |||||
| const totalProcessingTime = performance.now() - processingStartTime; | |||||
| console.error(`❌ [QR PROCESS] processOutsideQrCode error after ${processCallTime.toFixed(2)}ms:`, error); | |||||
| console.error(`❌ [QR PROCESS] Total processing time before error: ${totalProcessingTime.toFixed(2)}ms`); | |||||
| }) | |||||
| .finally(async () => { | |||||
| // Wait a tick so refresh/local status updates commit into lotDataIndexesRef. | |||||
| await new Promise((r) => setTimeout(r, 50)); | |||||
| try { | |||||
| const payload = parseWorkbenchScanQrPayload(latestQr); | |||||
| const activeForItem = payload | |||||
| ? lotDataIndexesRef.current?.activeLotsByItemId.get(payload.itemId) | |||||
| : undefined; | |||||
| if ( | |||||
| payload && | |||||
| activeLotsStillNeedStockInLine(activeForItem, payload.stockInLineId) | |||||
| ) { | |||||
| processedQrCodesRef.current.delete(latestQr); | |||||
| if (lastProcessedQrRef.current === latestQr) { | |||||
| lastProcessedQrRef.current = ""; | |||||
| } | |||||
| setProcessedQrCodes(new Set(processedQrCodesRef.current)); | |||||
| setLastProcessedQr(""); | |||||
| console.log( | |||||
| ` [QR PROCESS] Cleared processed QR so same lot can be scanned for remaining line(s)`, | |||||
| ); | |||||
| } | |||||
| } finally { | |||||
| qrProcessInFlightRef.current = false; | |||||
| } | |||||
| }); | |||||
| } | } | ||||
| // Update state for UI (but don't block on it) | // Update state for UI (but don't block on it) | ||||
| @@ -3053,6 +3209,10 @@ const handleStartScan = useCallback(() => { | |||||
| setProcessedQrCodes(new Set()); | setProcessedQrCodes(new Set()); | ||||
| setLastProcessedQr(''); | setLastProcessedQr(''); | ||||
| setProcessedQrCombinations(new Map()); | |||||
| processedQrCodesRef.current = new Set(); | |||||
| lastProcessedQrRef.current = ''; | |||||
| qrProcessInFlightRef.current = false; | |||||
| setQrScanError(false); | setQrScanError(false); | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| @@ -3742,16 +3902,22 @@ paginatedData.map((row, index) => { | |||||
| const solSt = String(lot.stockOutLineStatus || "").toLowerCase(); | const solSt = String(lot.stockOutLineStatus || "").toLowerCase(); | ||||
| const isSolRejected = | const isSolRejected = | ||||
| solSt === "rejected" || String(lot.lotAvailability || "").toLowerCase() === "rejected"; | solSt === "rejected" || String(lot.lotAvailability || "").toLowerCase() === "rejected"; | ||||
| const groupRowBg = | |||||
| row.groupDisplayIndex % 2 === 0 ? "action.hover" : "background.paper"; | |||||
| const nextRow = paginatedData[index + 1]; | |||||
| const isLastInGroup = | |||||
| !nextRow || nextRow.groupDisplayIndex !== row.groupDisplayIndex; | |||||
| return ( | return ( | ||||
| <TableRow | <TableRow | ||||
| key={`${lot.pickOrderLineId}-${lot.lotId || 'null'}`} | key={`${lot.pickOrderLineId}-${lot.lotId || 'null'}`} | ||||
| sx={{ | sx={{ | ||||
| //backgroundColor: isIssueLot ? '#fff3e0' : 'inherit', | |||||
| // opacity: isIssueLot ? 0.6 : 1, | |||||
| '& .MuiTableCell-root': { | |||||
| //color: isIssueLot ? 'warning.main' : 'inherit' | |||||
| } | |||||
| backgroundColor: groupRowBg, | |||||
| "&:nth-of-type(even)": { backgroundColor: groupRowBg }, | |||||
| "& .MuiTableCell-root": { | |||||
| backgroundColor: groupRowBg, | |||||
| ...(!isLastInGroup ? { borderBottom: "none" } : {}), | |||||
| }, | |||||
| }} | }} | ||||
| > | > | ||||
| <TableCell> | <TableCell> | ||||
| @@ -4003,58 +4169,56 @@ paginatedData.map((row, index) => { | |||||
| ? String(pickQtyData[lotKey]) | ? String(pickQtyData[lotKey]) | ||||
| : String(displayedSubmitQty) | : String(displayedSubmitQty) | ||||
| : String(displayedSubmitQty); | : String(displayedSubmitQty); | ||||
| const pickStatus = String(lot.stockOutLineStatus ?? "").toLowerCase(); | |||||
| const isRowPicked = | |||||
| pickStatus === "completed" || | |||||
| pickStatus === "checked" || | |||||
| pickStatus === "partially_completed" || | |||||
| pickStatus === "partially_complete"; | |||||
| return ( | return ( | ||||
| <Stack direction="row" spacing={1} alignItems="center"> | |||||
| {/* | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => { | |||||
| const submitQty = displayedSubmitQty; | |||||
| handlePickQtyChange(lotKey, submitQty); | |||||
| handleSubmitPickQtyWithQty(lot, submitQty, 'singleSubmit'); | |||||
| }} | |||||
| disabled={ | |||||
| lot.lotAvailability === 'expired' || | |||||
| isInventoryLotLineUnavailable(lot) || | |||||
| lot.lotAvailability === 'rejected' || | |||||
| lot.stockOutLineStatus === 'completed' || | |||||
| lot.stockOutLineStatus === 'pending' || | |||||
| (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) | |||||
| } | |||||
| sx={{ fontSize: '0.75rem', py: 0.5, minHeight: '28px', minWidth: '70px' }} | |||||
| > | |||||
| {t("Submit")} | |||||
| </Button> | |||||
| */} | |||||
| <TextField | |||||
| type="number" | |||||
| size="small" | |||||
| disabled={!qtyFieldEnabled} | |||||
| value={textFieldValue} | |||||
| onKeyDown={(e) => { | |||||
| if (!qtyFieldEnabled) return; | |||||
| if (e.key !== "{") return; | |||||
| e.preventDefault(); | |||||
| setWorkbenchSubmitQtyFieldEnabledByLotKey((prev) => ({ | |||||
| ...prev, | |||||
| [lotKey]: false, | |||||
| })); | |||||
| (e.currentTarget as HTMLInputElement).blur(); | |||||
| }} | |||||
| onChange={(e) => { | |||||
| if (!qtyFieldEnabled) return; | |||||
| const n = Number(e.target.value); | |||||
| if (Number.isFinite(n) && n < 0) return; | |||||
| handlePickQtyChange(lotKey, e.target.value); | |||||
| }} | |||||
| inputProps={{ min: 0, step: 1 }} | |||||
| sx={{ | |||||
| width: 96, | |||||
| '& .MuiInputBase-input': { fontSize: '0.75rem', py: 0.5, textAlign: 'center' }, | |||||
| }} | |||||
| /> | |||||
| <Stack direction="row" spacing={1} alignItems="center" justifyContent="center"> | |||||
| {isRowPicked ? ( | |||||
| <Typography | |||||
| variant="body2" | |||||
| sx={{ | |||||
| width: 96, | |||||
| textAlign: "center", | |||||
| fontSize: "1rem", | |||||
| fontWeight: 500, | |||||
| }} | |||||
| > | |||||
| {textFieldValue} | |||||
| </Typography> | |||||
| ) : ( | |||||
| <TextField | |||||
| type="number" | |||||
| size="small" | |||||
| disabled={!qtyFieldEnabled} | |||||
| value={textFieldValue} | |||||
| onKeyDown={(e) => { | |||||
| if (!qtyFieldEnabled) return; | |||||
| if (e.key !== "{") return; | |||||
| e.preventDefault(); | |||||
| setWorkbenchSubmitQtyFieldEnabledByLotKey((prev) => ({ | |||||
| ...prev, | |||||
| [lotKey]: false, | |||||
| })); | |||||
| (e.currentTarget as HTMLInputElement).blur(); | |||||
| }} | |||||
| onChange={(e) => { | |||||
| if (!qtyFieldEnabled) return; | |||||
| const n = Number(e.target.value); | |||||
| if (Number.isFinite(n) && n < 0) return; | |||||
| handlePickQtyChange(lotKey, e.target.value); | |||||
| }} | |||||
| inputProps={{ min: 0, step: 1 }} | |||||
| sx={{ | |||||
| width: 96, | |||||
| "& .MuiInputBase-input": { fontSize: "0.75rem", py: 0.5, textAlign: "center" }, | |||||
| }} | |||||
| /> | |||||
| )} | |||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||