| @@ -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<Props> = ({ 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<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 { | |||
| // 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<Props> = ({ 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 ( | |||
| <> | |||