|
- "use client";
-
- 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,
- } from "../ReactQrCodeScanner/ReactQrCodeScanner";
- import { WarehouseResult } from "@/app/api/warehouse";
- import { useTranslation } from "react-i18next";
- import { QrCodeScanner as QrCodeIcon } from "@mui/icons-material";
- import PutAwayModal from "./PutAwayModal";
- 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[];
- };
-
- type ScanStatusType = "pending" | "scanning";
-
- const dummyScanner: QrCodeScannerType = {
- values: [],
- isScanning: false,
- startScan: () => {},
- stopScan: () => {},
- resetScan: () => {},
- result: undefined,
- state: "pending",
- error: undefined,
- };
-
- const PutAwayCamScan: React.FC<Props> = ({ warehouse }) => {
- const { t } = useTranslation("putAway");
-
- const [scanStatus, setScanStatus] = useState<ScanStatusType>("pending");
- const [openPutAwayModal, setOpenPutAwayModal] = useState(false);
- const [scannedSilId, setScannedSilId] = useState<number>(0);
- const [scannedWareHouseId, setScannedWareHouseId] = useState<number>(0);
- const [putAwayHistory, setPutAwayHistory] = useState<PutAwayRecord[]>([]);
-
- const addPutAwayHistory = (putAwayData: PutAwayRecord) => {
- const newPutaway = { ...putAwayData, id: putAwayHistory.length + 1 };
- setPutAwayHistory((prev) => [...prev, newPutaway]);
- };
-
- const handleSetDefaultWarehouseId = useCallback((warehouseId: number) => {
- if (scannedWareHouseId === 0) {
- setScannedWareHouseId(warehouseId);
- }
- }, [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) => {
- 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 done = () => {
- lastScannedRef.current = { text: trimmed, at: now };
- };
-
- const trySetSilId = (num: number): boolean => {
- if (!Number.isFinite(num) || num <= 0) return false;
- 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
- try {
- 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
- }
-
- // 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) {
- setOpenPutAwayModal(true);
- setScanStatus("pending");
- }
- }, [scannedSilId, scannedWareHouseId]);
-
- const closeModal = () => {
- setScannedSilId(0);
- setScannedWareHouseId(0);
- setOpenPutAwayModal(false);
- setScanStatus("pending");
- };
-
- const displayText = useMemo(() => {
- if (scanStatus === "scanning") {
- return t("Scanning");
- }
- if (scannedSilId > 0 && scannedWareHouseId > 0) {
- return t("Scanned, opening detail");
- }
- if (scannedSilId > 0) {
- return t("Please scan warehouse qr code");
- }
- return t("Pending scan");
- }, [scanStatus, scannedSilId, scannedWareHouseId, t]);
-
- const scannerConfig: ScannerConfig = useMemo(
- () => ({
- ...defaultScannerConfig,
- onUpdate: (_err: unknown, result?: Result): void => {
- if (result) {
- handleScanRef.current(result.getText());
- }
- },
- }),
- [],
- );
-
- return (
- <>
- <Paper
- sx={{
- display: "flex",
- flexDirection: "column",
- justifyContent: "center",
- alignItems: "center",
- textAlign: "center",
- gap: 2,
- p: 2,
- }}
- >
- <Typography variant="h4" sx={{ mb: 1 }}>
- {displayText}
- </Typography>
- <QrCodeIcon sx={{ fontSize: 80, mb: 1 }} color="primary" />
- <Box
- sx={{
- width: "100%",
- maxWidth: 480,
- aspectRatio: "4 / 3",
- overflow: "hidden",
- }}
- >
- <ReactQrCodeScanner scannerConfig={scannerConfig} />
- </Box>
- </Paper>
-
- {putAwayHistory.length > 0 && (
- <>
- <Typography variant="h5" sx={{ mt: 3, mb: 1 }}>
- {t("putAwayHistory")}
- </Typography>
- <PutAwayReviewGrid putAwayHistory={putAwayHistory} />
- </>
- )}
-
- <PutAwayModal
- open={openPutAwayModal}
- onClose={closeModal}
- warehouse={warehouse}
- stockInLineId={scannedSilId}
- warehouseId={scannedWareHouseId}
- scanner={dummyScanner}
- addPutAwayHistory={addPutAwayHistory}
- onSetDefaultWarehouseId={handleSetDefaultWarehouseId}
- />
- </>
- );
- };
-
- export default PutAwayCamScan;
|