From 435d041f5c0e33c67a793d65608f1d50d36d110d Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Fri, 6 Mar 2026 00:46:01 +0800 Subject: [PATCH] no message --- src/components/PutAwayScan/PutAwayCamScan.tsx | 112 +++++++++++++----- 1 file changed, 83 insertions(+), 29 deletions(-) diff --git a/src/components/PutAwayScan/PutAwayCamScan.tsx b/src/components/PutAwayScan/PutAwayCamScan.tsx index 00b6c0a..20ae585 100644 --- a/src/components/PutAwayScan/PutAwayCamScan.tsx +++ b/src/components/PutAwayScan/PutAwayCamScan.tsx @@ -1,7 +1,8 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Box, Paper, Typography } from "@mui/material"; +import type { Result } from "@zxing/library"; import ReactQrCodeScanner, { ScannerConfig, defaultScannerConfig, @@ -14,6 +15,15 @@ import PutAwayReviewGrid from "./PutAwayReviewGrid"; import type { PutAwayRecord } from "."; import type { QrCodeScanner as QrCodeScannerType } from "../QrCodeScannerProvider/QrCodeScannerProvider"; +/** Find first number after a keyword in a string (e.g. "StockInLine" or "warehouseId"). */ +function findIdByRoughMatch(inputString: string, keyword: string): number | null { + const idx = inputString.indexOf(keyword); + if (idx === -1) return null; + const after = inputString.slice(idx + keyword.length); + const match = after.match(/\d+/); + return match ? parseInt(match[0], 10) : null; +} + type Props = { warehouse: WarehouseResult[]; }; @@ -51,45 +61,86 @@ const PutAwayCamScan: React.FC = ({ warehouse }) => { } }, [scannedWareHouseId]); + // Refs so the scanner (which only gets config on mount) always calls the latest handler and we throttle duplicates + const handleScanRef = useRef<(rawText: string) => void>(() => {}); + const lastScannedRef = useRef({ text: "", at: 0 }); + const THROTTLE_MS = 2000; + const handleScan = useCallback( (rawText: string) => { - if (!rawText) return; + const trimmed = (rawText || "").trim(); + if (!trimmed) return; + const now = Date.now(); + if ( + lastScannedRef.current.text === trimmed && + now - lastScannedRef.current.at < THROTTLE_MS + ) { + return; + } setScanStatus("scanning"); - const trySetNumeric = (value: unknown) => { - const num = Number(value); + const done = () => { + lastScannedRef.current = { text: trimmed, at: now }; + }; + + const trySetSilId = (num: number): boolean => { if (!Number.isFinite(num) || num <= 0) return false; - if (scannedSilId === 0) { - setScannedSilId(num); - } else if (scannedWareHouseId === 0) { - setScannedWareHouseId(num); - } + setScannedSilId(num); + done(); return true; }; + const trySetWarehouseId = (num: number): boolean => { + if (!Number.isFinite(num) || num <= 0) return false; + setScannedWareHouseId(num); + done(); + return true; + }; + + const isFirstScan = scannedSilId === 0; + const isSecondScan = scannedSilId > 0 && scannedWareHouseId === 0; - // 1) Try JSON payload first + // 1) Try JSON try { - const data = JSON.parse(rawText) as any; - if (data) { - if (scannedSilId === 0) { - if (data.stockInLineId && trySetNumeric(data.stockInLineId)) return; - if (data.value && trySetNumeric(data.value)) return; - } else { - if (data.warehouseId && trySetNumeric(data.warehouseId)) return; - if (data.value && trySetNumeric(data.value)) return; + const data = JSON.parse(trimmed) as Record; + if (data && typeof data === "object") { + if (isFirstScan) { + if (data.stockInLineId != null && trySetSilId(Number(data.stockInLineId))) return; + if (data.value != null && trySetSilId(Number(data.value))) return; + } + if (isSecondScan) { + if (data.warehouseId != null && trySetWarehouseId(Number(data.warehouseId))) return; + if (data.value != null && trySetWarehouseId(Number(data.value))) return; } } } catch { - // Not JSON – fall through to numeric parsing + // not JSON } - // 2) Fallback: plain numeric content - if (trySetNumeric(rawText)) return; + // 2) Rough match: "StockInLine" or "warehouseId" + number (same as barcode scanner) + if (isFirstScan) { + const sil = + findIdByRoughMatch(trimmed, "StockInLine") ?? + findIdByRoughMatch(trimmed, "stockInLineId"); + if (sil != null && trySetSilId(sil)) return; + } + if (isSecondScan) { + const wh = + findIdByRoughMatch(trimmed, "warehouseId") ?? + findIdByRoughMatch(trimmed, "WarehouseId"); + if (wh != null && trySetWarehouseId(wh)) return; + } + + // 3) Plain number + const num = Number(trimmed); + if (isFirstScan && trySetSilId(num)) return; + if (isSecondScan && trySetWarehouseId(num)) return; }, [scannedSilId, scannedWareHouseId], ); + handleScanRef.current = handleScan; + // Open modal only after both stock-in-line and location (warehouse) are scanned useEffect(() => { if (scannedSilId > 0 && scannedWareHouseId > 0) { @@ -118,14 +169,17 @@ const PutAwayCamScan: React.FC = ({ warehouse }) => { return t("Pending scan"); }, [scanStatus, scannedSilId, scannedWareHouseId, t]); - const scannerConfig: ScannerConfig = { - ...defaultScannerConfig, - onUpdate: (_err, result) => { - if (result) { - handleScan(result.getText()); - } - }, - }; + const scannerConfig: ScannerConfig = useMemo( + () => ({ + ...defaultScannerConfig, + onUpdate: (_err: unknown, result?: Result): void => { + if (result) { + handleScanRef.current(result.getText()); + } + }, + }), + [], + ); return ( <>