| @@ -1,7 +1,8 @@ | |||||
| "use client"; | "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 { Box, Paper, Typography } from "@mui/material"; | ||||
| import type { Result } from "@zxing/library"; | |||||
| import ReactQrCodeScanner, { | import ReactQrCodeScanner, { | ||||
| ScannerConfig, | ScannerConfig, | ||||
| defaultScannerConfig, | defaultScannerConfig, | ||||
| @@ -14,6 +15,15 @@ import PutAwayReviewGrid from "./PutAwayReviewGrid"; | |||||
| import type { PutAwayRecord } from "."; | import type { PutAwayRecord } from "."; | ||||
| import type { QrCodeScanner as QrCodeScannerType } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | 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 = { | type Props = { | ||||
| warehouse: WarehouseResult[]; | warehouse: WarehouseResult[]; | ||||
| }; | }; | ||||
| @@ -51,45 +61,86 @@ const PutAwayCamScan: React.FC<Props> = ({ warehouse }) => { | |||||
| } | } | ||||
| }, [scannedWareHouseId]); | }, [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( | const handleScan = useCallback( | ||||
| (rawText: string) => { | (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"); | 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 (!Number.isFinite(num) || num <= 0) return false; | ||||
| if (scannedSilId === 0) { | |||||
| setScannedSilId(num); | |||||
| } else if (scannedWareHouseId === 0) { | |||||
| setScannedWareHouseId(num); | |||||
| } | |||||
| setScannedSilId(num); | |||||
| done(); | |||||
| return true; | 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 { | 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<string, unknown>; | |||||
| 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 { | } 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], | [scannedSilId, scannedWareHouseId], | ||||
| ); | ); | ||||
| handleScanRef.current = handleScan; | |||||
| // Open modal only after both stock-in-line and location (warehouse) are scanned | // Open modal only after both stock-in-line and location (warehouse) are scanned | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (scannedSilId > 0 && scannedWareHouseId > 0) { | if (scannedSilId > 0 && scannedWareHouseId > 0) { | ||||
| @@ -118,14 +169,17 @@ const PutAwayCamScan: React.FC<Props> = ({ warehouse }) => { | |||||
| return t("Pending scan"); | return t("Pending scan"); | ||||
| }, [scanStatus, scannedSilId, scannedWareHouseId, t]); | }, [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 ( | return ( | ||||
| <> | <> | ||||