From a51e5f9dd5743eb5f53ec40f7c92d2b13ab8ee59 Mon Sep 17 00:00:00 2001 From: kelvinsuen Date: Wed, 3 Sep 2025 05:34:54 +0800 Subject: [PATCH] update putaway scan --- src/app/(main)/putAway/page.tsx | 2 +- src/components/PoDetail/PutAwayForm.tsx | 2 +- src/components/PoDetail/QcComponent.tsx | 12 + src/components/PoDetail/QcStockInModal.tsx | 8 +- src/components/PutAwayScan/PutAwayModal.tsx | 419 ++++++++++++++++++ src/components/PutAwayScan/PutAwayScan.tsx | 194 +++++++- .../PutAwayScan/PutAwayScanWrapper.tsx | 9 +- .../QrCodeScannerProvider.tsx | 4 +- src/i18n/zh/purchaseOrder.json | 2 +- src/i18n/zh/putAway.json | 19 +- 10 files changed, 654 insertions(+), 17 deletions(-) create mode 100644 src/components/PutAwayScan/PutAwayModal.tsx diff --git a/src/app/(main)/putAway/page.tsx b/src/app/(main)/putAway/page.tsx index 0f36748..c257dc6 100644 --- a/src/app/(main)/putAway/page.tsx +++ b/src/app/(main)/putAway/page.tsx @@ -28,7 +28,7 @@ const PutAway: React.FC = async () => { {t("Put Away")} - + }> diff --git a/src/components/PoDetail/PutAwayForm.tsx b/src/components/PoDetail/PutAwayForm.tsx index e3ccf3a..7f4512d 100644 --- a/src/components/PoDetail/PutAwayForm.tsx +++ b/src/components/PoDetail/PutAwayForm.tsx @@ -540,7 +540,7 @@ const PutAwayForm: React.FC = ({ itemDetail, warehouse, disabled, setRowM _formKey={"putAwayLines"} columns={columns} validateRow={validation} - needAdd={true} + needAdd={false} showRemoveBtn={false} addRowDefaultValue={addRowDefaultValue} _setRowModesModel={setRowModesModel} diff --git a/src/components/PoDetail/QcComponent.tsx b/src/components/PoDetail/QcComponent.tsx index 1a67059..15cd55f 100644 --- a/src/components/PoDetail/QcComponent.tsx +++ b/src/components/PoDetail/QcComponent.tsx @@ -527,6 +527,18 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false }) => { + + + Group A - 急凍貨類 (QCA1-MEAT01) + + + 品檢類型:IQC + + + 記錄探測溫度的時間,請在1小時内完成卸貨盤點入庫,以保障食品安全
+ 監察方法:目視檢查、嗅覺檢查和使用適當的食物溫度計,檢查食物溫度是否符合指標 +
+
line._isNew !== false).length <= 0) { - alert("請新增上架資料!"); - return; - } + // if (data.putAwayLines!!.filter((line) => line._isNew !== false).length <= 0) { + // alert("請新增上架資料!"); + // return; + // } if (data.putAwayLines!!.filter((line) => /[^0-9]/.test(String(line.qty))).length > 0) { //TODO Improve alert("上架數量不正確!"); return; diff --git a/src/components/PutAwayScan/PutAwayModal.tsx b/src/components/PutAwayScan/PutAwayModal.tsx new file mode 100644 index 0000000..11f89ad --- /dev/null +++ b/src/components/PutAwayScan/PutAwayModal.tsx @@ -0,0 +1,419 @@ +"use client"; + +import { + Box, + Button, + Grid, + Modal, + ModalProps, + Stack, + TextField, + Typography, + Paper, +} from "@mui/material"; +import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react"; +import ReactQrCodeScanner, { + ScannerConfig, +} from "../ReactQrCodeScanner/ReactQrCodeScanner"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import { + fetchStockInLineInfo, + ModalFormInput, + StockInLineEntry, + updateStockInLine, +} from "@/app/api/po/actions"; +import { StockInLine } from "@/app/api/po"; +import { WarehouseResult } from "@/app/api/warehouse"; +// import { QrCodeInfo } from "@/app/api/qrcde"; +import { Check, QrCode } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; +import { useSearchParams } from "next/navigation"; +import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; +import LoadingComponent from "../General/LoadingComponent"; +import StockInForm from "../PoDetail/StockInForm"; +import { arrayToDateString, INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; +import { QrCodeScanner } from "../QrCodeScannerProvider/QrCodeScannerProvider"; +import { msg } from "../Swal/CustomAlerts"; + + +interface Props extends Omit { + warehouse: WarehouseResult[]; + stockInLineId: number; + warehouseId: number; + scanner: QrCodeScanner; +} +const style = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + bgcolor: "background.paper", + pt: 5, + px: 5, + pb: 10, + // width: "auto", + width: { xs: "90%", sm: "90%", md: "90%" }, +}; + +const scannerStyle = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + bgcolor: "background.paper", + pt: 5, + px: 5, + pb: 10, + // width: "auto", + width: { xs: "60%", sm: "60%", md: "60%" }, +}; + +const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId, warehouseId, scanner }) => { + const { t } = useTranslation("putAway"); + const [serverError, setServerError] = useState(""); + const params = useSearchParams(); + + const [isOpenScanner, setIsOpenScanner] = useState(false); + + const [itemDetail, setItemDetail] = useState(); + const [unavailableText, setUnavailableText] = useState( + undefined, + ); + + const [putQty, setPutQty] = useState(itemDetail?.demandQty ?? 0); + + const defaultNewValue = useMemo(() => { + // console.log("%c ItemDetail", "color:purple", itemDetail); + return ( + { + ...itemDetail, + // status: itemDetail.status ?? "pending", + dnDate: arrayToDateString(itemDetail?.dnDate, "input")?? undefined, + // // putAwayLines: dummyPutAwayLine, + // // putAwayLines: itemDetail.putAwayLines.map((line) => (return {...line, printQty: 1})) ?? [], + // putAwayLines: itemDetail.putAwayLines?.map((line) => ({...line, printQty: 1, _isNew: false})) ?? [], + // // qcResult: (itemDetail.qcResult && itemDetail.qcResult?.length > 0) ? itemDetail.qcResult : [],//[...dummyQCData], + // escResult: (itemDetail.escResult && itemDetail.escResult?.length > 0) ? itemDetail.escResult : [], + productionDate: itemDetail?.productionDate ? arrayToDateString(itemDetail?.productionDate, "input") : undefined, + expiryDate: itemDetail?.expiryDate ? arrayToDateString(itemDetail?.expiryDate, "input") : undefined, + receiptDate: itemDetail?.receiptDate ? arrayToDateString(itemDetail?.receiptDate, "input") : undefined, + // acceptQty: itemDetail.demandQty?? itemDetail.acceptedQty, + defaultWarehouseId: itemDetail?.defaultWarehouseId ?? 1, + } + ) + }, [itemDetail]) + + const formProps = useForm({ + defaultValues: { + ...defaultNewValue, + }, + }); + const errors = formProps.formState.errors; + + + const closeHandler = useCallback>( + (...args) => { + setVerified(false); + setItemDetail(undefined); + onClose?.(...args); + // reset(); + }, + [onClose], + ); + + const scannerCloseHandler = useCallback>( + (...args) => { + setIsOpenScanner(false); + scanner.stopScan(); + console.log("%c Scanning stopped ", "color:cyan"); + }, + [], + ); + + const openScanner = () => { + setIsOpenScanner(true); + scanner.startScan(); + console.log("%c Scanning started ", "color:cyan"); + }; + + useEffect(() => { + if (warehouseId > 0) { // Scanned Warehouse + if (isOpenScanner) { + setIsOpenScanner(false); + setVerified(true); + scanner.stopScan(); + console.log("%c Scanner stopped", "color:cyan"); + } + } + }, [warehouseId]) + + useLayoutEffect(() => { + if (itemDetail !== undefined) { + if (itemDetail.status == "received") { + formProps.reset({ + ...defaultNewValue + }) + setPutQty(itemDetail?.demandQty); + console.log("%c Loaded data:", "color:lime", defaultNewValue); + } else { + switch (itemDetail.status) { + case "pending": alert("此貨品有待品檢"); break; + case "rejected": alert("此貨品已被拒收"); break; + case "escalated": alert("此貨品已被上報"); break; + case "partially_completed": alert("此貨品已部分上架"); break; + case "completed": alert("此貨品已上架"); break; + default: alert("此貨品暫時無法上架"); break; + } + + closeHandler({}, "backdropClick"); + } + } + }, [open, itemDetail, defaultNewValue]) + + const fetchStockInLine = useCallback( + async (stockInLineId: number) => { + setUnavailableText(undefined); + const res = await fetchStockInLineInfo(stockInLineId); + console.log("%c Fetched Stock In Line Info:", "color:gold", res); + setItemDetail(res); + }, + [formProps, itemDetail, fetchStockInLineInfo], + ); + + useEffect(() => { + if (stockInLineId) { fetchStockInLine(stockInLineId); } + }, [stockInLineId]); + + const [verified, setVerified] = useState(false); + + const [qtyError, setQtyError] = useState(""); + + const validateQty = useCallback((qty : number = putQty) => { + // if (isNaN(putQty) || putQty === undefined || putQty === null || typeof(putQty) != "number") { + // setQtyError(t("value must be a number")); + // } else + if (!Number.isInteger(qty)) { + setQtyError(t("value must be integer")); + } + if (qty > itemDetail?.acceptedQty!!) { + setQtyError(`${t("putQty must not greater than")} ${ + itemDetail?.acceptedQty}` ); + } else + if (qty < 1) { + setQtyError(t("minimal value is 1")); + } else { + setQtyError(""); + } + console.log("%c Validated putQty:", "color:yellow", putQty); + },[setQtyError, putQty, itemDetail]) + + const onSubmit = useCallback>( + async (data, event) => { + // console.log("errors", errors); + // const lotLine = { + // warehouseId: warehouseId, + // qty: acceptQty; + // } + try { + const args = { + // ...itemDetail, + id: itemDetail?.id, + purchaseOrderId: itemDetail?.purchaseOrderId, + purchaseOrderLineId: itemDetail?.purchaseOrderLineId, + itemId: itemDetail?.itemId, + acceptedQty: itemDetail?.acceptedQty, + acceptQty: itemDetail?.demandQty, + status: "received", + // purchaseOrderId: parseInt(params.get("id")!), + // purchaseOrderLineId: itemDetail?.purchaseOrderLineId, + // itemId: itemDetail?.itemId, + // acceptedQty: data.acceptedQty, + // status: data.status, + // ...data, + // productionDate: productionDate, + + // for putaway data + + inventoryLotLines: [{ + warehouseId: warehouseId, + qty: putQty, + }], + // data.putAwayLines?.filter((line) => line._isNew !== false) + + } as StockInLineEntry & ModalFormInput; + + console.log(args); + // return + // if (formProps.formState.errors) { + // setServerError(t("An error has occurred. Please try again later.")); + // return false; + // } + if (qtyError !== "") { + return; + } + console.log("%c Submitting Data:", "color:blue", args); + const res = await updateStockInLine(args); + if (Boolean(res.id)) { + // update entries + console.log("%c Update Success:", "color:green", res); + // add loading + msg("貨品上架成功!"); + + closeHandler({}, "backdropClick"); + } + console.log(res); + // if (res) + } catch (e) { + // server error + setServerError(t("An error has occurred. Please try again later.")); + console.log(e); + } + }, + [t, itemDetail, putQty, warehouseId], + ); + + return ( + + + + + + {itemDetail != undefined ? ( + <> + + + 處理上架 + + + + + + + + + { + const value = e.target.value; + validateQty(Number(value)); + setPutQty(Number(value)); + }} + // onBlur={(e) => { + // const value = e.target.value; + // setPutQty(Number(value)); + // validateQty(Number(value)); + // }} + // disabled={true} + // {...register("acceptedQty", { + // required: "acceptedQty required!", + // })} + error={qtyError !== ""} + helperText={qtyError} + /> + + + + + + 0 ? warehouse.find((w) => w.id == warehouseId)?.name + : warehouse.find((w) => w.id == 3)?.name + // : warehouse.find((w) => w.id == itemDetail.defaultWarehouseId)?.name //TODO fix empty + } + // {...register("acceptedQty", { + // required: "acceptedQty required!", + // })} + error={!verified} + helperText={verified ? "掃碼完成" : "等待掃碼"} + /> + + {/* */} + + + + + + + + + ) : ( + // + <> + + {t("scan loading")} + + + + )} + + + + + + + {t("Please scan warehouse qr code")} + + {/* */} + + + + + + + ); +}; + +export default PutAwayModal; diff --git a/src/components/PutAwayScan/PutAwayScan.tsx b/src/components/PutAwayScan/PutAwayScan.tsx index e94dcd1..e2a8937 100644 --- a/src/components/PutAwayScan/PutAwayScan.tsx +++ b/src/components/PutAwayScan/PutAwayScan.tsx @@ -1,12 +1,198 @@ -import React from "react"; +"use client"; +import { + Box, + Button, + Paper, + Grid, + Modal, + ModalProps, + Stack, + Typography, + } from "@mui/material"; + import { useCallback, useEffect, useMemo, useState } from "react"; + import ReactQrCodeScanner, { + ScannerConfig, + } from "../ReactQrCodeScanner/ReactQrCodeScanner"; + import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; + import { + fetchStockInLineInfo, + ModalFormInput, + StockInLineEntry, + updateStockInLine, + } from "@/app/api/po/actions"; + import { StockInLine } from "@/app/api/po"; + import { WarehouseResult } from "@/app/api/warehouse"; + import { QrCodeInfo } from "@/app/api/qrcode"; + import { Check, QrCodeScanner, Warehouse } from "@mui/icons-material"; + import { useTranslation } from "react-i18next"; + import { useSearchParams } from "next/navigation"; + import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; +import PutAwayModal from "./PutAwayModal"; type Props = { - + warehouse : WarehouseResult[]; }; -const PutAwayScan: React.FC = ({} ) => { - return <> +type ScanStatusType = "pending" | "rescan"; + +const PutAwayScan: React.FC = ({ warehouse }) => { + const { t } = useTranslation("putAway"); + + const [scanDisplay, setScanDisplay] = useState("pending"); + const [openPutAwayModal, setOpenPutAwayModal] = useState(false); + const [scannedSilId, setScannedSilId] = useState(0); // TODO use QR code info + const [scannedWareHouseId, setScannedWareHouseId] = useState(0); // TODO use QR code info + + // QR Code Scanner + const scanner = useQrCodeScannerContext(); + useEffect(() => { + if (!scanner.isScanning) { + scanner.startScan(); + console.log("%c Scanning started ", "color:cyan"); + } else if (scanner.isScanning) { + scanner.stopScan(); + console.log("%c Scanning stopped ", "color:cyan"); + } + }, []); + + const resetScan = (error : string = "") => { + if (error !== "") { + console.log("%c Scan failed, error: ", "color:red", error); + setScanDisplay("rescan"); + } else { + console.log("%c Scan reset", "color:red"); + } + scanner.resetScan(); + }; + + const openModal = () => { + console.log("Scanned successfully and open modal"); + scanner.stopScan(); + console.log("%c Scanning stopped ", "color:cyan"); + setOpenPutAwayModal(true); + setScanDisplay("pending"); + }; + + const closeModal = () => { + setScannedSilId(0); + setScannedWareHouseId(0); + setScanDisplay("pending"); + console.log("Modal Closed"); + scanner.startScan(); + console.log("%c Scanning started ", "color:cyan"); + setOpenPutAwayModal(false); + } + + + const findIdByRoughMatch = (inputString : string, keyword : string) => { + const keywordIndex = inputString.indexOf(keyword); + + if (keywordIndex === -1) { + return { + keywordFound: false, + number: null, + message: `${keyword} not found in the input`, + }; + } + + const substringAfterKeyword = inputString.slice(keywordIndex + keyword.length); + + const numberMatch = substringAfterKeyword.match(/\d+/); + + if (!numberMatch) { + return { + keywordFound: true, + number: null, + message: `No valid number found after ${keyword}`, + }; + } + + return { + keywordFound: true, + number: parseInt(numberMatch[0], 10), + message: `Found ${keyword} at index ${keywordIndex}, first number found after is: ${numberMatch[0]}`, + }; + } + + + useEffect(() => { + if (scannedSilId > 0) { + openModal(); + } + }, [scannedSilId]) + + // Get Scanned Values + useEffect(() => { + if (scanner.values.length > 0) {//} && !Boolean(itemDetail)) { + const scannedValues = scanner.values[0]; + console.log("%c Scanned: ", "color:cyan", scannedValues); + + if (scannedValues.substring(0, 8) == "{2fitest") { // DEBUGGING + const number = scannedValues.substring(8, scannedValues.length - 1); + if (/^\d+$/.test(number)) { // Check if number contains only digits + console.log("%c DEBUG: Testing SIL ID: ", "color:cyan", number); + if (scannedSilId === 0) { + setScannedSilId(Number(number)); + } else setScannedWareHouseId(Number(number)); + } else { + console.error("%c DEBUG: Invalid number format: ", "color:red", number); + resetScan(); + } + return; + } + try { + const data: QrCodeInfo = JSON.parse(scannedValues); + console.log("%c Scanned with data", "color:green", data); + + if (scannedSilId == 0) { // Initial State + if (data.stockInLineId !== undefined) { + setScannedSilId(Number(data.stockInLineId)); + } else resetScan("Cannot read Stock In Line Id"); + } else { // Processing + if (data.warehouseId !== undefined) { + setScannedWareHouseId(Number(data.warehouseId)); + } else resetScan("Cannot read Warehouse Id"); + } + } catch (error) { // Rought match for other scanner -- Pending Review + if (scannedSilId == 0) { + const silId = findIdByRoughMatch(scannedValues, "StockInLine").number ?? 0; + setScannedSilId(silId); + + } else { + const whId = findIdByRoughMatch(scannedValues, "warehouseId").number ?? 0; + setScannedWareHouseId(whId); + } + + resetScan(String(error)); + } + scanner.resetScan(); + } + }, [scanner.values]); + + return (<> + + + {scanDisplay == "pending" ? t("Pending scan") : t("Rescan")} + + + + + + ) } export default PutAwayScan; \ No newline at end of file diff --git a/src/components/PutAwayScan/PutAwayScanWrapper.tsx b/src/components/PutAwayScan/PutAwayScanWrapper.tsx index 3f11222..e8c7c80 100644 --- a/src/components/PutAwayScan/PutAwayScanWrapper.tsx +++ b/src/components/PutAwayScan/PutAwayScanWrapper.tsx @@ -1,13 +1,20 @@ import React from "react"; import GeneralLoading from "../General/GeneralLoading"; import PutAwayScan from "./PutAwayScan"; +import { fetchWarehouseList } from "@/app/api/warehouse"; interface SubComponents { Loading: typeof GeneralLoading; } const PutAwayScanWrapper: React.FC & SubComponents = async () => { - return + const [ + warehouse, + ] = await Promise.all([ + fetchWarehouseList(), + ]) + + return } PutAwayScanWrapper.Loading = GeneralLoading; diff --git a/src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx b/src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx index 9e78bb3..76ec9f9 100644 --- a/src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx +++ b/src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx @@ -8,7 +8,7 @@ import { useState, } from "react"; -interface QrCodeScanner { +export interface QrCodeScanner { values: string[]; isScanning: boolean; startScan: () => void; @@ -82,7 +82,7 @@ const QrCodeScannerProvider: React.FC = ({ keys.join("").substring(startBrace, endBrace + 1), ]); console.log(keys); - console.log(qrCodeScannerValues); + console.log("%c QR Scanner Values:", "color:cyan", qrCodeScannerValues); // reset setKeys(() => []); diff --git a/src/i18n/zh/purchaseOrder.json b/src/i18n/zh/purchaseOrder.json index 105a2ce..e12c370 100644 --- a/src/i18n/zh/purchaseOrder.json +++ b/src/i18n/zh/purchaseOrder.json @@ -120,7 +120,7 @@ "Escalation Result": "上報結果", "update qc info": "更新品檢資料", "email supplier": "電郵供應商", - "confirm putaway": "確定及上架", + "confirm putaway": "確定及完成上架", "confirm qc result": "確定品檢結果", "warehouse": "倉庫", "qcItem": "品檢項目", diff --git a/src/i18n/zh/putAway.json b/src/i18n/zh/putAway.json index e62a81b..369aee3 100644 --- a/src/i18n/zh/putAway.json +++ b/src/i18n/zh/putAway.json @@ -1,4 +1,17 @@ { - "Put Away": "上架", - "Put Away Scan": "上架掃碼" -} \ No newline at end of file + "Put Away": "上架", + "Put Away Scan": "上架掃碼", + "Pending scan": "等待掃瞄中,請掃瞄貨品二維碼開始上架程序", + "Rescan": "讀取不成功,請重新掃瞄", + "acceptedQty": "是次來貨數量", + "confirm putaway": "確定及上架貨物", + "scan warehouse": "掃瞄倉庫二維碼", + "Please scan warehouse qr code": "請掃瞄倉庫二維碼", + "scan loading": "載入中,請稍候…", + "warehouse": "倉庫", + "putQty": "上架數量", + "minimal value is 1": "最小為1", + "putQty must not greater than": "上架數量不得大於", + "value must be integer": "必須是整數", + "value must be a number": "必須是數字" +}