Ver a proveniência

Put away fix

production
CANCERYS\kw093 há 13 horas
ascendente
cometimento
cb0f899fc5
3 ficheiros alterados com 254 adições e 127 eliminações
  1. +137
    -86
      src/components/PutAwayScan/PutAwayModal.tsx
  2. +91
    -28
      src/components/PutAwayScan/PutAwayScan.tsx
  3. +26
    -13
      src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx

+ 137
- 86
src/components/PutAwayScan/PutAwayModal.tsx Ver ficheiro

@@ -15,7 +15,7 @@ import {
import { fetchItemForPutAway } from "@/app/api/stockIn/actions";
import { Result } from "@/app/api/settings/item"; // 只导入类型

import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import ReactQrCodeScanner, {
ScannerConfig,
} from "../ReactQrCodeScanner/ReactQrCodeScanner";
@@ -36,20 +36,24 @@ import LoadingComponent from "../General/LoadingComponent";
import StockInForm from "../StockIn/StockInForm";
import { arrayToDateString, INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { QrCodeScanner } from "../QrCodeScannerProvider/QrCodeScannerProvider";
import { msg } from "../Swal/CustomAlerts";
import { msg, msgError } from "../Swal/CustomAlerts";
import { PutAwayRecord } from ".";
import FgStockInForm from "../StockIn/FgStockInForm";

import Swal from "sweetalert2";


interface Props extends Omit<ModalProps, "children"> {
interface Props extends Omit<ModalProps, "children" | "onClose"> {
warehouse: WarehouseResult[];
stockInLineId: number;
warehouseId: number;
scanner: QrCodeScanner;
addPutAwayHistory: (putAwayData: PutAwayRecord) => void;
onSetDefaultWarehouseId?: (warehouseId: number) => void; // 新增回调
onSetDefaultWarehouseId?: (warehouseId: number) => void;
/** Parent toggles lot vs warehouse scan routing. */
onScanPhaseChange?: (acceptWarehouse: boolean) => void;
/** When false, parent keeps scanner stopped (e.g. until alert is dismissed). */
onClose: (resumeScan?: boolean) => void;
}
const style = {
position: "absolute",
@@ -80,7 +84,7 @@ const scannerStyle = {
maxWidth: "600px",
};

const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId, warehouseId, scanner, addPutAwayHistory, onSetDefaultWarehouseId }) => {
const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId, warehouseId, scanner, addPutAwayHistory, onSetDefaultWarehouseId, onScanPhaseChange }) => {
const { t } = useTranslation("putAway");
const [serverError, setServerError] = useState("");
const params = useSearchParams();
@@ -101,6 +105,40 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
const [putQty, setPutQty] = useState<number>(itemDetail?.acceptedQty ?? 0);
const [verified, setVerified] = useState<boolean>(false);
const [qtyError, setQtyError] = useState<string>("");
const fetchRequestIdRef = useRef(0);
const processedItemKeyRef = useRef<string | null>(null);
const onCloseRef = useRef(onClose);
const onScanPhaseChangeRef = useRef(onScanPhaseChange);
const scannerRef = useRef(scanner);
onCloseRef.current = onClose;
onScanPhaseChangeRef.current = onScanPhaseChange;
scannerRef.current = scanner;

const setWarehouseScanPhase = useCallback((accept: boolean) => {
onScanPhaseChangeRef.current?.(accept);
}, []);

const getRejectMessage = (status: string): string => {
switch (status) {
case "pending": return "此貨品有待品檢";
case "rejected": return "此貨品已被拒收";
case "escalated": return "此貨品已被上報";
case "partially_completed": return "此貨品已部分上架";
case "completed": return "此貨品已上架";
default: return "此貨品暫時無法上架";
}
};

const resetModalState = useCallback(() => {
fetchRequestIdRef.current += 1;
setVerified(false);
setItemDetail(undefined);
setTotalPutAwayQty(0);
setItemDefaultWarehouseId(null);
setFirstWarehouseId(null);
setFirstWarehouseInfo(null);
setWarehouseMismatchError("");
}, []);
const defaultNewValue = useMemo(() => {
// console.log("%c ItemDetail", "color:purple", itemDetail);
@@ -128,54 +166,48 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
...defaultNewValue,
},
});
const { isSubmitting } = formProps.formState;
const { reset: resetForm, formState: { isSubmitting } } = formProps;
const errors = formProps.formState.errors;

useEffect(() => {
if (itemDetail) {
startScanner();
if (itemDetail?.status === "received") {
scannerRef.current.startScan();
console.log("%c Scanning started ", "color:cyan");
}
}, [itemDetail])
}, [itemDetail?.id, itemDetail?.status]);

const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
(...args) => {
setVerified(false);
setItemDetail(undefined);
setTotalPutAwayQty(0);
setItemDefaultWarehouseId(null);
setFirstWarehouseId(null);
setFirstWarehouseInfo(null);
onClose?.(...args);
// reset();
setWarehouseScanPhase(false);
onCloseRef.current(true);
resetModalState();
},
[onClose],
[resetModalState, setWarehouseScanPhase],
);

const scannerCloseHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
(...args) => {
setIsOpenScanner(false);
scanner.stopScan();
scannerRef.current.stopScan();
console.log("%c Scanning stopped ", "color:cyan");
},
[],
);

const startScanner = () => {
// setIsOpenScanner(true);
scanner.startScan();
scannerRef.current.startScan();
console.log("%c Scanning started ", "color:cyan");
};

const openScanner = () => {
setIsOpenScanner(true);
scanner.startScan();
scannerRef.current.startScan();
console.log("%c Scanning started ", "color:cyan");
};

// 根据 item 的 locationCode 设置默认 warehouseId
// 根据 item 的 locationCode 设置默认 warehouseId(仅 received 状态)
useEffect(() => {
// 直接使用 fetchStockInLineInfo 返回的 locationCode
// 只在第一次上架时(firstWarehouseId === null)设置默认值
if (itemDetail?.status !== "received") return;
if (itemDetail?.locationCode && warehouse.length > 0 && firstWarehouseId === null) {
const locationCode = itemDetail.locationCode;
@@ -198,9 +230,11 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
}
}
}
}, [itemDetail?.locationCode, warehouse, firstWarehouseId]);
}, [itemDetail?.status, itemDetail?.locationCode, warehouse, firstWarehouseId]);

useEffect(() => {
if (itemDetail?.status !== "received") return;

// 只使用实际扫描的 warehouseId,不使用默认值进行验证
if (warehouseId > 0 && firstWarehouseId !== null) {
// 第二次及后续上架:必须使用第一次的仓库
@@ -215,7 +249,7 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
setIsOpenScanner(false);
setVerified(true);
msg("貨倉掃瞄成功!");
scanner.resetScan();
scannerRef.current.resetScan();
}
}
} else if (warehouseId > 0 && firstWarehouseId === null) {
@@ -224,10 +258,10 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
setIsOpenScanner(false);
setVerified(true);
msg("貨倉掃瞄成功!");
scanner.resetScan();
scannerRef.current.resetScan();
}
}
}, [warehouseId, firstWarehouseId, scanner.isScanning]);
}, [warehouseId, firstWarehouseId, scanner.isScanning, itemDetail?.status, warehouse]);

const warehouseDisplay = useMemo(() => {
// 优先使用扫描的 warehouseId,如果没有扫描则显示默认值作为建议
@@ -247,69 +281,86 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
// }, [isOpenScanner])

useLayoutEffect(() => {
if (itemDetail !== undefined) {
if (itemDetail.status == "received") {
formProps.reset({
...defaultNewValue
})
const total = itemDetail.putAwayLines?.reduce((sum, p) => sum + (p.stockQty ?? p.qty ?? 0), 0) ?? 0;
setPutQty(itemDetail?.acceptedQty - total);
// ✅ Get first warehouse from existing put away lines
const firstPutAwayLine = itemDetail.putAwayLines?.[0];
if (firstPutAwayLine?.warehouseId) {
setFirstWarehouseId(firstPutAwayLine.warehouseId);
// ✅ Store first warehouse info for display
const firstWh = warehouse.find((w) => w.id == firstPutAwayLine.warehouseId);
if (firstWh) {
setFirstWarehouseInfo({
name: firstWh.name || "",
code: firstWh.code || ""
});
}
} else {
setFirstWarehouseId(null);
setFirstWarehouseInfo(null);
if (itemDetail === undefined) {
processedItemKeyRef.current = null;
return;
}

const itemKey = `${itemDetail.id}:${itemDetail.status}`;
if (processedItemKeyRef.current === itemKey) {
return;
}
processedItemKeyRef.current = itemKey;

if (itemDetail.status == "received") {
resetForm({
...defaultNewValue,
});
const total = itemDetail.putAwayLines?.reduce((sum, p) => sum + (p.stockQty ?? p.qty ?? 0), 0) ?? 0;
setPutQty(itemDetail?.acceptedQty - total);

const firstPutAwayLine = itemDetail.putAwayLines?.[0];
if (firstPutAwayLine?.warehouseId) {
setFirstWarehouseId(firstPutAwayLine.warehouseId);
const firstWh = warehouse.find((w) => w.id == firstPutAwayLine.warehouseId);
if (firstWh) {
setFirstWarehouseInfo({
name: firstWh.name || "",
code: firstWh.code || "",
});
}
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");
setFirstWarehouseId(null);
setFirstWarehouseInfo(null);
}
}
}, [open, itemDetail, defaultNewValue])

const fetchStockInLine = useCallback(
async (stockInLineId: number) => {
setUnavailableText(undefined);
try {
const res = await fetchStockInLineInfo(stockInLineId);
console.log("%c Fetched Stock In Line Info:", "color:gold", res);

const total = res.putAwayLines?.reduce((sum, p) => sum + (p.stockQty ?? p.qty ?? 0), 0) ?? 0;
setTotalPutAwayQty(total);
setItemDetail(res);
} catch (e) {
console.log("%c Error when fetching Stock In Line: ", "color:red", e);
alert("錯誤的二維碼");
closeHandler({}, "backdropClick");
}
},
[formProps, itemDetail, fetchStockInLineInfo],
);
console.log("%c Loaded data:", "color:lime", defaultNewValue);
setWarehouseScanPhase(true);
} else {
processedItemKeyRef.current = null;
setWarehouseScanPhase(false);
resetModalState();
onCloseRef.current(true);
msgError(getRejectMessage(itemDetail.status));
}
}, [itemDetail, defaultNewValue, warehouse, resetModalState, resetForm, setWarehouseScanPhase]);

const fetchStockInLine = useCallback(async (stockInLineId: number) => {
const requestId = ++fetchRequestIdRef.current;
processedItemKeyRef.current = null;
setWarehouseScanPhase(false);
setUnavailableText(undefined);
setItemDetail(undefined);
try {
const res = await fetchStockInLineInfo(stockInLineId);
if (requestId !== fetchRequestIdRef.current) return;

console.log("%c Fetched Stock In Line Info:", "color:gold", res);

const total = res.putAwayLines?.reduce((sum, p) => sum + (p.stockQty ?? p.qty ?? 0), 0) ?? 0;
setTotalPutAwayQty(total);
setItemDetail(res);
} catch (e) {
if (requestId !== fetchRequestIdRef.current) return;

console.log("%c Error when fetching Stock In Line: ", "color:red", e);
setWarehouseScanPhase(false);
resetModalState();
onCloseRef.current(true);
msgError("錯誤的二維碼");
}
}, [resetModalState, setWarehouseScanPhase]);

useEffect(() => {
if (stockInLineId) { fetchStockInLine(stockInLineId); }
}, [stockInLineId]);
if (stockInLineId) {
fetchStockInLine(stockInLineId);
return;
}
fetchRequestIdRef.current += 1;
processedItemKeyRef.current = null;
setWarehouseScanPhase(false);
setItemDetail(undefined);
}, [stockInLineId, fetchStockInLine, setWarehouseScanPhase]);

const validateQty = useCallback((qty : number = putQty) => {
// if (isNaN(putQty) || putQty === undefined || putQty === null || typeof(putQty) != "number") {


+ 91
- 28
src/components/PutAwayScan/PutAwayScan.tsx Ver ficheiro

@@ -45,14 +45,21 @@ const PutAwayScan: React.FC<Props> = ({ warehouse }) => {
const [scannedWareHouseId, setScannedWareHouseId] = useState<number>(0); // TODO use QR code info
const [putAwayHistory, setPutAwayHistory] = useState<PutAwayRecord[]>([]);

const openPutAwayModalRef = useRef(false);
const scannedSilIdRef = useRef(0);
/** Only true after modal loaded a received item — gates warehouse vs lot routing. */
const acceptWarehouseScansRef = useRef(false);

// QR Code Scanner
const scanner = useQrCodeScannerContext();
const scannerRef = useRef(scanner);
scannerRef.current = scanner;

useEffect(() => {
if (!scanner.isScanning) {
scanner.startScan();
} else if (scanner.isScanning) {
scanner.stopScan();
}
scannerRef.current.startScan();
return () => {
scannerRef.current.stopScan();
};
}, []);

const resetScan = (error : string = "") => {
@@ -62,26 +69,49 @@ const PutAwayScan: React.FC<Props> = ({ warehouse }) => {
} else {
console.log("%c Scan reset", "color:red");
}
scanner.resetScan();
scannerRef.current.resetScan();
};

const openModal = () => {
console.log("Scanned successfully and open modal");
scanner.stopScan();
openPutAwayModalRef.current = true;
acceptWarehouseScansRef.current = false;
scannerRef.current.stopScan();
console.log("%c Scanning stopped ", "color:cyan");
setOpenPutAwayModal(true);
setScanDisplay("pending");
};

const applyLotScan = useCallback((lotId: number) => {
acceptWarehouseScansRef.current = false;
scannedSilIdRef.current = lotId;
setScannedWareHouseId(0);
setScannedSilId(lotId);
}, []);

const handleScanPhaseChange = useCallback((acceptWarehouse: boolean) => {
acceptWarehouseScansRef.current = acceptWarehouse;
if (!acceptWarehouse) {
setScannedWareHouseId(0);
}
}, []);
const closeModal = () => {
const closeModal = useCallback((resumeScan = true) => {
openPutAwayModalRef.current = false;
acceptWarehouseScansRef.current = false;
scannedSilIdRef.current = 0;
setScannedSilId(0);
setScannedWareHouseId(0);
setScanDisplay("pending");
console.log("Modal Closed");
scanner.startScan();
console.log("%c Scanning started ", "color:cyan");
setOpenPutAwayModal(false);
}
console.log("Modal Closed");
scannerRef.current.resetScan();
scannerRef.current.stopScan();
if (resumeScan) {
scannerRef.current.startScan();
console.log("%c Scanning started ", "color:cyan");
}
}, []);

const addPutAwayHistory = (putAwayData: PutAwayRecord) => {
console.log("%c Added new data to Putaway history: ", "color:orange", putAwayData);
@@ -92,6 +122,7 @@ const PutAwayScan: React.FC<Props> = ({ warehouse }) => {

// 处理默认 warehouseId 的回调
const handleSetDefaultWarehouseId = useCallback((warehouseId: number) => {
if (!acceptWarehouseScansRef.current) return;
if (scannedWareHouseId === 0) {
setScannedWareHouseId(warehouseId);
console.log("%c Set default warehouseId from item locationCode:", "color:green", warehouseId);
@@ -112,7 +143,11 @@ const PutAwayScan: React.FC<Props> = ({ warehouse }) => {
const id = parseInt(stockInLineIdQ, 10);
if (!Number.isFinite(id) || id <= 0) return;
deepLinkConsumedRef.current = true;
setScannedSilId((prev) => (prev === id ? prev : id));
setScannedSilId((prev) => {
if (prev === id) return prev;
scannedSilIdRef.current = id;
return id;
});

// Consume deep link once, then remove it from URL
const nextParams = new URLSearchParams(searchParams.toString());
@@ -121,24 +156,51 @@ const PutAwayScan: React.FC<Props> = ({ warehouse }) => {
router.replace(nextQuery ? `${pathname}?${nextQuery}` : pathname);
}, [stockInLineIdQ, pathname, router, searchParams]);

// Get Scanned Values
const parseLotId = useCallback((data: QrCodeInfo): number | null => {
const fromValue = Number(data.value);
if (Number.isFinite(fromValue) && fromValue > 0) return fromValue;
const fromField = Number(data.stockInLineId);
if (Number.isFinite(fromField) && fromField > 0) return fromField;
return null;
}, []);

const parseWarehouseId = useCallback((data: QrCodeInfo): number | null => {
const fromValue = Number(data.value);
if (Number.isFinite(fromValue) && fromValue > 0) return fromValue;
const fromField = Number(data.warehouseId);
if (Number.isFinite(fromField) && fromField > 0) return fromField;
return null;
}, []);

// Route scans: lot unless modal has a received item ready for warehouse scan.
useEffect(() => {
if (scanner.result) {
const data = scanner.result;
if (scannedSilId == 0) { // Initial State
if (!isNaN(Number(data.value))) { setScannedSilId(Number(data.value)); }
else if (data.stockInLineId) {
setScannedSilId(Number(data.stockInLineId));
} else resetScan("Cannot read Stock In Line Id");
} else { // Processing
if (!isNaN(Number(data.value))) { setScannedWareHouseId(Number(data.value)); }
else if (data.warehouseId) {
setScannedWareHouseId(Number(data.warehouseId));
} else resetScan("Cannot read Warehouse Id");
if (!scanner.result) return;

const data = scanner.result;
scannerRef.current.resetScan();

const modalOpen = openPutAwayModalRef.current;
const acceptWarehouse = acceptWarehouseScansRef.current;

// Lot scan when modal closed, or still loading / rejecting (not yet received).
if (!modalOpen || !acceptWarehouse) {
const lotId = parseLotId(data);
if (lotId != null) {
applyLotScan(lotId);
} else if (!modalOpen) {
resetScan("Cannot read Stock In Line Id");
}
scanner.resetScan();
// Warehouse-only scan during loading/reject: silently discard.
return;
}

const whId = parseWarehouseId(data);
if (whId != null) {
setScannedWareHouseId(whId);
} else {
resetScan("Cannot read Warehouse Id");
}
}, [scanner.result]);
}, [scanner.result, parseLotId, parseWarehouseId, applyLotScan]);

// Get Scanner State
useEffect(() => {
@@ -196,6 +258,7 @@ const PutAwayScan: React.FC<Props> = ({ warehouse }) => {
scanner={scanner}
addPutAwayHistory={addPutAwayHistory}
onSetDefaultWarehouseId={handleSetDefaultWarehouseId}
onScanPhaseChange={handleScanPhaseChange}
/>
</>)
}


+ 26
- 13
src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx Ver ficheiro

@@ -1,12 +1,13 @@
"use client";
import { QrCodeInfo } from "@/app/api/qrcode";
import { useRef } from "react";
import {
ReactNode,
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
startTransition,
} from "react";
@@ -352,19 +353,31 @@ useEffect(() => {
}
}, [qrCodeScannerValues]);

const contextValue = useMemo(
() => ({
values: qrCodeScannerValues,
isScanning: isScanning,
startScan: startQrCodeScanner,
stopScan: endQrCodeScanner,
resetScan: resetQrCodeScanner,
result: scanResult,
state: scanState,
error: scanError,
}),
[
qrCodeScannerValues,
isScanning,
startQrCodeScanner,
endQrCodeScanner,
resetQrCodeScanner,
scanResult,
scanState,
scanError,
],
);

return (
<QrCodeScannerContext.Provider
value={{
values: qrCodeScannerValues,
isScanning: isScanning,
startScan: startQrCodeScanner,
stopScan: endQrCodeScanner,
resetScan: resetQrCodeScanner,
result: scanResult,
state: scanState,
error: scanError,
}}
>
<QrCodeScannerContext.Provider value={contextValue}>
{children}
</QrCodeScannerContext.Provider>
);


Carregando…
Cancelar
Guardar