FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

238 line
7.2 KiB

  1. "use client";
  2. import { useCallback, useEffect, useMemo, useRef, useState } from "react";
  3. import { Box, Paper, Typography } from "@mui/material";
  4. import type { Result } from "@zxing/library";
  5. import ReactQrCodeScanner, {
  6. ScannerConfig,
  7. defaultScannerConfig,
  8. } from "../ReactQrCodeScanner/ReactQrCodeScanner";
  9. import { WarehouseResult } from "@/app/api/warehouse";
  10. import { useTranslation } from "react-i18next";
  11. import { QrCodeScanner as QrCodeIcon } from "@mui/icons-material";
  12. import PutAwayModal from "./PutAwayModal";
  13. import PutAwayReviewGrid from "./PutAwayReviewGrid";
  14. import type { PutAwayRecord } from ".";
  15. import type { QrCodeScanner as QrCodeScannerType } from "../QrCodeScannerProvider/QrCodeScannerProvider";
  16. /** Find first number after a keyword in a string (e.g. "StockInLine" or "warehouseId"). */
  17. function findIdByRoughMatch(inputString: string, keyword: string): number | null {
  18. const idx = inputString.indexOf(keyword);
  19. if (idx === -1) return null;
  20. const after = inputString.slice(idx + keyword.length);
  21. const match = after.match(/\d+/);
  22. return match ? parseInt(match[0], 10) : null;
  23. }
  24. type Props = {
  25. warehouse: WarehouseResult[];
  26. };
  27. type ScanStatusType = "pending" | "scanning";
  28. const dummyScanner: QrCodeScannerType = {
  29. values: [],
  30. isScanning: false,
  31. startScan: () => {},
  32. stopScan: () => {},
  33. resetScan: () => {},
  34. result: undefined,
  35. state: "pending",
  36. error: undefined,
  37. };
  38. const PutAwayCamScan: React.FC<Props> = ({ warehouse }) => {
  39. const { t } = useTranslation("putAway");
  40. const [scanStatus, setScanStatus] = useState<ScanStatusType>("pending");
  41. const [openPutAwayModal, setOpenPutAwayModal] = useState(false);
  42. const [scannedSilId, setScannedSilId] = useState<number>(0);
  43. const [scannedWareHouseId, setScannedWareHouseId] = useState<number>(0);
  44. const [putAwayHistory, setPutAwayHistory] = useState<PutAwayRecord[]>([]);
  45. const addPutAwayHistory = (putAwayData: PutAwayRecord) => {
  46. const newPutaway = { ...putAwayData, id: putAwayHistory.length + 1 };
  47. setPutAwayHistory((prev) => [...prev, newPutaway]);
  48. };
  49. const handleSetDefaultWarehouseId = useCallback((warehouseId: number) => {
  50. if (scannedWareHouseId === 0) {
  51. setScannedWareHouseId(warehouseId);
  52. }
  53. }, [scannedWareHouseId]);
  54. // Refs so the scanner (which only gets config on mount) always calls the latest handler and we throttle duplicates
  55. const handleScanRef = useRef<(rawText: string) => void>(() => {});
  56. const lastScannedRef = useRef({ text: "", at: 0 });
  57. const THROTTLE_MS = 2000;
  58. const handleScan = useCallback(
  59. (rawText: string) => {
  60. const trimmed = (rawText || "").trim();
  61. if (!trimmed) return;
  62. const now = Date.now();
  63. if (
  64. lastScannedRef.current.text === trimmed &&
  65. now - lastScannedRef.current.at < THROTTLE_MS
  66. ) {
  67. return;
  68. }
  69. setScanStatus("scanning");
  70. const done = () => {
  71. lastScannedRef.current = { text: trimmed, at: now };
  72. };
  73. const trySetSilId = (num: number): boolean => {
  74. if (!Number.isFinite(num) || num <= 0) return false;
  75. setScannedSilId(num);
  76. done();
  77. return true;
  78. };
  79. const trySetWarehouseId = (num: number): boolean => {
  80. if (!Number.isFinite(num) || num <= 0) return false;
  81. setScannedWareHouseId(num);
  82. done();
  83. return true;
  84. };
  85. const isFirstScan = scannedSilId === 0;
  86. const isSecondScan = scannedSilId > 0 && scannedWareHouseId === 0;
  87. // 1) Try JSON
  88. try {
  89. const data = JSON.parse(trimmed) as Record<string, unknown>;
  90. if (data && typeof data === "object") {
  91. if (isFirstScan) {
  92. if (data.stockInLineId != null && trySetSilId(Number(data.stockInLineId))) return;
  93. if (data.value != null && trySetSilId(Number(data.value))) return;
  94. }
  95. if (isSecondScan) {
  96. if (data.warehouseId != null && trySetWarehouseId(Number(data.warehouseId))) return;
  97. if (data.value != null && trySetWarehouseId(Number(data.value))) return;
  98. }
  99. }
  100. } catch {
  101. // not JSON
  102. }
  103. // 2) Rough match: "StockInLine" or "warehouseId" + number (same as barcode scanner)
  104. if (isFirstScan) {
  105. const sil =
  106. findIdByRoughMatch(trimmed, "StockInLine") ??
  107. findIdByRoughMatch(trimmed, "stockInLineId");
  108. if (sil != null && trySetSilId(sil)) return;
  109. }
  110. if (isSecondScan) {
  111. const wh =
  112. findIdByRoughMatch(trimmed, "warehouseId") ??
  113. findIdByRoughMatch(trimmed, "WarehouseId");
  114. if (wh != null && trySetWarehouseId(wh)) return;
  115. }
  116. // 3) Plain number
  117. const num = Number(trimmed);
  118. if (isFirstScan && trySetSilId(num)) return;
  119. if (isSecondScan && trySetWarehouseId(num)) return;
  120. },
  121. [scannedSilId, scannedWareHouseId],
  122. );
  123. handleScanRef.current = handleScan;
  124. // Open modal only after both stock-in-line and location (warehouse) are scanned
  125. useEffect(() => {
  126. if (scannedSilId > 0 && scannedWareHouseId > 0) {
  127. setOpenPutAwayModal(true);
  128. setScanStatus("pending");
  129. }
  130. }, [scannedSilId, scannedWareHouseId]);
  131. const closeModal = () => {
  132. setScannedSilId(0);
  133. setScannedWareHouseId(0);
  134. setOpenPutAwayModal(false);
  135. setScanStatus("pending");
  136. };
  137. const displayText = useMemo(() => {
  138. if (scanStatus === "scanning") {
  139. return t("Scanning");
  140. }
  141. if (scannedSilId > 0 && scannedWareHouseId > 0) {
  142. return t("Scanned, opening detail");
  143. }
  144. if (scannedSilId > 0) {
  145. return t("Please scan warehouse qr code");
  146. }
  147. return t("Pending scan");
  148. }, [scanStatus, scannedSilId, scannedWareHouseId, t]);
  149. const scannerConfig: ScannerConfig = useMemo(
  150. () => ({
  151. ...defaultScannerConfig,
  152. onUpdate: (_err: unknown, result?: Result): void => {
  153. if (result) {
  154. handleScanRef.current(result.getText());
  155. }
  156. },
  157. }),
  158. [],
  159. );
  160. return (
  161. <>
  162. <Paper
  163. sx={{
  164. display: "flex",
  165. flexDirection: "column",
  166. justifyContent: "center",
  167. alignItems: "center",
  168. textAlign: "center",
  169. gap: 2,
  170. p: 2,
  171. }}
  172. >
  173. <Typography variant="h4" sx={{ mb: 1 }}>
  174. {displayText}
  175. </Typography>
  176. <QrCodeIcon sx={{ fontSize: 80, mb: 1 }} color="primary" />
  177. <Box
  178. sx={{
  179. width: "100%",
  180. maxWidth: 480,
  181. aspectRatio: "4 / 3",
  182. overflow: "hidden",
  183. }}
  184. >
  185. <ReactQrCodeScanner scannerConfig={scannerConfig} />
  186. </Box>
  187. </Paper>
  188. {putAwayHistory.length > 0 && (
  189. <>
  190. <Typography variant="h5" sx={{ mt: 3, mb: 1 }}>
  191. {t("putAwayHistory")}
  192. </Typography>
  193. <PutAwayReviewGrid putAwayHistory={putAwayHistory} />
  194. </>
  195. )}
  196. <PutAwayModal
  197. open={openPutAwayModal}
  198. onClose={closeModal}
  199. warehouse={warehouse}
  200. stockInLineId={scannedSilId}
  201. warehouseId={scannedWareHouseId}
  202. scanner={dummyScanner}
  203. addPutAwayHistory={addPutAwayHistory}
  204. onSetDefaultWarehouseId={handleSetDefaultWarehouseId}
  205. />
  206. </>
  207. );
  208. };
  209. export default PutAwayCamScan;