|
- "use client";
-
- import {
- Box,
- Button,
- Stack,
- TextField,
- Typography,
- Alert,
- CircularProgress,
- Autocomplete,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
- Checkbox,
- TablePagination,
- Modal,
- } from "@mui/material";
- import TestQrCodeProvider from "../QrCodeScannerProvider/TestQrCodeProvider";
- import {
- useCallback,
- useEffect,
- useState,
- useRef,
- useMemo,
- startTransition,
- } from "react";
- import { useTranslation } from "react-i18next";
- import { useRouter } from "next/navigation";
- import {
- updateStockOutLineStatus,
- createStockOutLine,
- //applyPickExecutionHoldAndChecked,
- fetchFGPickOrdersByUserIdWorkbench,
- FGPickOrderResponse,
- autoAssignAndReleasePickOrder,
- AutoAssignReleaseResponse,
- checkPickOrderCompletion,
- PickOrderCompletionResponse,
- confirmLotSubstitution,
- updateStockOutLineStatusByQRCodeAndLotNo, // ✅ 添加
- } from "@/app/api/pickOrder/actions";
- // 修改:使用 Job Order API
- import {
- fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench,
- updateJoPickOrderHandledBy,
- JobOrderLotsHierarchicalWorkbenchResponse,
- applyPickExecutionHoldAndChecked,
- PrintPickRecord,
- } from "@/app/api/jo/actions";
- import { assignJobOrderPickOrderForWorkbench } from "@/app/api/jo/workbenchActions";
- import { fetchNameList, NameList } from "@/app/api/user/actions";
- import { FormProvider, useForm } from "react-hook-form";
- import SearchBox, { Criterion } from "../SearchBox";
- import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
- import {
- updateInventoryLotLineQuantities,
- analyzeQrCode,
- fetchLotDetail,
- } from "@/app/api/inventory/actions";
- import QrCodeIcon from "@mui/icons-material/QrCode";
- import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
- import { useSession } from "next-auth/react";
- import { SessionWithTokens } from "@/config/authConfig";
- import { fetchStockInLineInfo } from "@/app/api/po/actions";
- import GoodPickExecutionForm from "../Jodetail/JobPickExecutionForm";
- import FGPickOrderCard from "../Jodetail/FGPickOrderCard";
- import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal";
- import LinearProgressWithLabel from "../common/LinearProgressWithLabel";
- import ScanStatusAlert from "../common/ScanStatusAlert";
- import {
- workbenchBatchScanPick,
- workbenchScanPick,
- } from "@/app/api/doworkbench/actions";
- import type { PrinterCombo } from "@/app/api/settings/printer";
- import { msg, msgError } from "@/components/Swal/CustomAlerts";
- import {
- buildPrintPickRecordRequest,
- promptAllFloorsPlasticBoxCartonQty,
- promptPlasticBoxCartonQty,
- type PickRecordFloor,
- } from "@/components/Jodetail/pickRecordHelpers";
- interface Props {
- filterArgs: Record<string, any>;
- //onSwitchToRecordTab: () => void;
- onBackToList?: () => void;
- printerCombo?: PrinterCombo[];
- }
-
- /** 過期批號:與 noLot 類似——單筆/批量預設 0,除非 Issue 改數(對齊 GoodPickExecutiondetail) */
- function isLotAvailabilityExpired(lot: any): boolean {
- return String(lot?.lotAvailability || "").toLowerCase() === "expired";
- }
-
- /** inventory_lot_line.status = unavailable(API 可能用 lotAvailability 或 lotStatus) */
- function isInventoryLotLineUnavailable(lot: any): boolean {
- if (!lot) return false;
- if (lot.lotAvailability === "status_unavailable") return true;
- return String(lot.lotStatus || "").toLowerCase() === "unavailable";
- }
-
- /** 同 DO Workbench:多行時優先替換有建議批號的行 */
- function pickExpectedLotForSubstitution(
- activeSuggestedLots: any[],
- ): any | null {
- if (!activeSuggestedLots?.length) return null;
- const withLotNo = activeSuggestedLots.filter(
- (l) => l.lotNo != null && String(l.lotNo).trim() !== "",
- );
- if (withLotNo.length === 1) return withLotNo[0];
- if (withLotNo.length > 1) {
- const pending = withLotNo.find(
- (l) => String(l.stockOutLineStatus || "").toLowerCase() === "pending",
- );
- return pending || withLotNo[0];
- }
- return activeSuggestedLots[0];
- }
-
- type PickOrderT = (key: string, options?: Record<string, unknown>) => string;
-
- /** 與 DO Workbench:優先顯示 scan-pick 暫存拒絕訊息 */
- function buildLotRejectDisplayMessage(
- lot: any,
- scanRejectBySolId: Record<number, string>,
- t: PickOrderT,
- ): string | undefined {
- const solId = Number(lot.stockOutLineId) || 0;
- const fromScan = solId > 0 ? scanRejectBySolId[solId]?.trim() : "";
- if (fromScan) return fromScan;
-
- const fromApi =
- typeof lot.stockOutLineRejectMessage === "string"
- ? lot.stockOutLineRejectMessage.trim()
- : "";
- if (fromApi) return fromApi;
-
- const st = String(lot.stockOutLineStatus || "").toLowerCase();
- const av = String(lot.lotAvailability || "").toLowerCase();
- const isRejected = st === "rejected" || av === "rejected";
- if (!isRejected) return undefined;
-
- if (isLotAvailabilityExpired(lot) || av === "expired") {
- return t("Rejected: lot expired or no longer valid.");
- }
- if (av === "insufficient_stock") {
- return t("Rejected: no remaining quantity / empty lot.");
- }
- if (isInventoryLotLineUnavailable(lot) || av === "status_unavailable") {
- return t("Rejected: lot unavailable or not yet putaway.");
- }
- return t("Pick was rejected. Please scan another lot or check stock.");
- }
-
- const JO_ISSUE_PICKED_KEY = (pickOrderId: number) =>
- `fpsms-jo-issuePickedQty:${pickOrderId}`;
-
- function loadIssuePickedMapJo(pickOrderId: number): Record<number, number> {
- if (typeof window === "undefined" || !pickOrderId) return {};
- try {
- const raw = sessionStorage.getItem(JO_ISSUE_PICKED_KEY(pickOrderId));
- if (!raw) return {};
- const parsed = JSON.parse(raw) as Record<string, number>;
- const out: Record<number, number> = {};
- Object.entries(parsed).forEach(([k, v]) => {
- const n = Number(v);
- if (!Number.isNaN(n)) out[Number(k)] = n;
- });
- return out;
- } catch {
- return {};
- }
- }
-
- function saveIssuePickedMapJo(
- pickOrderId: number,
- map: Record<number, number>,
- ) {
- if (typeof window === "undefined" || !pickOrderId) return;
- try {
- sessionStorage.setItem(
- JO_ISSUE_PICKED_KEY(pickOrderId),
- JSON.stringify(map),
- );
- } catch {
- // ignore quota / private mode
- }
- }
-
- // Manual Lot Confirmation Modal (align with GoodPickExecutiondetail, opened by {2fic})
- const ManualLotConfirmationModal: React.FC<{
- open: boolean;
- onClose: () => void;
- onConfirm: (expectedLotNo: string, scannedLotNo: string) => void;
- expectedLot: { lotNo: string; itemCode: string; itemName: string } | null;
- scannedLot: { lotNo: string; itemCode: string; itemName: string } | null;
- isLoading?: boolean;
- }> = ({
- open,
- onClose,
- onConfirm,
- expectedLot,
- scannedLot,
- isLoading = false,
- }) => {
- const { t } = useTranslation("jo");
- const [expectedLotInput, setExpectedLotInput] = useState<string>("");
- const [scannedLotInput, setScannedLotInput] = useState<string>("");
- const [error, setError] = useState<string>("");
-
- useEffect(() => {
- if (open) {
- setExpectedLotInput(expectedLot?.lotNo || "");
- setScannedLotInput(scannedLot?.lotNo || "");
- setError("");
- }
- }, [open, expectedLot, scannedLot]);
-
- const handleConfirm = () => {
- if (!expectedLotInput.trim() || !scannedLotInput.trim()) {
- setError(t("Please enter both expected and scanned lot numbers."));
- return;
- }
- if (expectedLotInput.trim() === scannedLotInput.trim()) {
- setError(t("Expected and scanned lot numbers cannot be the same."));
- return;
- }
- onConfirm(expectedLotInput.trim(), scannedLotInput.trim());
- };
-
- return (
- <Modal open={open} onClose={onClose}>
- <Box
- sx={{
- position: "absolute",
- top: "50%",
- left: "50%",
- transform: "translate(-50%, -50%)",
- bgcolor: "background.paper",
- p: 3,
- borderRadius: 2,
- minWidth: 500,
- }}
- >
- <Typography variant="h6" gutterBottom color="warning.main">
- {t("Manual Lot Confirmation")}
- </Typography>
-
- <Box sx={{ mb: 2 }}>
- <Typography variant="body2" gutterBottom>
- <strong>{t("Expected Lot Number")}:</strong>
- </Typography>
- <TextField
- fullWidth
- size="small"
- value={expectedLotInput}
- onChange={(e) => {
- setExpectedLotInput(e.target.value);
- setError("");
- }}
- sx={{ mb: 2 }}
- error={!!error && !expectedLotInput.trim()}
- />
- </Box>
-
- <Box sx={{ mb: 2 }}>
- <Typography variant="body2" gutterBottom>
- <strong>{t("Scanned Lot Number")}:</strong>
- </Typography>
- <TextField
- fullWidth
- size="small"
- value={scannedLotInput}
- onChange={(e) => {
- setScannedLotInput(e.target.value);
- setError("");
- }}
- sx={{ mb: 2 }}
- error={!!error && !scannedLotInput.trim()}
- />
- </Box>
-
- {error && (
- <Box
- sx={{ mb: 2, p: 1, backgroundColor: "#ffebee", borderRadius: 1 }}
- >
- <Typography variant="body2" color="error">
- {error}
- </Typography>
- </Box>
- )}
-
- <Box
- sx={{ mt: 2, display: "flex", justifyContent: "flex-end", gap: 2 }}
- >
- <Button onClick={onClose} variant="outlined" disabled={isLoading}>
- {t("Cancel")}
- </Button>
- <Button
- onClick={handleConfirm}
- variant="contained"
- color="warning"
- disabled={
- isLoading || !expectedLotInput.trim() || !scannedLotInput.trim()
- }
- >
- {isLoading ? t("Processing...") : t("Confirm")}
- </Button>
- </Box>
- </Box>
- </Modal>
- );
- };
-
- // QR Code Modal Component (from GoodPickExecution)
- const QrCodeModal: React.FC<{
- open: boolean;
- onClose: () => void;
- lot: any | null;
- onQrCodeSubmit: (lotNo: string) => void;
- combinedLotData: any[];
- }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
- const { t } = useTranslation("jo");
- const {
- values: qrValues,
- isScanning,
- startScan,
- stopScan,
- resetScan,
- } = useQrCodeScannerContext();
- const [manualInput, setManualInput] = useState<string>("");
-
- const [selectedFloor, setSelectedFloor] = useState<string | null>(null);
- const [manualInputSubmitted, setManualInputSubmitted] =
- useState<boolean>(false);
- const [manualInputError, setManualInputError] = useState<boolean>(false);
- const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
- const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
- const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
-
- const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(
- new Set(),
- );
- const [scannedQrResult, setScannedQrResult] = useState<string>("");
-
- // Process scanned QR codes
- useEffect(() => {
- if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
- const latestQr = qrValues[qrValues.length - 1];
-
- if (processedQrCodes.has(latestQr)) {
- console.log("QR code already processed, skipping...");
- return;
- }
-
- setProcessedQrCodes((prev) => new Set(prev).add(latestQr));
-
- try {
- const qrData = JSON.parse(latestQr);
-
- if (qrData.stockInLineId && qrData.itemId) {
- setIsProcessingQr(true);
- setQrScanFailed(false);
-
- fetchStockInLineInfo(qrData.stockInLineId)
- .then((stockInLineInfo) => {
- console.log("Stock in line info:", stockInLineInfo);
- setScannedQrResult(stockInLineInfo.lotNo || "Unknown lot number");
-
- if (stockInLineInfo.lotNo === lot.lotNo) {
- console.log(` QR Code verified for lot: ${lot.lotNo}`);
- setQrScanSuccess(true);
- onQrCodeSubmit(lot.lotNo);
- onClose();
- resetScan();
- } else {
- console.log(
- `❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`,
- );
- setQrScanFailed(true);
- setManualInputError(true);
- setManualInputSubmitted(true);
- }
- })
- .catch((error) => {
- console.error("Error fetching stock in line info:", error);
- setScannedQrResult("Error fetching data");
- setQrScanFailed(true);
- setManualInputError(true);
- setManualInputSubmitted(true);
- })
- .finally(() => {
- setIsProcessingQr(false);
- });
- } else {
- const qrContent = latestQr.replace(/[{}]/g, "");
- setScannedQrResult(qrContent);
-
- if (qrContent === lot.lotNo) {
- setQrScanSuccess(true);
- onQrCodeSubmit(lot.lotNo);
- onClose();
- resetScan();
- } else {
- setQrScanFailed(true);
- setManualInputError(true);
- setManualInputSubmitted(true);
- }
- }
- } catch (error) {
- console.log("QR code is not JSON format, trying direct comparison");
- const qrContent = latestQr.replace(/[{}]/g, "");
- setScannedQrResult(qrContent);
-
- if (qrContent === lot.lotNo) {
- setQrScanSuccess(true);
- onQrCodeSubmit(lot.lotNo);
- onClose();
- resetScan();
- } else {
- setQrScanFailed(true);
- setManualInputError(true);
- setManualInputSubmitted(true);
- }
- }
- }
- }, [
- qrValues,
- lot,
- onQrCodeSubmit,
- onClose,
- resetScan,
- isProcessingQr,
- qrScanSuccess,
- processedQrCodes,
- ]);
-
- // Clear states when modal opens
- useEffect(() => {
- if (open) {
- setManualInput("");
- setManualInputSubmitted(false);
- setManualInputError(false);
- setIsProcessingQr(false);
- setQrScanFailed(false);
- setQrScanSuccess(false);
- setScannedQrResult("");
- setProcessedQrCodes(new Set());
- }
- }, [open]);
-
- useEffect(() => {
- if (lot) {
- setManualInput("");
- setManualInputSubmitted(false);
- setManualInputError(false);
- setIsProcessingQr(false);
- setQrScanFailed(false);
- setQrScanSuccess(false);
- setScannedQrResult("");
- setProcessedQrCodes(new Set());
- }
- }, [lot]);
-
- // Auto-submit manual input when it matches
- useEffect(() => {
- if (
- manualInput.trim() === lot?.lotNo &&
- manualInput.trim() !== "" &&
- !qrScanFailed &&
- !qrScanSuccess
- ) {
- console.log(" Auto-submitting manual input:", manualInput.trim());
-
- const timer = setTimeout(() => {
- setQrScanSuccess(true);
- onQrCodeSubmit(lot.lotNo);
- onClose();
- setManualInput("");
- setManualInputError(false);
- setManualInputSubmitted(false);
- }, 200);
-
- return () => clearTimeout(timer);
- }
- }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
-
- const handleManualSubmit = () => {
- if (manualInput.trim() === lot?.lotNo) {
- setQrScanSuccess(true);
- onQrCodeSubmit(lot.lotNo);
- onClose();
- setManualInput("");
- } else {
- setQrScanFailed(true);
- setManualInputError(true);
- setManualInputSubmitted(true);
- }
- };
-
- useEffect(() => {
- if (open) {
- startScan();
- }
- }, [open, startScan]);
-
- return (
- <Modal open={open} onClose={onClose}>
- <Box
- sx={{
- position: "absolute",
- top: "50%",
- left: "50%",
- transform: "translate(-50%, -50%)",
- bgcolor: "background.paper",
- p: 3,
- borderRadius: 2,
- minWidth: 400,
- }}
- >
- <Typography variant="h6" gutterBottom>
- {t("QR Code Scan for Lot")}: {lot?.lotNo}
- </Typography>
-
- {isProcessingQr && (
- <Box
- sx={{ mb: 2, p: 2, backgroundColor: "#e3f2fd", borderRadius: 1 }}
- >
- <Typography variant="body2" color="primary">
- {t("Processing QR code...")}
- </Typography>
- </Box>
- )}
-
- <Box sx={{ mb: 2 }}>
- <Typography variant="body2" gutterBottom>
- <strong>{t("Manual Input")}:</strong>
- </Typography>
- <TextField
- fullWidth
- size="small"
- value={manualInput}
- onChange={(e) => {
- setManualInput(e.target.value);
- if (qrScanFailed || manualInputError) {
- setQrScanFailed(false);
- setManualInputError(false);
- setManualInputSubmitted(false);
- }
- }}
- sx={{ mb: 1 }}
- error={manualInputSubmitted && manualInputError}
- helperText={
- manualInputSubmitted && manualInputError
- ? `${t(
- "The input is not the same as the expected lot number.",
- )}`
- : ""
- }
- />
-
- <Button
- variant="contained"
- onClick={handleManualSubmit}
- disabled={!manualInput.trim() || lot?.noLot === true || !lot?.lotId}
- size="small"
- color="primary"
- >
- {t("Submit")}
- </Button>
- </Box>
-
- {qrValues.length > 0 && (
- <Box
- sx={{
- mb: 2,
- p: 2,
- backgroundColor: qrScanFailed
- ? "#ffebee"
- : qrScanSuccess
- ? "#e8f5e8"
- : "#f5f5f5",
- borderRadius: 1,
- }}
- >
- <Typography
- variant="body2"
- color={
- qrScanFailed
- ? "error"
- : qrScanSuccess
- ? "success"
- : "text.secondary"
- }
- >
- <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
- </Typography>
-
- {qrScanSuccess && (
- <Typography variant="caption" color="success" display="block">
- {t("Verified successfully!")}
- </Typography>
- )}
- </Box>
- )}
-
- <Box sx={{ mt: 2, textAlign: "right" }}>
- <Button onClick={onClose} variant="outlined">
- {t("Cancel")}
- </Button>
- </Box>
- </Box>
- </Modal>
- );
- };
-
- const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCombo = [] }) => {
- const workbenchMode = true;
- const { t } = useTranslation("jo");
- const { t: tPick } = useTranslation("pickOrder");
- const router = useRouter();
- const { data: session } = useSession() as { data: SessionWithTokens | null };
-
- const currentUserId = session?.id ? parseInt(session.id) : undefined;
-
- // 修改:使用 Job Order 数据结构
-
- const [combinedDataLoading, setCombinedDataLoading] = useState(false);
-
- // 添加未分配订单状态
- const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
- const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
-
- const {
- values: qrValues,
- isScanning,
- startScan,
- stopScan,
- resetScan,
- } = useQrCodeScannerContext();
- const [expectedLotData, setExpectedLotData] = useState<any>(null);
- const [scannedLotData, setScannedLotData] = useState<any>(null);
- const [isConfirmingLot, setIsConfirmingLot] = useState(false);
- const [qrScanInput, setQrScanInput] = useState<string>("");
- const [qrScanError, setQrScanError] = useState<boolean>(false);
- const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>("");
- const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
- const [jobOrderData, setJobOrderData] =
- useState<JobOrderLotsHierarchicalWorkbenchResponse | null>(null);
- const a4Printers = useMemo(
- () =>
- (printerCombo || []).filter((p) =>
- String(p.type || "")
- .trim()
- .toUpperCase()
- .includes("A4"),
- ),
- [printerCombo],
- );
- const printerOptions = useMemo(
- () => (a4Printers.length > 0 ? a4Printers : printerCombo || []),
- [a4Printers, printerCombo],
- );
- const isPrinterComboMissing = printerCombo.length === 0;
- const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>(
- printerOptions.length > 0 ? printerOptions[0] : null,
- );
- const [printQty, setPrintQty] = useState<number>(1);
- const pickRecordPrintInFlightRef = useRef(false);
-
- useEffect(() => {
- // Keep selected printer valid when combo list changes.
- if (!printerOptions.length) {
- setSelectedPrinter(null);
- return;
- }
- setSelectedPrinter((prev) => {
- if (!prev) return printerOptions[0];
- const stillExists = printerOptions.some((p) => p.id === prev.id);
- return stillExists ? prev : printerOptions[0];
- });
- }, [printerOptions]);
-
- useEffect(() => {
- console.log("[JO Workbench] printerCombo:", printerCombo);
- console.log("[JO Workbench] a4Printers:", a4Printers);
- console.log("[JO Workbench] printerOptions:", printerOptions);
- }, [printerCombo, a4Printers, printerOptions]);
-
- const handlePickRecord = useCallback(
- async (floor: PickRecordFloor) => {
- if (pickRecordPrintInFlightRef.current) return;
- try {
- const pickOrderId = jobOrderData?.pickOrder?.id;
- if (!pickOrderId) {
- msgError(t("Pick Order not found"));
- return;
- }
- if (!selectedPrinter) {
- msgError(t("Please select a printer first"));
- return;
- }
- if (!printQty || printQty < 1) {
- msgError(t("Please enter a valid print quantity (at least 1)"));
- return;
- }
-
- let printRequest;
- if (floor === "ALL") {
- const allFloorsQty = await promptAllFloorsPlasticBoxCartonQty(t, pickOrderId);
- if (allFloorsQty === null) {
- return;
- }
- printRequest = buildPrintPickRecordRequest({
- pickOrderId,
- printerId: selectedPrinter.id,
- printQty,
- floor,
- allFloorsQty,
- });
- } else {
- const plasticBoxCartonQty = await promptPlasticBoxCartonQty(t);
- if (plasticBoxCartonQty === null) {
- return;
- }
- printRequest = buildPrintPickRecordRequest({
- pickOrderId,
- printerId: selectedPrinter.id,
- printQty,
- floor,
- plasticBoxCartonQty,
- });
- }
-
- pickRecordPrintInFlightRef.current = true;
-
- const response = await PrintPickRecord(printRequest);
-
- if (response?.success) {
- msg(t("Printed Successfully."));
- } else {
- msgError(response?.message || t("Print failed"));
- }
- } catch (e) {
- console.error(e);
- msgError(t("An error occurred while printing"));
- } finally {
- pickRecordPrintInFlightRef.current = false;
- }
- },
- [jobOrderData, printQty, selectedPrinter, t],
- );
- const workbenchStoreId = useMemo(() => {
- const po = jobOrderData?.pickOrder as
- | { storeId?: string | null }
- | undefined;
- const s = po?.storeId;
- return typeof s === "string" && s.trim() !== "" ? s.trim() : null;
- }, [jobOrderData]);
- const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
- /** 與 DO Workbench 一致:false = 數量只讀;Edit 切換為可輸入「將提交數量」,不開 Issue 表單 */
- const [
- workbenchSubmitQtyFieldEnabledByLotKey,
- setWorkbenchSubmitQtyFieldEnabledByLotKey,
- ] = useState<Record<string, boolean>>({});
- const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
- // issue form 里填的 actualPickQty(用于 submit/batch submit 不补拣到 required)
- const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState<
- Record<number, number>
- >({});
- const [localSolStatusById, setLocalSolStatusById] = useState<
- Record<number, string>
- >({});
- // 防止同一行(以 stockOutLineId/solId 识别)被重复点击提交/完成
- const [actionBusyBySolId, setActionBusyBySolId] = useState<
- Record<number, boolean>
- >({});
- /** DO Workbench:scan-pick 失敗訊息按 SOL 顯示 */
- const [scanRejectMessageBySolId, setScanRejectMessageBySolId] = useState<
- Record<number, string>
- >({});
- const rememberWorkbenchScanReject = useCallback(
- (stockOutLineId: number, message: string | undefined | null) => {
- const id = Number(stockOutLineId);
- const m = String(message ?? "").trim();
- if (!id || !m) return;
- setScanRejectMessageBySolId((prev) => ({ ...prev, [id]: m }));
- },
- [],
- );
- const clearWorkbenchScanReject = useCallback((stockOutLineId: number) => {
- const id = Number(stockOutLineId);
- if (!id) return;
- setScanRejectMessageBySolId((prev) => {
- if (!(id in prev)) return prev;
- const next = { ...prev };
- delete next[id];
- return next;
- });
- }, []);
-
- const [paginationController, setPaginationController] = useState({
- pageNum: 0,
- pageSize: 10,
- });
-
- const [usernameList, setUsernameList] = useState<NameList[]>([]);
-
- const initializationRef = useRef(false);
- const scannerInitializedRef = useRef(false);
- const autoAssignRef = useRef(false);
-
- const formProps = useForm();
- const errors = formProps.formState.errors;
- const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
- const [autoAssignStatus, setAutoAssignStatus] = useState<
- "idle" | "checking" | "assigned" | "no_orders"
- >("idle");
- const [completionStatus, setCompletionStatus] =
- useState<PickOrderCompletionResponse | null>(null);
- const [autoAssignMessage, setAutoAssignMessage] = useState<string>("");
-
- // Add QR modal states
- const [qrModalOpen, setQrModalOpen] = useState(false);
- const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
- const [selectedFloor, setSelectedFloor] = useState<string | null>(null);
- // Add GoodPickExecutionForm states
- const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
- const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] =
- useState<any | null>(null);
- const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
- const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
- const [workbenchLotLabelModalOpen, setWorkbenchLotLabelModalOpen] =
- useState(false);
- const [workbenchLotLabelInitialPayload, setWorkbenchLotLabelInitialPayload] =
- useState<{ itemId: number; stockInLineId: number } | null>(null);
- const [workbenchLotLabelContextLot, setWorkbenchLotLabelContextLot] =
- useState<any | null>(null);
- const [workbenchLotLabelReminderText, setWorkbenchLotLabelReminderText] =
- useState<string | null>(null);
-
- useEffect(() => {
- if (!qrScanSuccess || !workbenchLotLabelModalOpen) return;
- setWorkbenchLotLabelModalOpen(false);
- setWorkbenchLotLabelInitialPayload(null);
- setWorkbenchLotLabelContextLot(null);
- setWorkbenchLotLabelReminderText(null);
- }, [qrScanSuccess, workbenchLotLabelModalOpen]);
-
- // Add these missing state variables
- const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
- const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(
- new Set(),
- );
- const [lastProcessedQr, setLastProcessedQr] = useState<string>("");
- const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
- // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling
- const [processedQrCombinations, setProcessedQrCombinations] = useState<
- Map<number, Set<number>>
- >(new Map());
-
- // Cache for fetchStockInLineInfo API calls to avoid redundant requests
- const stockInLineInfoCache = useRef<
- Map<number, { lotNo: string | null; timestamp: number }>
- >(new Map());
- const CACHE_TTL = 60000; // 60 seconds cache TTL
- const abortControllerRef = useRef<AbortController | null>(null);
- const qrProcessingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
-
- // Use refs for processed QR tracking to avoid useEffect dependency issues and delays
- const processedQrCodesRef = useRef<Set<string>>(new Set());
- const lastProcessedQrRef = useRef<string>("");
-
- // Store callbacks in refs to avoid useEffect dependency issues
- const processOutsideQrCodeRef = useRef<
- ((latestQr: string, qrScanCountAtInvoke?: number) => Promise<void>) | null
- >(null);
- const resetScanRef = useRef<(() => void) | null>(null);
-
- // Manual lot confirmation modal state (test shortcut {2fic})
- const [manualLotConfirmationOpen, setManualLotConfirmationOpen] =
- useState(false);
- const getAllLotsFromHierarchical = useCallback(
- (data: JobOrderLotsHierarchicalWorkbenchResponse | null): any[] => {
- if (!data || !data.pickOrder || !data.pickOrderLines) {
- return [];
- }
-
- const allLots: any[] = [];
-
- data.pickOrderLines.forEach((line) => {
- // 用来记录这一行已经通过 lots 出现过的 lotId(避免 stockouts 再渲染一次)
- const lotIdSet = new Set<number>();
- /** 已由有批次建議分配的量(加總後與 pick_order_line.requiredQty 的差額 = 無批次列應顯示的數),對齊 DO Workbench */
- let lotsAllocatedSumForLine = 0;
-
- // lots:按 lotId 去重并合并 requiredQty(对齐 GoodPickExecutiondetail)
- if (line.lots && line.lots.length > 0) {
- const lotMap = new Map<number, any>();
-
- line.lots.forEach((lot: any) => {
- const lotId = lot.lotId;
- if (lotId == null) return;
- if (lotMap.has(lotId)) {
- const existingLot = lotMap.get(lotId);
- existingLot.requiredQty =
- (existingLot.requiredQty || 0) + (lot.requiredQty || 0);
- } else {
- lotMap.set(lotId, { ...lot });
- }
- });
-
- lotMap.forEach((lot: any) => {
- lotsAllocatedSumForLine += Number(lot.requiredQty) || 0;
- if (lot.lotId != null) lotIdSet.add(lot.lotId);
-
- allLots.push({
- ...lot,
- pickOrderLineId: line.id,
- itemId: line.itemId,
- itemCode: line.itemCode,
- itemName: line.itemName,
- uomCode: line.uomCode,
- uomDesc: line.uomDesc,
- itemTotalAvailableQty: line.totalAvailableQty ?? null,
- pickOrderLineRequiredQty: line.requiredQty,
- pickOrderLineStatus: line.status,
- jobOrderId: data.pickOrder.jobOrder.id,
- jobOrderCode: data.pickOrder.jobOrder.code,
- pickOrderId: data.pickOrder.id,
- pickOrderCode: data.pickOrder.code,
- pickOrderConsoCode: data.pickOrder.consoCode,
- pickOrderTargetDate: data.pickOrder.targetDate,
- pickOrderType: data.pickOrder.type,
- pickOrderStatus: data.pickOrder.status,
- pickOrderAssignTo: data.pickOrder.assignTo,
- handler: line.handler,
- noLot: false,
- });
- });
- }
-
- /** 工單 API 常在有揀貨後仍回傳 lots: [],缺口只在 stockouts;此時用非 noLot 的已揀量扣 POL(對齊實際剩餘) */
- const stockoutsPickedSumNonNoLot = (line.stockouts ?? []).reduce(
- (acc: number, s: any) => {
- if (!s || s.noLot) return acc;
- return acc + (Number(s.qty) || 0);
- },
- 0,
- );
- const noLotRemainingBasis =
- lotsAllocatedSumForLine > 0
- ? lotsAllocatedSumForLine
- : stockoutsPickedSumNonNoLot;
-
- // stockouts:用于“无 suggested lot / noLot”场景也显示并可 submit 0 闭环
- if (line.stockouts && line.stockouts.length > 0) {
- line.stockouts.forEach((stockout: any) => {
- const hasLot = stockout.lotId != null;
- const lotAlreadyInLots =
- hasLot && lotIdSet.has(stockout.lotId as number);
-
- // 有批次 & 已经通过 lots 渲染过 → 跳过,避免一条变两行
- if (!stockout.noLot && lotAlreadyInLots) {
- return;
- }
-
- allLots.push({
- pickOrderLineId: line.id,
- itemId: line.itemId,
- itemCode: line.itemCode,
- itemName: line.itemName,
- uomCode: line.uomCode,
- uomDesc: line.uomDesc,
- itemTotalAvailableQty: line.totalAvailableQty ?? null,
- pickOrderLineRequiredQty: line.requiredQty,
- pickOrderLineStatus: line.status,
- jobOrderId: data.pickOrder.jobOrder.id,
- jobOrderCode: data.pickOrder.jobOrder.code,
- pickOrderId: data.pickOrder.id,
- pickOrderCode: data.pickOrder.code,
- pickOrderConsoCode: data.pickOrder.consoCode,
- pickOrderTargetDate: data.pickOrder.targetDate,
- pickOrderType: data.pickOrder.type,
- pickOrderStatus: data.pickOrder.status,
- pickOrderAssignTo: data.pickOrder.assignTo,
- handler: line.handler,
-
- lotId: stockout.lotId || null,
- lotNo: stockout.lotNo || null,
- expiryDate: null,
- location: stockout.location || null,
- availableQty: stockout.availableQty ?? 0,
- // 無批次列:有 SPL 時扣 suggested 合計;僅有 stockouts(lots 空)時扣已揀量(對齊 DO + workbench 僅 SOL 情境)
- requiredQty: stockout.noLot
- ? Math.max(
- 0,
- (Number(line.requiredQty) || 0) - noLotRemainingBasis,
- )
- : Number(line.requiredQty) || 0,
- actualPickQty: stockout.qty ?? 0,
- processingStatus: stockout.status || "pending",
- lotAvailability: stockout.noLot
- ? "insufficient_stock"
- : "available",
- suggestedPickLotId: null,
- stockOutLineId: stockout.id || null,
- stockOutLineQty: stockout.qty ?? 0,
- stockOutLineStatus: stockout.status || null,
- stockInLineId: null,
- routerIndex: stockout.noLot ? 999999 : null,
- routerArea: null,
- routerRoute: null,
- noLot: !!stockout.noLot,
- });
- });
- }
- });
-
- return allLots;
- },
- [],
- );
- const extractFloor = (lot: any): string => {
- const raw = lot.routerRoute || lot.routerArea || lot.location || "";
- const match = raw.match(/^(\d+F?)/i) || raw.split("-")[0];
- return (match?.[1] || match || raw || "")
- .toUpperCase()
- .replace(/(\d)F?/i, "$1F");
- };
-
- // 楼层排序权重:4F > 3F > 2F(数字越大越靠前)
- const floorSortOrder = (floor: string): number => {
- const n = parseInt(floor.replace(/\D/g, ""), 10);
- return isNaN(n) ? 0 : n;
- };
- const combinedLotData = useMemo(() => {
- const lots = getAllLotsFromHierarchical(jobOrderData);
- // 前端覆盖:issue form/submit0 不会立刻改写后端 qty 时,用本地缓存让 UI 与 batch submit 计算一致
- return lots.map((lot: any) => {
- const solId = Number(lot.stockOutLineId) || 0;
- if (solId > 0) {
- const hasPickedOverride = Object.prototype.hasOwnProperty.call(
- issuePickedQtyBySolId,
- solId,
- );
- const picked = Number(
- issuePickedQtyBySolId[solId] ?? lot.actualPickQty ?? 0,
- );
- const statusRaw =
- localSolStatusById[solId] ?? lot.stockOutLineStatus ?? "";
- const status = String(statusRaw).toLowerCase();
- const isEnded = status === "completed" || status === "rejected";
- return {
- ...lot,
- actualPickQty: hasPickedOverride ? picked : lot.actualPickQty,
- stockOutLineQty: hasPickedOverride ? picked : lot.stockOutLineQty,
- stockOutLineStatus: isEnded ? statusRaw : statusRaw || "checked",
- };
- }
- return lot;
- });
- }, [
- jobOrderData,
- getAllLotsFromHierarchical,
- issuePickedQtyBySolId,
- localSolStatusById,
- ]);
-
- /** 單筆將提交數量:Issue 改數 → pickQtyData → 已完成行用實際量 → noLot/過期/unavailable → 0 → 否則 required(對齊 DO Workbench) */
- const resolveSingleSubmitQty = useCallback(
- (lot: any) => {
- const required = Number(
- lot.requiredQty || lot.pickOrderLineRequiredQty || 0,
- );
- const solId = Number(lot.stockOutLineId) || 0;
- const lotId = lot.lotId;
- const lotKey =
- Number.isFinite(solId) && solId > 0
- ? `sol:${solId}`
- : `${lot.pickOrderLineId}-${lotId}`;
-
- const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined;
- if (issuePicked !== undefined && !Number.isNaN(Number(issuePicked))) {
- return Number(issuePicked);
- }
-
- const st = String(lot.stockOutLineStatus || "").toLowerCase();
- if (
- st === "completed" ||
- st === "partially_completed" ||
- st === "partially_complete"
- ) {
- return Number(lot.stockOutLineQty ?? lot.actualPickQty ?? 0);
- }
-
- if (Object.prototype.hasOwnProperty.call(pickQtyData, lotKey)) {
- const fromPick = pickQtyData[lotKey];
- if (
- fromPick !== undefined &&
- fromPick !== null &&
- !Number.isNaN(Number(fromPick))
- ) {
- return Number(fromPick);
- }
- }
- if (isInventoryLotLineUnavailable(lot)) {
- return 0;
- }
- if (lot.noLot === true || !lot.lotId) {
- return 0;
- }
- if (isLotAvailabilityExpired(lot)) {
- return 0;
- }
- return required;
- },
- [issuePickedQtyBySolId, pickQtyData],
- );
-
- const getWorkbenchQtyLotKey = useCallback((lot: any) => {
- const solId = Number(lot?.stockOutLineId) || 0;
- if (Number.isFinite(solId) && solId > 0) return `sol:${solId}`;
- return `${lot?.pickOrderLineId}-${lot?.lotId}`;
- }, []);
-
- const workbenchScanPickQtyFromLot = useCallback(
- (lot: any) => {
- const solId = Number(lot?.stockOutLineId);
- const sourceLot =
- Number.isFinite(solId) && solId > 0
- ? combinedLotData.find((r) => Number(r.stockOutLineId) === solId) ??
- lot
- : lot;
- const lotKey = getWorkbenchQtyLotKey(sourceLot);
- const hasExplicitPickOverride = Object.prototype.hasOwnProperty.call(
- pickQtyData,
- lotKey,
- );
- const n = Number(resolveSingleSubmitQty(sourceLot));
- if (hasExplicitPickOverride && Number.isFinite(n) && n === 0)
- return { qty: 0 } as const;
- if (!Number.isFinite(n) || n <= 0) return {};
- return { qty: n } as const;
- },
- [resolveSingleSubmitQty, combinedLotData, pickQtyData, getWorkbenchQtyLotKey],
- );
-
- const lotFloorPrefixFilter = useMemo(() => {
- const storeId = String(workbenchStoreId ?? "")
- .trim()
- .toUpperCase()
- .replace(/\s/g, "");
- const floorKey = storeId.replace(/\//g, "");
- return floorKey ? `${floorKey}-` : "";
- }, [workbenchStoreId]);
-
- const defaultLabelPrinterName = useMemo(() => {
- const storeId = String(workbenchStoreId ?? "")
- .trim()
- .toUpperCase()
- .replace(/\s/g, "");
- const floorKey = storeId.replace(/\//g, "");
- if (floorKey === "2F") return "Label機 2F A+B";
- if (floorKey === "4F") return "Label機 4F 乾貨 C, D";
- return undefined;
- }, [workbenchStoreId]);
-
- const openWorkbenchLotLabelModalForLot = useCallback((lot: any, reminderText?: string | null) => {
- const itemId = Number(lot?.itemId);
- const stockInLineId = Number(lot?.stockInLineId);
- const solId = Number(lot?.stockOutLineId);
- if (!Number.isFinite(itemId) || itemId <= 0 || !Number.isFinite(solId) || solId <= 0) {
- return;
- }
- setWorkbenchLotLabelContextLot(lot);
- if (Number.isFinite(stockInLineId) && stockInLineId > 0) {
- setWorkbenchLotLabelInitialPayload({ itemId, stockInLineId });
- } else {
- setWorkbenchLotLabelInitialPayload(null);
- }
- setWorkbenchLotLabelReminderText(reminderText ?? null);
- // Clear latched success so the lot-label modal effect cannot instantly re-close on open.
- setQrScanSuccess(false);
- setWorkbenchLotLabelModalOpen(true);
- }, []);
-
- const shouldOpenWorkbenchLotLabelModalForFailure = useCallback(
- (code?: string | null, msg?: string | null): boolean => {
- const normalizedCode = String(code || "").toUpperCase();
- if (
- normalizedCode.includes("UNAVAILABLE") ||
- normalizedCode.includes("EXPIRED")
- ) {
- return true;
- }
- const normalizedMsg = String(msg || "").toLowerCase();
- if (!normalizedMsg) return false;
- return (
- normalizedMsg.includes("unavailable") ||
- normalizedMsg.includes("not available") ||
- normalizedMsg.includes("expired") ||
- normalizedMsg.includes("不可用") ||
- normalizedMsg.includes("無法使用") ||
- normalizedMsg.includes("过期")
- );
- },
- [],
- );
-
- const handleWorkbenchLotLabelScanPick = useCallback(
- async ({
- inventoryLotLineId,
- lotNo,
- qty,
- }: {
- inventoryLotLineId: number;
- lotNo: string;
- qty?: number;
- }) => {
- const lot = workbenchLotLabelContextLot;
- if (!lot?.stockOutLineId) {
- throw new Error(t("Missing stock out line for this row."));
- }
- const qtyPayload = Number.isFinite(Number(qty))
- ? { qty: Number(qty) }
- : workbenchScanPickQtyFromLot(lot);
- const res = await workbenchScanPick({
- stockOutLineId: Number(lot.stockOutLineId),
- lotNo: String(lotNo || "").trim(),
- inventoryLotLineId,
- storeId: workbenchStoreId,
- userId: currentUserId ?? 1,
- ...qtyPayload,
- });
- if (res.code !== "SUCCESS") {
- const errMsg =
- (res as { message?: string })?.message ||
- t("Workbench scan-pick failed.");
- rememberWorkbenchScanReject(Number(lot.stockOutLineId), errMsg);
- throw new Error(errMsg);
- }
- clearWorkbenchScanReject(Number(lot.stockOutLineId));
- const pickOrderId = filterArgs?.pickOrderId
- ? Number(filterArgs.pickOrderId)
- : undefined;
- if (pickOrderId) {
- const latest =
- await fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench(pickOrderId);
- setJobOrderData(latest);
- setIssuePickedQtyBySolId(loadIssuePickedMapJo(pickOrderId));
- getAllLotsFromHierarchical(latest);
- }
- setWorkbenchLotLabelModalOpen(false);
- setWorkbenchLotLabelContextLot(null);
- setWorkbenchLotLabelInitialPayload(null);
- },
- [
- workbenchLotLabelContextLot,
- currentUserId,
- workbenchScanPickQtyFromLot,
- workbenchStoreId,
- rememberWorkbenchScanReject,
- clearWorkbenchScanReject,
- filterArgs?.pickOrderId,
- getAllLotsFromHierarchical,
- t,
- ],
- );
-
- const workbenchLotLabelSubmitQty = useMemo(() => {
- if (!workbenchLotLabelContextLot) return 0;
- return Number(resolveSingleSubmitQty(workbenchLotLabelContextLot)) || 0;
- }, [workbenchLotLabelContextLot, resolveSingleSubmitQty]);
-
- const handleWorkbenchLotLabelSubmitQtyChange = useCallback(
- (qty: number) => {
- if (!workbenchLotLabelContextLot) return;
- const lotKey = getWorkbenchQtyLotKey(workbenchLotLabelContextLot);
- setPickQtyData((prev) => ({ ...prev, [lotKey]: qty }));
- },
- [workbenchLotLabelContextLot, getWorkbenchQtyLotKey],
- );
-
- const workbenchLotLabelScanPickDisabled = useMemo(() => {
- if (!workbenchLotLabelModalOpen || !workbenchLotLabelContextLot) return true;
- const lot = workbenchLotLabelContextLot;
- const status = String(lot.stockOutLineStatus || "").toLowerCase();
- const isNoLot = !lot.lotNo || String(lot.lotNo).trim() === "";
- if (isNoLot) return false;
- if (status === "pending" || status === "rejected") return false;
- return true;
- }, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot]);
-
- const originalCombinedData = useMemo(() => {
- return getAllLotsFromHierarchical(jobOrderData);
- }, [jobOrderData, getAllLotsFromHierarchical]);
-
- // Enhanced lotDataIndexes with cached active lots for better performance (align with GoodPickExecutiondetail)
- const lotDataIndexes = useMemo(() => {
- const byItemId = new Map<number, any[]>();
- const byItemCode = new Map<string, any[]>();
- const byLotId = new Map<number, any>();
- const byLotNo = new Map<string, any[]>();
- const byStockInLineId = new Map<number, any[]>();
- const activeLotsByItemId = new Map<number, any[]>();
- const rejectedStatuses = new Set(["rejected"]);
-
- for (let i = 0; i < combinedLotData.length; i++) {
- const lot = combinedLotData[i];
- const solStatus = String(lot.stockOutLineStatus || "").toLowerCase();
- const lotAvailability = String(lot.lotAvailability || "").toLowerCase();
- const processingStatus = String(lot.processingStatus || "").toLowerCase();
- const isUnavailable = isInventoryLotLineUnavailable(lot);
- const isExpired = isLotAvailabilityExpired(lot);
- const isRejected =
- rejectedStatuses.has(lotAvailability) ||
- rejectedStatuses.has(solStatus) ||
- rejectedStatuses.has(processingStatus);
- const isEnded = solStatus === "checked" || solStatus === "completed";
- const isPartially =
- solStatus === "partially_completed" ||
- solStatus === "partially_complete";
- const isPending = solStatus === "pending" || solStatus === "";
- const isActive =
- !isRejected &&
- !isUnavailable &&
- !isExpired &&
- !isEnded &&
- (isPending || isPartially);
-
- if (lot.itemId) {
- if (!byItemId.has(lot.itemId)) {
- byItemId.set(lot.itemId, []);
- activeLotsByItemId.set(lot.itemId, []);
- }
- byItemId.get(lot.itemId)!.push(lot);
- if (isActive) activeLotsByItemId.get(lot.itemId)!.push(lot);
- }
- if (lot.itemCode) {
- if (!byItemCode.has(lot.itemCode)) byItemCode.set(lot.itemCode, []);
- byItemCode.get(lot.itemCode)!.push(lot);
- }
- if (lot.lotId) byLotId.set(lot.lotId, lot);
- if (lot.lotNo) {
- if (!byLotNo.has(lot.lotNo)) byLotNo.set(lot.lotNo, []);
- byLotNo.get(lot.lotNo)!.push(lot);
- }
- if (lot.stockInLineId) {
- if (!byStockInLineId.has(lot.stockInLineId))
- byStockInLineId.set(lot.stockInLineId, []);
- byStockInLineId.get(lot.stockInLineId)!.push(lot);
- }
- }
-
- return {
- byItemId,
- byItemCode,
- byLotId,
- byLotNo,
- byStockInLineId,
- activeLotsByItemId,
- };
- }, [combinedLotData]);
-
- // Cached version of fetchStockInLineInfo to avoid redundant API calls
- const fetchStockInLineInfoCached = useCallback(
- async (stockInLineId: number): Promise<{ lotNo: string | null }> => {
- const now = Date.now();
- const cached = stockInLineInfoCache.current.get(stockInLineId);
- if (cached && now - cached.timestamp < CACHE_TTL) {
- return { lotNo: cached.lotNo };
- }
-
- if (abortControllerRef.current) abortControllerRef.current.abort();
- const abortController = new AbortController();
- abortControllerRef.current = abortController;
-
- const stockInLineInfo = await fetchStockInLineInfo(stockInLineId);
- stockInLineInfoCache.current.set(stockInLineId, {
- lotNo: stockInLineInfo.lotNo || null,
- timestamp: now,
- });
- if (stockInLineInfoCache.current.size > 100) {
- const firstKey = stockInLineInfoCache.current.keys().next().value;
- if (firstKey !== undefined)
- stockInLineInfoCache.current.delete(firstKey);
- }
- return { lotNo: stockInLineInfo.lotNo || null };
- },
- [],
- );
- // 修改:加载未分配的 Job Order 订单
- const loadUnassignedOrders = useCallback(async () => {
- setIsLoadingUnassigned(true);
- try {
- //const orders = await fetchUnassignedJobOrderPickOrders();
- //setUnassignedOrders(orders);
- } catch (error) {
- console.error("Error loading unassigned orders:", error);
- } finally {
- setIsLoadingUnassigned(false);
- }
- }, []);
-
- // 修改:分配订单给当前用户
- const handleAssignOrder = useCallback(
- async (pickOrderId: number) => {
- if (!currentUserId) {
- console.error("Missing user id in session");
- return;
- }
-
- try {
- const result = await assignJobOrderPickOrderForWorkbench(
- pickOrderId,
- currentUserId,
- );
- const msg = String(result?.message || "");
- const assignSkippedByStatus =
- msg.toLowerCase().startsWith("skipped assign:");
- if (msg === "Successfully assigned" || assignSkippedByStatus) {
- console.log(" Successfully assigned pick order");
- if (!assignSkippedByStatus) {
- try {
- /*
- const refreshed =
- await fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench(
- pickOrderId,
- );
-
- const uniqueItemIds = Array.from(
- new Set(
- (refreshed?.pickOrderLines ?? [])
- .map((line) => line?.itemId)
- .filter(
- (itemId): itemId is number =>
- typeof itemId === "number" &&
- Number.isFinite(itemId) &&
- itemId > 0,
- ),
- ),
- );
-
- await Promise.all(
- uniqueItemIds.map((itemId) =>
- updateJoPickOrderHandledBy({
- pickOrderId,
- itemId,
- userId: currentUserId,
- }),
- ),
- );
- */
- } catch (handledBySyncError) {
- console.warn(
- "⚠️ Assigned but failed to sync JoPickOrder.handledBy for some lines:",
- handledBySyncError,
- );
- }
- }
- // 刷新数据
- window.dispatchEvent(new CustomEvent("pickOrderAssigned"));
- // 重新加载未分配订单列表
- loadUnassignedOrders();
- } else {
- console.warn("⚠️ Assignment failed:", result.message);
- alert(`Assignment failed: ${result.message}`);
- }
- } catch (error) {
- console.error("❌ Error assigning order:", error);
- alert("Error occurred during assignment");
- }
- },
- [currentUserId, loadUnassignedOrders],
- );
-
- const fetchFgPickOrdersData = useCallback(async () => {
- if (!currentUserId) return;
-
- setFgPickOrdersLoading(true);
- try {
- const allFgPickOrders = await fetchFGPickOrdersByUserIdWorkbench(
- currentUserId,
- );
- setFgPickOrders(Array.isArray(allFgPickOrders) ? allFgPickOrders : []);
- console.log(" Fetched FG pick orders(workbench):", allFgPickOrders);
- } catch (error) {
- console.error("❌ Error fetching FG pick orders:", error);
- setFgPickOrders([]);
- } finally {
- setFgPickOrdersLoading(false);
- }
- }, [currentUserId]);
-
- useEffect(() => {
- if (combinedLotData.length > 0) {
- fetchFgPickOrdersData();
- }
- }, [combinedLotData, fetchFgPickOrdersData]);
-
- // Handle QR code button click
- const handleQrCodeClick = (pickOrderId: number) => {
- console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
- // TODO: Implement QR code functionality
- };
-
- // 修改:使用 Job Order API 获取数据
- const fetchJobOrderData = useCallback(
- async (pickOrderId?: number) => {
- setCombinedDataLoading(true);
- try {
- if (!pickOrderId) {
- console.warn("⚠️ No pickOrderId provided, skipping API call");
- return;
- }
-
- // 直接使用类型化的响应
- const jobOrderData =
- await fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench(pickOrderId);
- console.log("✅ Job Order data (hierarchical):", jobOrderData);
-
- setJobOrderData(jobOrderData);
- setIssuePickedQtyBySolId(loadIssuePickedMapJo(pickOrderId));
-
- // 使用辅助函数获取所有 lots(不再扁平化)
- getAllLotsFromHierarchical(jobOrderData);
- } catch (error) {
- console.error("❌ Error fetching job order data:", error);
- setJobOrderData(null);
- setIssuePickedQtyBySolId({});
- } finally {
- setCombinedDataLoading(false);
- }
- },
- [getAllLotsFromHierarchical],
- );
-
- const applyLocalStockOutLineUpdate = useCallback((..._args: unknown[]) => {
- // JO Workbench:以伺服器刷新為主;保留呼叫點與 DO Workbench 一致(不更新本地 SOL 鏡像)。
- }, []);
-
- const refreshWorkbenchAfterScanPick = useCallback(async () => {
- const pickOrderId = filterArgs?.pickOrderId
- ? Number(filterArgs.pickOrderId)
- : undefined;
- await fetchJobOrderData(pickOrderId);
- }, [fetchJobOrderData, filterArgs?.pickOrderId]);
-
- const updateHandledBy = useCallback(
- async (pickOrderId: number, itemId: number) => {
- if (!currentUserId || !pickOrderId || !itemId) {
- return;
- }
-
- try {
- console.log(
- `Updating JoPickOrder.handledBy for pickOrderId: ${pickOrderId}, itemId: ${itemId}, userId: ${currentUserId}`,
- );
- await updateJoPickOrderHandledBy({
- pickOrderId: pickOrderId,
- itemId: itemId,
- userId: currentUserId,
- });
- console.log("✅ JoPickOrder.handledBy updated successfully");
- } catch (error) {
- console.error("❌ Error updating JoPickOrder.handledBy:", error);
- // Don't throw - this is not critical for the main flow
- }
- },
- [currentUserId],
- );
- // 修改:初始化 — Workbench 先 prime SPL/SOL(後端不寫 pick_order.assignTo),再載入階層資料
- useEffect(() => {
- if (session && currentUserId && !initializationRef.current) {
- console.log("✅ Session loaded, initializing job order...");
- initializationRef.current = true;
-
- const pickOrderId = filterArgs?.pickOrderId
- ? Number(filterArgs.pickOrderId)
- : undefined;
- if (pickOrderId) {
- void (async () => {
- try {
- await handleAssignOrder(pickOrderId);
- } finally {
- await fetchJobOrderData(pickOrderId);
- }
- })();
- }
- loadUnassignedOrders();
- }
- }, [
- session,
- currentUserId,
- fetchJobOrderData,
- loadUnassignedOrders,
- filterArgs?.pickOrderId,
- handleAssignOrder,
- ]);
-
- // 與 GoodPickExecutiondetail 一致:session 就緒後自動開啟背景掃碼(平板現場用)
- useEffect(() => {
- if (session && currentUserId && !scannerInitializedRef.current) {
- scannerInitializedRef.current = true;
- console.log("✅ [JO] Auto-starting QR scanner in background mode");
- setIsManualScanning(true);
- startScan();
- }
- }, [session, currentUserId, startScan]);
-
- // 僅在元件卸載時重置,讓 React Strict Mode 二次掛載仍能再走一次自動開掃(不因 startScan 引用變化而重複開掃)
- useEffect(() => {
- return () => {
- scannerInitializedRef.current = false;
- };
- }, []);
-
- // Add event listener for manual assignment
- useEffect(() => {
- const handlePickOrderAssigned = () => {
- console.log("🔄 Pick order assigned event received, refreshing data...");
- const pickOrderId = filterArgs?.pickOrderId
- ? Number(filterArgs.pickOrderId)
- : undefined;
- if (pickOrderId) {
- fetchJobOrderData(pickOrderId);
- }
- };
-
- window.addEventListener("pickOrderAssigned", handlePickOrderAssigned);
-
- return () => {
- window.removeEventListener("pickOrderAssigned", handlePickOrderAssigned);
- };
- }, [fetchJobOrderData, filterArgs?.pickOrderId]);
-
- /** 純 lotNo 手動輸入:與 DO Workbench 相同,不寫 checked,改提示用 scan-pick / Just Completed */
- const handleQrCodeSubmit = useCallback(
- async (lotNo: string) => {
- console.log(` Processing QR Code for lot: ${lotNo}`);
-
- if (!lotNo || lotNo === "null" || lotNo.trim() === "") {
- console.error(" Invalid lotNo: null, undefined, or empty");
- return;
- }
-
- const currentLotData = combinedLotData;
- console.log(
- ` Available lots:`,
- currentLotData.map((lot) => lot.lotNo),
- );
-
- const lotNoLower = lotNo.toLowerCase();
- const matchingLots = currentLotData.filter((lot) => {
- if (!lot.lotNo) return false;
- return lot.lotNo === lotNo || lot.lotNo.toLowerCase() === lotNoLower;
- });
-
- if (matchingLots.length === 0) {
- console.error(` Lot not found: ${lotNo}`);
- setQrScanError(true);
- setQrScanSuccess(false);
- const availableLotNos = currentLotData
- .map((lot) => lot.lotNo)
- .join(", ");
- console.log(
- ` QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`,
- );
- return;
- }
-
- const hasExpiredLot = matchingLots.some(
- (lot: any) =>
- String(lot.lotAvailability || "").toLowerCase() === "expired",
- );
- if (hasExpiredLot) {
- console.warn(`⚠️ [QR PROCESS] Scanned lot ${lotNo} is expired`);
- setQrScanError(true);
- setQrScanSuccess(false);
- return;
- }
-
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(
- tPick(
- "Workbench uses scan-pick. Please use Just Completed / submit via scan-pick instead of checked status.",
- ),
- );
- },
- [combinedLotData, tPick],
- );
-
- const clearLotConfirmationState = useCallback(
- (clearProcessedRefs: boolean = false) => {
- setExpectedLotData(null);
- setScannedLotData(null);
- setSelectedLotForQr(null);
-
- if (clearProcessedRefs) {
- setTimeout(() => {
- lastProcessedQrRef.current = "";
- processedQrCodesRef.current.clear();
- }, 100);
- }
- },
- [],
- );
-
- // Add handleLotConfirmation function
- const handleLotConfirmation = useCallback(
- async (overrideScannedLot?: any) => {
- const effectiveScannedLot = overrideScannedLot ?? scannedLotData;
- if (!expectedLotData || !effectiveScannedLot || !selectedLotForQr) {
- console.error(
- "❌ [LOT CONFIRM] Missing required data for lot confirmation",
- );
- return;
- }
-
- console.log(
- "✅ [LOT CONFIRM] User confirmed lot substitution - processing now",
- );
- console.log("✅ [LOT CONFIRM] Expected lot:", expectedLotData);
- console.log("✅ [LOT CONFIRM] Scanned lot:", scannedLotData);
- console.log("✅ [LOT CONFIRM] Selected lot for QR:", selectedLotForQr);
-
- setIsConfirmingLot(true);
- try {
- let newLotLineId = effectiveScannedLot?.inventoryLotLineId;
- if (!newLotLineId && effectiveScannedLot?.stockInLineId) {
- try {
- if (
- currentUserId &&
- selectedLotForQr.pickOrderId &&
- selectedLotForQr.itemId
- ) {
- try {
- await updateHandledBy(
- selectedLotForQr.pickOrderId,
- selectedLotForQr.itemId,
- );
- console.log(
- `✅ [LOT CONFIRM] Handler updated for itemId ${selectedLotForQr.itemId}`,
- );
- } catch (error) {
- console.error(
- `❌ [LOT CONFIRM] Error updating handler (non-critical):`,
- error,
- );
- }
- }
- console.log(
- `🔍 [LOT CONFIRM] Fetching lot detail for stockInLineId: ${effectiveScannedLot.stockInLineId}`,
- );
- const ld = await fetchLotDetail(effectiveScannedLot.stockInLineId);
- newLotLineId = ld.inventoryLotLineId;
- console.log(
- `✅ [LOT CONFIRM] Fetched lot detail: inventoryLotLineId=${newLotLineId}`,
- );
- } catch (error) {
- console.error(
- "❌ [LOT CONFIRM] Error fetching lot detail (stockInLineId may not exist):",
- error,
- );
- // If stockInLineId doesn't exist, we can still proceed with lotNo substitution
- // The backend confirmLotSubstitution should handle this case
- }
- }
- if (!newLotLineId) {
- console.warn(
- "⚠️ [LOT CONFIRM] No inventory lot line id for scanned lot, proceeding with lotNo only",
- );
- // Continue anyway - backend may handle lotNo substitution without inventoryLotLineId
- }
-
- console.log("=== [LOT CONFIRM] Lot Confirmation Debug ===");
- console.log("Selected Lot:", selectedLotForQr);
- console.log("Pick Order Line ID:", selectedLotForQr.pickOrderLineId);
- console.log("Stock Out Line ID:", selectedLotForQr.stockOutLineId);
- console.log(
- "Suggested Pick Lot ID:",
- selectedLotForQr.suggestedPickLotId,
- );
- console.log("Lot ID (fallback):", selectedLotForQr.lotId);
- console.log("New Inventory Lot Line ID:", newLotLineId);
- console.log("Scanned Lot No:", effectiveScannedLot.lotNo);
- console.log(
- "Scanned StockInLineId:",
- effectiveScannedLot.stockInLineId,
- );
-
- const originalSuggestedPickLotId =
- selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId;
-
- let switchedToUnavailable = false;
- // noLot / missing suggestedPickLotId 场景:没有 originalSuggestedPickLotId,改用 updateStockOutLineStatusByQRCodeAndLotNo
- if (!originalSuggestedPickLotId) {
- if (!selectedLotForQr?.stockOutLineId) {
- throw new Error("Missing stockOutLineId for noLot line");
- }
- console.log(
- "🔄 [LOT CONFIRM] No originalSuggestedPickLotId, using updateStockOutLineStatusByQRCodeAndLotNo...",
- );
- const res = await updateStockOutLineStatusByQRCodeAndLotNo({
- pickOrderLineId: selectedLotForQr.pickOrderLineId,
- inventoryLotNo: effectiveScannedLot.lotNo || "",
- stockInLineId: effectiveScannedLot?.stockInLineId ?? null,
- stockOutLineId: selectedLotForQr.stockOutLineId,
- itemId: selectedLotForQr.itemId,
- status: "checked",
- });
- console.log(
- "✅ [LOT CONFIRM] updateStockOutLineStatusByQRCodeAndLotNo result:",
- res,
- );
- switchedToUnavailable = res?.code === "BOUND_UNAVAILABLE";
- const ok =
- res?.code === "checked" ||
- res?.code === "SUCCESS" ||
- switchedToUnavailable;
- if (!ok) {
- const errMsg =
- res?.code === "LOT_UNAVAILABLE"
- ? tPick(
- "The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated.",
- )
- : res?.message ||
- tPick(
- "Lot switch failed; pick line was not marked as checked.",
- );
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(errMsg);
- return;
- }
- } else {
- // Call confirmLotSubstitution to update the suggested lot
- console.log("🔄 [LOT CONFIRM] Calling confirmLotSubstitution...");
- const substitutionResult = await confirmLotSubstitution({
- pickOrderLineId: selectedLotForQr.pickOrderLineId,
- stockOutLineId: selectedLotForQr.stockOutLineId,
- originalSuggestedPickLotId,
- newInventoryLotNo: effectiveScannedLot.lotNo || "",
- // ✅ required by LotSubstitutionConfirmRequest
- newStockInLineId: effectiveScannedLot?.stockInLineId ?? null,
- });
-
- console.log(
- "✅ [LOT CONFIRM] Lot substitution result:",
- substitutionResult,
- );
-
- // ✅ CRITICAL: substitution failed => DO NOT mark original stockOutLine as checked.
- // Keep modal open so user can cancel/rescan.
- switchedToUnavailable =
- substitutionResult?.code === "SUCCESS_UNAVAILABLE" ||
- substitutionResult?.code === "BOUND_UNAVAILABLE";
- if (
- !substitutionResult ||
- (substitutionResult.code !== "SUCCESS" && !switchedToUnavailable)
- ) {
- console.error(
- "❌ [LOT CONFIRM] Lot substitution failed. Will NOT update stockOutLine status.",
- );
- const errMsg =
- substitutionResult?.code === "LOT_UNAVAILABLE"
- ? tPick(
- "The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated.",
- )
- : substitutionResult?.message ||
- `换批失败:stockInLineId ${
- effectiveScannedLot?.stockInLineId ?? ""
- } 不存在或无法匹配`;
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(errMsg);
- return;
- }
- }
-
- // Workbench:以 scan-pick 過帳(與 DO Workbench 一致);非 workbench 仍寫 checked
- if (
- workbenchMode &&
- selectedLotForQr?.stockOutLineId &&
- !switchedToUnavailable
- ) {
- const lotNoToScan = String(effectiveScannedLot.lotNo || "").trim();
- const silId = effectiveScannedLot?.stockInLineId;
- if (!lotNoToScan) {
- const errMsg = tPick("Cannot resolve lot number for confirmation.");
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(errMsg);
- return;
- }
- const res = await workbenchScanPick({
- stockOutLineId: selectedLotForQr.stockOutLineId,
- lotNo: lotNoToScan,
- ...(typeof silId === "number" && Number.isFinite(silId) && silId > 0
- ? { stockInLineId: silId }
- : {}),
- ...workbenchScanPickQtyFromLot(selectedLotForQr),
- storeId: workbenchStoreId,
- userId: currentUserId ?? 1,
- });
- if (res.code !== "SUCCESS") {
- const errMsg =
- (res as { message?: string })?.message ||
- tPick("Workbench scan-pick failed.");
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(errMsg);
- if (selectedLotForQr.stockOutLineId != null) {
- rememberWorkbenchScanReject(
- Number(selectedLotForQr.stockOutLineId),
- errMsg,
- );
- }
- return;
- }
- clearWorkbenchScanReject(Number(selectedLotForQr.stockOutLineId));
- } else if (selectedLotForQr?.stockOutLineId && !switchedToUnavailable) {
- console.log(
- `🔄 [LOT CONFIRM] Updating stockOutLine ${selectedLotForQr.stockOutLineId} to 'checked'`,
- );
- await updateStockOutLineStatus({
- id: selectedLotForQr.stockOutLineId,
- status: "checked",
- qty: 0,
- });
- console.log(
- `✅ [LOT CONFIRM] Stock out line ${selectedLotForQr.stockOutLineId} status updated to 'checked'`,
- );
- }
-
- // Close modal and clean up state BEFORE refreshing
- clearLotConfirmationState(false);
-
- // Clear QR processing state but DON'T clear processedQrCodes yet
- setQrScanError(false);
- setQrScanSuccess(true);
- setQrScanErrorMsg("");
- setQrScanInput("");
-
- // Set refreshing flag to prevent QR processing during refresh
- setIsRefreshingData(true);
-
- // Refresh data to show updated lot
- console.log("🔄 Refreshing job order data...");
- const pickOrderId = filterArgs?.pickOrderId
- ? Number(filterArgs.pickOrderId)
- : undefined;
- await fetchJobOrderData(pickOrderId);
- console.log(" Lot substitution confirmed and data refreshed");
-
- // Clear processed QR codes and flags immediately after refresh
- // This allows new QR codes to be processed right away
- setTimeout(() => {
- console.log(" Clearing processed QR codes and resuming scan");
- setProcessedQrCodes(new Set());
- setLastProcessedQr("");
- setQrScanSuccess(false);
- setIsRefreshingData(false);
- // ✅ Clear processedQrCombinations to allow reprocessing the same QR if needed
- if (effectiveScannedLot?.stockInLineId && selectedLotForQr?.itemId) {
- setProcessedQrCombinations((prev) => {
- const newMap = new Map(prev);
- const itemId = selectedLotForQr.itemId;
- if (itemId && newMap.has(itemId)) {
- newMap.get(itemId)!.delete(effectiveScannedLot.stockInLineId);
- if (newMap.get(itemId)!.size === 0) {
- newMap.delete(itemId);
- }
- }
- return newMap;
- });
- }
- }, 500); // Reduced from 3000ms to 500ms - just enough for UI update
- } catch (error) {
- console.error("Error confirming lot substitution:", error);
- const errMsg = tPick("Lot confirmation failed. Please try again.");
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(errMsg);
- // Clear refresh flag on error
- setIsRefreshingData(false);
- } finally {
- setIsConfirmingLot(false);
- }
- },
- [
- expectedLotData,
- scannedLotData,
- selectedLotForQr,
- fetchJobOrderData,
- currentUserId,
- updateHandledBy,
- tPick,
- clearLotConfirmationState,
- workbenchMode,
- workbenchScanPickQtyFromLot,
- workbenchStoreId,
- rememberWorkbenchScanReject,
- clearWorkbenchScanReject,
- ],
- );
-
- const processOutsideQrCode = useCallback(
- async (latestQr: string, _qrScanCountAtInvoke?: number) => {
- const totalStartTime = performance.now();
- console.log(
- ` [PROCESS OUTSIDE QR START] QR: ${latestQr.substring(0, 50)}...`,
- );
- console.log(` Start time: ${new Date().toISOString()}`);
-
- // ✅ Measure index access time
- const indexAccessStart = performance.now();
- const indexes = lotDataIndexes; // Access the memoized indexes
- const indexAccessTime = performance.now() - indexAccessStart;
- console.log(
- ` [PERF] Index access time: ${indexAccessTime.toFixed(2)}ms`,
- );
-
- // 1) Parse JSON safely (parse once, reuse)
- const parseStartTime = performance.now();
- let qrData: any = null;
- let parseTime = 0;
- try {
- qrData = JSON.parse(latestQr);
- parseTime = performance.now() - parseStartTime;
- console.log(` [PERF] JSON parse time: ${parseTime.toFixed(2)}ms`);
- } catch {
- console.log(
- "QR content is not JSON; skipping lotNo direct submit to avoid false matches.",
- );
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- });
- return;
- }
-
- try {
- const validationStartTime = performance.now();
- if (!(qrData?.stockInLineId && qrData?.itemId)) {
- console.log(
- "QR JSON missing required fields (itemId, stockInLineId).",
- );
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- });
- return;
- }
- const validationTime = performance.now() - validationStartTime;
- console.log(` [PERF] Validation time: ${validationTime.toFixed(2)}ms`);
-
- const scannedItemId = qrData.itemId;
- const scannedStockInLineId = qrData.stockInLineId;
-
- // ✅ Check if this combination was already processed
- const duplicateCheckStartTime = performance.now();
- const itemProcessedSet = processedQrCombinations.get(scannedItemId);
- if (itemProcessedSet?.has(scannedStockInLineId)) {
- const duplicateCheckTime =
- performance.now() - duplicateCheckStartTime;
- console.log(
- ` [SKIP] Already processed combination: itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId} (check time: ${duplicateCheckTime.toFixed(
- 2,
- )}ms)`,
- );
- return;
- }
- const duplicateCheckTime = performance.now() - duplicateCheckStartTime;
- console.log(
- ` [PERF] Duplicate check time: ${duplicateCheckTime.toFixed(2)}ms`,
- );
-
- // ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed)
- const lookupStartTime = performance.now();
- const activeSuggestedLots =
- indexes.activeLotsByItemId.get(scannedItemId) || [];
- // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected
- const allLotsForItem = indexes.byItemId.get(scannedItemId) || [];
- const lookupTime = performance.now() - lookupStartTime;
- console.log(
- ` [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${
- activeSuggestedLots.length
- } active lots, ${allLotsForItem.length} total lots`,
- );
-
- // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots
- // This allows users to scan other lots even when all suggested lots are rejected
- const scannedLot = allLotsForItem.find(
- (lot: any) => lot.stockInLineId === scannedStockInLineId,
- );
-
- if (scannedLot) {
- const isRejected =
- scannedLot.stockOutLineStatus?.toLowerCase() === "rejected" ||
- scannedLot.lotAvailability === "rejected" ||
- isInventoryLotLineUnavailable(scannedLot);
-
- if (isRejected) {
- console.warn(
- `⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`,
- );
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(
- `此批次(${
- scannedLot.lotNo || scannedStockInLineId
- })已被拒绝,无法使用。请扫描其他批次。`,
- );
- });
- // Mark as processed to prevent re-processing
- setProcessedQrCombinations((prev) => {
- const newMap = new Map(prev);
- if (!newMap.has(scannedItemId))
- newMap.set(scannedItemId, new Set());
- newMap.get(scannedItemId)!.add(scannedStockInLineId);
- return newMap;
- });
- return;
- }
-
- const isExpired =
- String(scannedLot.lotAvailability || "").toLowerCase() ===
- "expired";
- if (isExpired) {
- console.warn(
- `⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is expired`,
- );
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(
- `此批次(${
- scannedLot.lotNo || scannedStockInLineId
- })已过期,无法使用。请扫描其他批次。`,
- );
- });
- // Mark as processed to prevent re-processing the same expired QR repeatedly
- setProcessedQrCombinations((prev) => {
- const newMap = new Map(prev);
- if (!newMap.has(scannedItemId))
- newMap.set(scannedItemId, new Set());
- newMap.get(scannedItemId)!.add(scannedStockInLineId);
- return newMap;
- });
- return;
- }
- }
-
- // ✅ If no active suggested lots, auto-switch to scanned lot (no modal)
- if (activeSuggestedLots.length === 0) {
- // Check if there are any lots for this item (even if all are rejected)
- if (allLotsForItem.length === 0) {
- console.error("No lots found for this item");
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg("当前订单中没有此物品的批次信息");
- });
- return;
- }
-
- console.log(
- `⚠️ [QR PROCESS] No active suggested lots, auto-switching to scanned lot.`,
- );
-
- // Find a rejected lot as expected lot (the one that was rejected)
- const rejectedLot = allLotsForItem.find(
- (lot: any) =>
- lot.stockOutLineStatus?.toLowerCase() === "rejected" ||
- lot.lotAvailability === "rejected" ||
- isInventoryLotLineUnavailable(lot),
- );
- const expectedLot =
- rejectedLot ||
- pickExpectedLotForSubstitution(
- allLotsForItem.filter(
- (l: any) => l.lotNo != null && String(l.lotNo).trim() !== "",
- ),
- ) ||
- allLotsForItem[0];
-
- let scannedLotNo: string | null = scannedLot?.lotNo || null;
- if (!scannedLotNo) {
- try {
- const info =
- await fetchStockInLineInfoCached(scannedStockInLineId);
- scannedLotNo = info?.lotNo || null;
- } catch (e) {
- console.warn(
- "Failed to fetch lotNo for stockInLineId:",
- scannedStockInLineId,
- e,
- );
- }
- }
- if (!scannedLotNo) {
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(
- tPick(
- "Cannot resolve lot number from QR. Please rescan or use manual confirmation.",
- ),
- );
- });
- return;
- }
-
- if (!workbenchMode) {
- const substitutionResult = await confirmLotSubstitution({
- pickOrderLineId: expectedLot.pickOrderLineId,
- stockOutLineId: expectedLot.stockOutLineId,
- originalSuggestedPickLotId: expectedLot.suggestedPickLotId,
- newInventoryLotNo: "",
- newStockInLineId: scannedStockInLineId,
- });
- const substitutionCode = (substitutionResult as any)?.code;
- const switchedToUnavailable =
- substitutionCode === "SUCCESS_UNAVAILABLE" ||
- substitutionCode === "BOUND_UNAVAILABLE";
- if (
- !substitutionResult ||
- (substitutionCode !== "SUCCESS" && !switchedToUnavailable)
- ) {
- const errMsg =
- substitutionResult?.message ||
- tPick("Lot switch failed; pick line was not updated.");
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(errMsg);
- });
- if (expectedLot.stockOutLineId != null) {
- rememberWorkbenchScanReject(
- Number(expectedLot.stockOutLineId),
- errMsg,
- );
- }
- return;
- }
- }
-
- const res = await workbenchScanPick({
- stockOutLineId: expectedLot.stockOutLineId,
- lotNo: scannedLotNo,
- ...(typeof scannedStockInLineId === "number" &&
- Number.isFinite(scannedStockInLineId) &&
- scannedStockInLineId > 0
- ? { stockInLineId: scannedStockInLineId }
- : {}),
- ...workbenchScanPickQtyFromLot(expectedLot),
- storeId: workbenchStoreId,
- userId: currentUserId ?? 1,
- });
- const ok = res.code === "SUCCESS";
- if (!ok) {
- const failMsg =
- (res as { message?: string })?.message ||
- tPick("Workbench scan-pick failed.");
- if (
- shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) &&
- expectedLot
- ) {
- openWorkbenchLotLabelModalForLot(expectedLot, failMsg);
- return;
- }
- if (workbenchMode && expectedLot.stockOutLineId != null) {
- rememberWorkbenchScanReject(
- Number(expectedLot.stockOutLineId),
- failMsg,
- );
- }
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(failMsg);
- });
- return;
- }
-
- clearWorkbenchScanReject(Number(expectedLot.stockOutLineId));
-
- startTransition(() => {
- setQrScanError(false);
- setQrScanSuccess(true);
- });
-
- setProcessedQrCombinations((prev) => {
- const newMap = new Map(prev);
- if (!newMap.has(scannedItemId))
- newMap.set(scannedItemId, new Set());
- newMap.get(scannedItemId)!.add(scannedStockInLineId);
- return newMap;
- });
-
- if (workbenchMode) {
- await refreshWorkbenchAfterScanPick();
- }
-
- return;
- }
-
- // ✅ OPTIMIZATION: Direct Map lookup for stockInLineId match (O(1))
- const matchStartTime = performance.now();
- let exactMatch: any = null;
- const stockInLineLots =
- indexes.byStockInLineId.get(scannedStockInLineId) || [];
- // Find exact match from stockInLineId index, then verify it's in active lots
- for (let i = 0; i < stockInLineLots.length; i++) {
- const lot = stockInLineLots[i];
- if (
- lot.itemId === scannedItemId &&
- activeSuggestedLots.includes(lot)
- ) {
- exactMatch = lot;
- break;
- }
- }
- const matchTime = performance.now() - matchStartTime;
- console.log(
- ` [PERF] Find exact match time: ${matchTime.toFixed(2)}ms, found: ${
- exactMatch ? "yes" : "no"
- }`,
- );
-
- // ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots
- // This handles the case where Lot A is rejected and user scans Lot B
- // Also handle case where scanned lot is not in allLotsForItem (scannedLot is undefined)
- if (!exactMatch) {
- const expectedLot =
- pickExpectedLotForSubstitution(activeSuggestedLots) ||
- allLotsForItem[0];
- if (expectedLot) {
- const shouldAutoSwitch =
- !scannedLot ||
- scannedLot.stockInLineId !== expectedLot.stockInLineId;
- if (shouldAutoSwitch) {
- console.log(
- `⚠️ [QR PROCESS] Auto-switching (scanned lot ${
- scannedLot?.lotNo || "not in data"
- } is not in active suggested lots)`,
- );
-
- let scannedLotNo: string | null = scannedLot?.lotNo || null;
- if (!scannedLotNo) {
- try {
- const info =
- await fetchStockInLineInfoCached(scannedStockInLineId);
- scannedLotNo = info?.lotNo || null;
- } catch (e) {
- console.warn(
- "Failed to fetch lotNo for stockInLineId:",
- scannedStockInLineId,
- e,
- );
- }
- }
- if (!scannedLotNo) {
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(
- tPick(
- "Cannot resolve lot number from QR. Please rescan or use manual confirmation.",
- ),
- );
- });
- return;
- }
-
- if (!workbenchMode) {
- const substitutionResult = await confirmLotSubstitution({
- pickOrderLineId: expectedLot.pickOrderLineId,
- stockOutLineId: expectedLot.stockOutLineId,
- originalSuggestedPickLotId: expectedLot.suggestedPickLotId,
- newInventoryLotNo: "",
- newStockInLineId: scannedStockInLineId,
- });
- const substitutionCode = (substitutionResult as any)?.code;
- const switchedToUnavailable =
- substitutionCode === "SUCCESS_UNAVAILABLE" ||
- substitutionCode === "BOUND_UNAVAILABLE";
- if (
- !substitutionResult ||
- (substitutionCode !== "SUCCESS" && !switchedToUnavailable)
- ) {
- const errMsg =
- substitutionResult?.message ||
- tPick("Lot switch failed; pick line was not updated.");
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(errMsg);
- });
- if (expectedLot.stockOutLineId != null) {
- rememberWorkbenchScanReject(
- Number(expectedLot.stockOutLineId),
- errMsg,
- );
- }
- return;
- }
- }
-
- const res = await workbenchScanPick({
- stockOutLineId: expectedLot.stockOutLineId,
- lotNo: scannedLotNo,
- ...(typeof scannedStockInLineId === "number" &&
- Number.isFinite(scannedStockInLineId) &&
- scannedStockInLineId > 0
- ? { stockInLineId: scannedStockInLineId }
- : {}),
- ...workbenchScanPickQtyFromLot(expectedLot),
- storeId: workbenchStoreId,
- userId: currentUserId ?? 1,
- });
- const ok = res.code === "SUCCESS";
- if (!ok) {
- const failMsg =
- (res as { message?: string })?.message ||
- tPick("Workbench scan-pick failed.");
- if (
- shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) &&
- expectedLot
- ) {
- openWorkbenchLotLabelModalForLot(expectedLot, failMsg);
- return;
- }
- if (workbenchMode && expectedLot.stockOutLineId != null) {
- rememberWorkbenchScanReject(
- Number(expectedLot.stockOutLineId),
- failMsg,
- );
- }
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(failMsg);
- });
- return;
- }
-
- clearWorkbenchScanReject(Number(expectedLot.stockOutLineId));
-
- startTransition(() => {
- setQrScanError(false);
- setQrScanSuccess(true);
- });
-
- setProcessedQrCombinations((prev) => {
- const newMap = new Map(prev);
- if (!newMap.has(scannedItemId))
- newMap.set(scannedItemId, new Set());
- newMap.get(scannedItemId)!.add(scannedStockInLineId);
- return newMap;
- });
- if (workbenchMode) {
- await refreshWorkbenchAfterScanPick();
- }
- return;
- }
- }
- }
-
- if (exactMatch) {
- // ✅ Case 1: stockInLineId 匹配 - 直接处理,不需要确认
- console.log(
- `✅ Exact stockInLineId match found for lot: ${exactMatch.lotNo}`,
- );
-
- if (!exactMatch.stockOutLineId) {
- console.warn(
- "No stockOutLineId on exactMatch, cannot update status by QR.",
- );
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- });
- return;
- }
-
- try {
- const apiStartTime = performance.now();
- console.log(
- workbenchMode
- ? ` [API CALL START] workbenchScanPick`
- : ` [API CALL START] Calling updateStockOutLineStatusByQRCodeAndLotNo`,
- );
- console.log(
- ` [API CALL] API start time: ${new Date().toISOString()}`,
- );
- const res = await workbenchScanPick({
- stockOutLineId: exactMatch.stockOutLineId,
- lotNo: exactMatch.lotNo,
- ...(typeof scannedStockInLineId === "number" &&
- Number.isFinite(scannedStockInLineId) &&
- scannedStockInLineId > 0
- ? { stockInLineId: scannedStockInLineId }
- : typeof exactMatch.stockInLineId === "number" &&
- Number.isFinite(exactMatch.stockInLineId) &&
- exactMatch.stockInLineId > 0
- ? { stockInLineId: exactMatch.stockInLineId }
- : {}),
- ...workbenchScanPickQtyFromLot(exactMatch),
- storeId: workbenchStoreId,
- userId: currentUserId ?? 1,
- });
- const apiTime = performance.now() - apiStartTime;
- console.log(
- ` [API CALL END] Total API time: ${apiTime.toFixed(2)}ms (${(
- apiTime / 1000
- ).toFixed(3)}s)`,
- );
- console.log(
- ` [API CALL] API end time: ${new Date().toISOString()}`,
- );
-
- const ok = res.code === "SUCCESS";
- if (ok) {
- clearWorkbenchScanReject(Number(exactMatch.stockOutLineId));
-
- // ✅ Batch state updates using startTransition
- const stateUpdateStartTime = performance.now();
- startTransition(() => {
- setQrScanError(false);
- setQrScanSuccess(true);
- });
- const stateUpdateTime = performance.now() - stateUpdateStartTime;
- console.log(
- ` [PERF] State update time: ${stateUpdateTime.toFixed(2)}ms`,
- );
-
- // Mark this combination as processed
- const markProcessedStartTime = performance.now();
- setProcessedQrCombinations((prev) => {
- const newMap = new Map(prev);
- if (!newMap.has(scannedItemId)) {
- newMap.set(scannedItemId, new Set());
- }
- newMap.get(scannedItemId)!.add(scannedStockInLineId);
- return newMap;
- });
- const markProcessedTime =
- performance.now() - markProcessedStartTime;
- console.log(
- ` [PERF] Mark processed time: ${markProcessedTime.toFixed(
- 2,
- )}ms`,
- );
-
- if (workbenchMode) {
- await refreshWorkbenchAfterScanPick();
- }
-
- const totalTime = performance.now() - totalStartTime;
- console.log(
- `✅ [PROCESS OUTSIDE QR END] Total time: ${totalTime.toFixed(
- 2,
- )}ms (${(totalTime / 1000).toFixed(3)}s)`,
- );
- console.log(` End time: ${new Date().toISOString()}`);
- console.log(
- `📊 Breakdown: parse=${parseTime.toFixed(
- 2,
- )}ms, validation=${validationTime.toFixed(
- 2,
- )}ms, duplicateCheck=${duplicateCheckTime.toFixed(
- 2,
- )}ms, lookup=${lookupTime.toFixed(
- 2,
- )}ms, match=${matchTime.toFixed(2)}ms, api=${apiTime.toFixed(
- 2,
- )}ms, stateUpdate=${stateUpdateTime.toFixed(
- 2,
- )}ms, markProcessed=${markProcessedTime.toFixed(2)}ms`,
- );
- console.log(
- workbenchMode
- ? "✅ Workbench scan-pick: list refreshed from server"
- : "✅ Status updated locally, no full data refresh needed",
- );
- } else {
- console.warn("Unexpected response code from backend:", res.code);
- const failMsg =
- (res as { message?: string })?.message ||
- tPick("Workbench scan-pick failed.");
- if (
- shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) &&
- exactMatch
- ) {
- openWorkbenchLotLabelModalForLot(exactMatch, failMsg);
- return;
- }
- if (workbenchMode && exactMatch.stockOutLineId != null) {
- rememberWorkbenchScanReject(
- Number(exactMatch.stockOutLineId),
- failMsg,
- );
- }
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(failMsg);
- });
- }
- } catch (e) {
- const totalTime = performance.now() - totalStartTime;
- console.error(
- `❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(
- 2,
- )}ms`,
- );
- console.error(
- "Error calling updateStockOutLineStatusByQRCodeAndLotNo:",
- e,
- );
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- });
- }
-
- return; // ✅ 直接返回,不需要确认表单
- }
-
- // ✅ Case 2: itemId 匹配但 stockInLineId 不匹配
- // Workbench 策略:不彈窗,直接切換到掃到的批次並提交一次掃描
- const mismatchCheckStartTime = performance.now();
- const itemProcessedSet2 = processedQrCombinations.get(scannedItemId);
- if (itemProcessedSet2?.has(scannedStockInLineId)) {
- const mismatchCheckTime = performance.now() - mismatchCheckStartTime;
- console.log(
- ` [SKIP] Already processed this exact combination (check time: ${mismatchCheckTime.toFixed(
- 2,
- )}ms)`,
- );
- return;
- }
- const mismatchCheckTime = performance.now() - mismatchCheckStartTime;
- console.log(
- ` [PERF] Mismatch check time: ${mismatchCheckTime.toFixed(2)}ms`,
- );
-
- const expectedLotStartTime = performance.now();
- const expectedLot = pickExpectedLotForSubstitution(activeSuggestedLots);
- if (!expectedLot) {
- console.error("Could not determine expected lot for auto-switch");
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- });
- return;
- }
- const expectedLotTime = performance.now() - expectedLotStartTime;
- console.log(
- ` [PERF] Get expected lot time: ${expectedLotTime.toFixed(2)}ms`,
- );
-
- console.log(
- `⚠️ Lot mismatch (auto): Expected stockInLineId=${expectedLot.stockInLineId}, Scanned stockInLineId=${scannedStockInLineId}`,
- );
-
- // 1) 先把掃到的 stockInLineId 轉成 lotNo(workbenchScanPick 需要 lotNo)
- let scannedLotNo: string | null = null;
- try {
- const info = await fetchStockInLineInfoCached(scannedStockInLineId);
- scannedLotNo = info?.lotNo || null;
- } catch (e) {
- console.warn(
- "Failed to fetch lotNo for stockInLineId:",
- scannedStockInLineId,
- e,
- );
- }
- if (!scannedLotNo) {
- const msg = tPick(
- "Cannot resolve lot number from QR. Please rescan or use manual confirmation.",
- );
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(msg);
- return;
- }
-
- // 2) 非 workbench:先 confirmLotSubstitution;workbench 僅依 scan-pick 規則與錯誤訊息
- if (!workbenchMode) {
- const substitutionResult = await confirmLotSubstitution({
- pickOrderLineId: expectedLot.pickOrderLineId,
- stockOutLineId: expectedLot.stockOutLineId,
- originalSuggestedPickLotId: expectedLot.suggestedPickLotId,
- newInventoryLotNo: "",
- newStockInLineId: scannedStockInLineId,
- });
- const substitutionCode = (substitutionResult as any)?.code;
- const switchedToUnavailable =
- substitutionCode === "SUCCESS_UNAVAILABLE" ||
- substitutionCode === "BOUND_UNAVAILABLE";
- if (
- !substitutionResult ||
- (substitutionCode !== "SUCCESS" && !switchedToUnavailable)
- ) {
- const errMsg =
- substitutionResult?.message ||
- tPick("Lot switch failed; pick line was not updated.");
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(errMsg);
- if (expectedLot.stockOutLineId != null) {
- rememberWorkbenchScanReject(
- Number(expectedLot.stockOutLineId),
- errMsg,
- );
- }
- return;
- }
- }
-
- // 3) 提交掃描(workbench:直接 workbenchScanPick)
- try {
- const res = await workbenchScanPick({
- stockOutLineId: expectedLot.stockOutLineId,
- lotNo: scannedLotNo,
- ...(typeof scannedStockInLineId === "number" &&
- Number.isFinite(scannedStockInLineId) &&
- scannedStockInLineId > 0
- ? { stockInLineId: scannedStockInLineId }
- : {}),
- ...workbenchScanPickQtyFromLot(expectedLot),
- storeId: workbenchStoreId,
- userId: currentUserId ?? 1,
- });
-
- const ok = res.code === "SUCCESS";
-
- if (!ok) {
- const failMsg =
- (res as { message?: string })?.message ||
- tPick("Workbench scan-pick failed.");
- if (
- shouldOpenWorkbenchLotLabelModalForFailure(res.code, failMsg) &&
- expectedLot
- ) {
- openWorkbenchLotLabelModalForLot(expectedLot, failMsg);
- return;
- }
- if (workbenchMode && expectedLot.stockOutLineId != null) {
- rememberWorkbenchScanReject(
- Number(expectedLot.stockOutLineId),
- failMsg,
- );
- }
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(failMsg);
- return;
- }
-
- clearWorkbenchScanReject(Number(expectedLot.stockOutLineId));
-
- startTransition(() => {
- setQrScanError(false);
- setQrScanSuccess(true);
- });
-
- setProcessedQrCombinations((prev) => {
- const newMap = new Map(prev);
- if (!newMap.has(scannedItemId))
- newMap.set(scannedItemId, new Set());
- newMap.get(scannedItemId)!.add(scannedStockInLineId);
- return newMap;
- });
-
- if (workbenchMode) {
- await refreshWorkbenchAfterScanPick();
- }
- } catch (e) {
- console.error("Auto-switch scanPick failed:", e);
- setQrScanError(true);
- setQrScanSuccess(false);
- return;
- }
-
- const totalTime = performance.now() - totalStartTime;
- console.log(
- `✅ [PROCESS OUTSIDE QR AUTO-SWITCH] Total time: ${totalTime.toFixed(
- 2,
- )}ms (${(totalTime / 1000).toFixed(3)}s)`,
- );
- console.log(` End time: ${new Date().toISOString()}`);
- console.log(
- `📊 Breakdown: parse=${parseTime.toFixed(
- 2,
- )}ms, validation=${validationTime.toFixed(
- 2,
- )}ms, duplicateCheck=${duplicateCheckTime.toFixed(
- 2,
- )}ms, lookup=${lookupTime.toFixed(2)}ms, match=${matchTime.toFixed(
- 2,
- )}ms, mismatchCheck=${mismatchCheckTime.toFixed(
- 2,
- )}ms, expectedLot=${expectedLotTime.toFixed(2)}ms`,
- );
- } catch (error) {
- const totalTime = performance.now() - totalStartTime;
- console.error(
- `❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`,
- );
- console.error("Error during QR code processing:", error);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- });
- return;
- }
- },
- [
- lotDataIndexes,
- processedQrCombinations,
- combinedLotData,
- fetchStockInLineInfoCached,
- workbenchMode,
- currentUserId,
- clearWorkbenchScanReject,
- rememberWorkbenchScanReject,
- refreshWorkbenchAfterScanPick,
- workbenchScanPickQtyFromLot,
- tPick,
- workbenchStoreId,
- confirmLotSubstitution,
- openWorkbenchLotLabelModalForLot,
- shouldOpenWorkbenchLotLabelModalForFailure,
- ],
- );
-
- // Store in refs for immediate access in qrValues effect
- processOutsideQrCodeRef.current = processOutsideQrCode;
- resetScanRef.current = resetScan;
-
- const handleManualInputSubmit = useCallback(() => {
- if (qrScanInput.trim() !== "") {
- handleQrCodeSubmit(qrScanInput.trim());
- }
- }, [qrScanInput, handleQrCodeSubmit]);
-
- // Handle QR code submission from modal (internal scanning)
- const handleQrCodeSubmitFromModal = useCallback(
- async (lotNo: string) => {
- if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
- console.log(` QR Code verified for lot: ${lotNo}`);
-
- const requiredQty = selectedLotForQr.requiredQty;
- const lotId = selectedLotForQr.lotId;
-
- // Create stock out line
- const stockOutLineData: CreateStockOutLine = {
- consoCode: selectedLotForQr.pickOrderConsoCode,
- pickOrderLineId: selectedLotForQr.pickOrderLineId,
- inventoryLotLineId: selectedLotForQr.lotId,
- qty: 0.0,
- };
-
- try {
- await createStockOutLine(stockOutLineData);
- console.log("Stock out line created successfully!");
-
- // Close modal
- setQrModalOpen(false);
- setSelectedLotForQr(null);
-
- // Set pick quantity
- const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
- setTimeout(() => {
- setPickQtyData((prev) => ({
- ...prev,
- [lotKey]: requiredQty,
- }));
- console.log(
- ` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`,
- );
- }, 500);
-
- // Refresh data
- const pickOrderId = filterArgs?.pickOrderId
- ? Number(filterArgs.pickOrderId)
- : undefined;
- await fetchJobOrderData(pickOrderId);
- } catch (error) {
- console.error("Error creating stock out line:", error);
- }
- }
- },
- [selectedLotForQr, fetchJobOrderData],
- );
-
- useEffect(() => {
- // Skip if scanner not active or no data or currently refreshing
- if (
- !isManualScanning ||
- qrValues.length === 0 ||
- combinedLotData.length === 0 ||
- isRefreshingData
- )
- return;
-
- const latestQr = qrValues[qrValues.length - 1];
-
- // ✅ Test shortcut: {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId
- if (
- (latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) &&
- latestQr.endsWith("}")
- ) {
- let content = "";
- if (latestQr.startsWith("{2fittest"))
- content = latestQr.substring(9, latestQr.length - 1);
- else content = latestQr.substring(8, latestQr.length - 1);
-
- const parts = content.split(",");
- if (parts.length === 2) {
- const itemId = parseInt(parts[0].trim(), 10);
- const stockInLineId = parseInt(parts[1].trim(), 10);
- if (!isNaN(itemId) && !isNaN(stockInLineId)) {
- const simulatedQr = JSON.stringify({ itemId, stockInLineId });
-
- lastProcessedQrRef.current = latestQr;
- processedQrCodesRef.current.add(latestQr);
- setLastProcessedQr(latestQr);
- setProcessedQrCodes(new Set(processedQrCodesRef.current));
-
- processOutsideQrCodeRef.current?.(simulatedQr);
- resetScanRef.current?.();
- return;
- }
- }
- }
-
- // ✅ Shortcut: {2fic} open manual lot confirmation modal
- if (latestQr === "{2fic}") {
- setManualLotConfirmationOpen(true);
- resetScanRef.current?.();
- lastProcessedQrRef.current = latestQr;
- processedQrCodesRef.current.add(latestQr);
- setLastProcessedQr(latestQr);
- setProcessedQrCodes(new Set(processedQrCodesRef.current));
- return;
- }
-
- // Skip processing if manual modal open for same QR
- if (manualLotConfirmationOpen) {
- if (latestQr === lastProcessedQrRef.current) return;
- }
-
- // Skip if already processed (refs)
- if (
- processedQrCodesRef.current.has(latestQr) ||
- lastProcessedQrRef.current === latestQr
- )
- return;
-
- // Mark processed immediately
- lastProcessedQrRef.current = latestQr;
- processedQrCodesRef.current.add(latestQr);
- if (processedQrCodesRef.current.size > 100) {
- const firstValue = processedQrCodesRef.current.values().next().value;
- if (firstValue !== undefined)
- processedQrCodesRef.current.delete(firstValue);
- }
-
- // Process immediately
- if (qrProcessingTimeoutRef.current) {
- clearTimeout(qrProcessingTimeoutRef.current);
- qrProcessingTimeoutRef.current = null;
- }
-
- processOutsideQrCodeRef.current?.(latestQr);
-
- // UI state updates (non-blocking)
- startTransition(() => {
- setLastProcessedQr(latestQr);
- setProcessedQrCodes(new Set(processedQrCodesRef.current));
- });
-
- return () => {
- if (qrProcessingTimeoutRef.current) {
- clearTimeout(qrProcessingTimeoutRef.current);
- qrProcessingTimeoutRef.current = null;
- }
- };
- }, [
- qrValues.length,
- isManualScanning,
- isRefreshingData,
- combinedLotData.length,
- manualLotConfirmationOpen,
- isConfirmingLot,
- ]);
-
- const handlePickQtyChange = useCallback(
- (lotKey: string, value: number | string) => {
- if (value === "" || value === null || value === undefined) {
- setPickQtyData((prev) => ({
- ...prev,
- [lotKey]: 0,
- }));
- return;
- }
-
- const numericValue =
- typeof value === "string" ? parseFloat(value) : value;
-
- if (isNaN(numericValue)) {
- setPickQtyData((prev) => ({
- ...prev,
- [lotKey]: 0,
- }));
- return;
- }
-
- setPickQtyData((prev) => ({
- ...prev,
- [lotKey]: numericValue,
- }));
- },
- [],
- );
-
- const checkAndAutoAssignNext = useCallback(async () => {
- if (!currentUserId) return;
-
- try {
- const completionResponse = await checkPickOrderCompletion(currentUserId);
-
- if (
- completionResponse.code === "SUCCESS" &&
- completionResponse.entity?.hasCompletedOrders
- ) {
- console.log("Found completed pick orders, auto-assigning next...");
- // 移除前端的自动分配逻辑,因为后端已经处理了
- // await handleAutoAssignAndRelease(); // 删除这个函数
- }
- } catch (error) {
- console.error("Error checking pick order completion:", error);
- }
- }, [currentUserId]);
-
- const handleSubmitPickQtyWithQty = useCallback(
- async (
- lot: any,
- submitQty: number,
- source: "justComplete" | "singleSubmit",
- ) => {
- if (!lot.stockOutLineId) {
- console.error("No stock out line found for this lot");
- return;
- }
- const solId = Number(lot.stockOutLineId);
- if (solId > 0 && actionBusyBySolId[solId]) {
- console.warn("Action already in progress for stockOutLineId:", solId);
- return;
- }
-
- const pickOrderIdForRefresh = filterArgs?.pickOrderId
- ? Number(filterArgs.pickOrderId)
- : undefined;
-
- try {
- if (solId > 0)
- setActionBusyBySolId((prev) => ({ ...prev, [solId]: true }));
- const targetUnavailable = isInventoryLotLineUnavailable(lot);
- const effectiveSubmitQty =
- targetUnavailable && submitQty > 0 ? 0 : submitQty;
-
- const canonicalLotForSol =
- solId > 0
- ? combinedLotData.find((r) => Number(r.stockOutLineId) === solId) ??
- lot
- : lot;
-
- if (workbenchMode && source === "justComplete") {
- const solIdForOverride =
- Number(canonicalLotForSol.stockOutLineId) || 0;
- const lotIdForOverride = canonicalLotForSol.lotId;
- const lotKeyForOverride =
- Number.isFinite(solIdForOverride) && solIdForOverride > 0
- ? `sol:${solIdForOverride}`
- : `${canonicalLotForSol.pickOrderLineId}-${lotIdForOverride}`;
- const hasExplicitSubmitOverride =
- Object.prototype.hasOwnProperty.call(
- pickQtyData,
- lotKeyForOverride,
- );
- const explicitSubmitOverride = hasExplicitSubmitOverride
- ? Number(pickQtyData[lotKeyForOverride])
- : NaN;
-
- const qtyPayload = workbenchScanPickQtyFromLot(canonicalLotForSol);
- const wbJustQty = qtyPayload.qty;
- const isUnavailableForJustComplete =
- isInventoryLotLineUnavailable(canonicalLotForSol);
- const isNoLotForJustComplete =
- canonicalLotForSol.noLot === true ||
- !String(canonicalLotForSol.lotNo ?? "").trim();
- const canPostScanPick =
- // unavailable lot: Just Completed must always submit qty=0, even without lotNo
- isUnavailableForJustComplete ||
- isLotAvailabilityExpired(canonicalLotForSol) ||
- // noLot row: Just Completed always submit qty=0
- isNoLotForJustComplete ||
- (canonicalLotForSol.lotNo &&
- String(canonicalLotForSol.lotNo).trim() !== "" &&
- ((hasExplicitSubmitOverride &&
- Number.isFinite(explicitSubmitOverride) &&
- explicitSubmitOverride === 0) ||
- (wbJustQty != null && wbJustQty > 0)));
-
- if (canPostScanPick) {
- const qtyToSend =
- isUnavailableForJustComplete
- ? 0
- : isLotAvailabilityExpired(canonicalLotForSol)
- ? 0
- : isNoLotForJustComplete
- ? 0
- : hasExplicitSubmitOverride && explicitSubmitOverride === 0
- ? 0
- : Number(wbJustQty);
- const res = await workbenchScanPick({
- stockOutLineId: Number(canonicalLotForSol.stockOutLineId),
- lotNo: String(canonicalLotForSol.lotNo).trim(),
- ...(typeof canonicalLotForSol.stockInLineId === "number" &&
- Number.isFinite(canonicalLotForSol.stockInLineId) &&
- canonicalLotForSol.stockInLineId > 0
- ? { stockInLineId: canonicalLotForSol.stockInLineId }
- : {}),
- qty: qtyToSend,
- storeId: workbenchStoreId,
- userId: currentUserId ?? 1,
- });
- const scanOk = res.code === "SUCCESS";
- if (!scanOk) {
- rememberWorkbenchScanReject(
- Number(canonicalLotForSol.stockOutLineId),
- (res as { message?: string })?.message,
- );
- throw new Error(
- (res as { message?: string })?.message ||
- "Workbench scan-pick failed",
- );
- }
- clearWorkbenchScanReject(Number(canonicalLotForSol.stockOutLineId));
- setPickQtyData((prev) => {
- if (!Object.prototype.hasOwnProperty.call(prev, lotKeyForOverride))
- return prev;
- const next = { ...prev };
- delete next[lotKeyForOverride];
- return next;
- });
- await refreshWorkbenchAfterScanPick();
- setTimeout(() => {
- checkAndAutoAssignNext();
- }, 1000);
- console.log(
- "Just Completed (workbench): workbenchScanPick posted without QR.",
- );
- return;
- }
- const justCompleteErr = t(
- "Just Completed (workbench): requires valid quantity; expired rows must not use this button.",
- );
- if (solId > 0) {
- rememberWorkbenchScanReject(solId, justCompleteErr);
- }
- setQrScanErrorMsg(justCompleteErr);
- throw new Error(justCompleteErr);
- }
-
- if (effectiveSubmitQty === 0 && source === "singleSubmit") {
- console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
- console.log(`Lot: ${lot.lotNo}`);
- console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
-
- if (workbenchMode) {
- const res = await workbenchScanPick({
- stockOutLineId: Number(lot.stockOutLineId),
- lotNo: String(lot.lotNo ?? "").trim(),
- ...(typeof lot.stockInLineId === "number" &&
- Number.isFinite(lot.stockInLineId) &&
- lot.stockInLineId > 0
- ? { stockInLineId: lot.stockInLineId }
- : {}),
- qty: 0,
- storeId: workbenchStoreId,
- userId: currentUserId ?? 1,
- });
- const scanOk = res.code === "SUCCESS";
- if (!scanOk) {
- rememberWorkbenchScanReject(
- Number(lot.stockOutLineId),
- (res as { message?: string })?.message,
- );
- throw new Error(
- (res as { message?: string })?.message ||
- "Workbench scan-pick failed (qty=0)",
- );
- }
- clearWorkbenchScanReject(Number(lot.stockOutLineId));
- await refreshWorkbenchAfterScanPick();
- setTimeout(() => {
- checkAndAutoAssignNext();
- }, 1000);
- return;
- }
-
- throw new Error("Unsupported legacy checked flow on workbench page");
- }
-
- if (
- workbenchMode &&
- effectiveSubmitQty > 0 &&
- lot.lotNo &&
- String(lot.lotNo).trim() !== "" &&
- !isLotAvailabilityExpired(lot) &&
- !isInventoryLotLineUnavailable(lot)
- ) {
- const res = await workbenchScanPick({
- stockOutLineId: Number(lot.stockOutLineId),
- lotNo: String(lot.lotNo).trim(),
- ...(typeof lot.stockInLineId === "number" &&
- Number.isFinite(lot.stockInLineId) &&
- lot.stockInLineId > 0
- ? { stockInLineId: lot.stockInLineId }
- : {}),
- qty: Number(effectiveSubmitQty),
- storeId: workbenchStoreId,
- userId: currentUserId ?? 1,
- });
- const scanOk = res.code === "SUCCESS";
- if (!scanOk) {
- rememberWorkbenchScanReject(
- Number(lot.stockOutLineId),
- (res as { message?: string })?.message,
- );
- throw new Error(
- (res as { message?: string })?.message ||
- "Workbench scan-pick failed",
- );
- }
- clearWorkbenchScanReject(Number(lot.stockOutLineId));
- const successLotKey = getWorkbenchQtyLotKey(lot);
- setPickQtyData((prev) => {
- if (!Object.prototype.hasOwnProperty.call(prev, successLotKey))
- return prev;
- const next = { ...prev };
- delete next[successLotKey];
- return next;
- });
- await refreshWorkbenchAfterScanPick();
- setTimeout(() => {
- checkAndAutoAssignNext();
- }, 1000);
- return;
- }
-
- const currentActualPickQty = lot.actualPickQty || 0;
- const cumulativeQty = currentActualPickQty + effectiveSubmitQty;
-
- let newStatus = "partially_completed";
-
- if (cumulativeQty >= lot.requiredQty) {
- newStatus = "completed";
- } else if (cumulativeQty > 0) {
- newStatus = "partially_completed";
- } else {
- newStatus = "pending";
- }
-
- console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
- console.log(`Lot: ${lot.lotNo}`);
- console.log(`Required Qty: ${lot.requiredQty}`);
- console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
- console.log(`New Submitted Qty: ${effectiveSubmitQty}`);
- console.log(`Cumulative Qty: ${cumulativeQty}`);
- console.log(`New Status: ${newStatus}`);
- console.log(`=====================================`);
-
- if (!workbenchMode) {
- await updateStockOutLineStatus({
- id: lot.stockOutLineId,
- status: newStatus,
- qty: effectiveSubmitQty,
- });
- applyLocalStockOutLineUpdate(
- Number(lot.stockOutLineId),
- newStatus,
- cumulativeQty,
- );
- }
-
- // Workbench completion is handled in backend scan-pick flow.
-
- void fetchJobOrderData(pickOrderIdForRefresh);
- console.log("Pick quantity submitted successfully!");
-
- setTimeout(() => {
- checkAndAutoAssignNext();
- }, 1000);
- } catch (error) {
- console.error("Error submitting pick quantity:", error);
- setQrScanError(true);
- setQrScanSuccess(false);
- } finally {
- if (solId > 0)
- setActionBusyBySolId((prev) => ({ ...prev, [solId]: false }));
- }
- },
- [
- fetchJobOrderData,
- checkAndAutoAssignNext,
- actionBusyBySolId,
- applyLocalStockOutLineUpdate,
- workbenchMode,
- currentUserId,
- rememberWorkbenchScanReject,
- clearWorkbenchScanReject,
- refreshWorkbenchAfterScanPick,
- combinedLotData,
- workbenchScanPickQtyFromLot,
- pickQtyData,
- tPick,
- workbenchStoreId,
- filterArgs?.pickOrderId,
- ],
- );
-
- const handleSkip = useCallback(
- async (lot: any) => {
- try {
- console.log(
- "Just Complete clicked (workbench: scan-pick without QR when possible):",
- lot.lotNo,
- );
- await handleSubmitPickQtyWithQty(lot, 0, "justComplete");
- } catch (err) {
- console.error("Error in Skip:", err);
- }
- },
- [handleSubmitPickQtyWithQty],
- );
-
- const hasPendingBatchSubmit = useMemo(() => {
- return combinedLotData.some((lot) => {
- const status = String(lot.stockOutLineStatus || "").toLowerCase();
- return (
- status === "pending" ||
- status === "partially_completed" ||
- status === "partially_complete"
- );
- });
- }, [combinedLotData]);
-
- useEffect(() => {
- if (!hasPendingBatchSubmit) return;
- const handler = (event: BeforeUnloadEvent) => {
- event.preventDefault();
- event.returnValue = "";
- };
- window.addEventListener("beforeunload", handler);
- return () => window.removeEventListener("beforeunload", handler);
- }, [hasPendingBatchSubmit]);
-
- const handleSubmitAllScanned = useCallback(async () => {
- const startTime = performance.now();
- console.log(` [BATCH SUBMIT START]`);
- console.log(` Start time: ${new Date().toISOString()}`);
-
- const scannedLots = combinedLotData.filter((lot) => {
- const status = lot.stockOutLineStatus;
- const statusLower = String(status || "").toLowerCase();
- if (statusLower === "completed" || statusLower === "complete") {
- return false;
- }
- if (
- lot.noLot === true ||
- isLotAvailabilityExpired(lot) ||
- isInventoryLotLineUnavailable(lot)
- ) {
- return true;
- }
- return false;
- });
-
- if (scannedLots.length === 0) {
- console.log("No scanned items to submit");
- return;
- }
-
- setIsSubmittingAll(true);
- console.log(
- `📦 Submitting ${scannedLots.length} items using workbench batch scan-pick (qty=0)...`,
- );
-
- try {
- const submitStartTime = performance.now();
-
- const result = await workbenchBatchScanPick({
- lines: scannedLots.map((lot) => ({
- stockOutLineId: Number(lot.stockOutLineId) || 0,
- lotNo: "",
- qty: 0,
- storeId: workbenchStoreId,
- userId: currentUserId ?? 1,
- })),
- });
-
- const submitTime = performance.now() - submitStartTime;
- console.log(
- ` Batch submit API call completed in ${submitTime.toFixed(2)}ms (${(
- submitTime / 1000
- ).toFixed(3)}s)`,
- );
- console.log(`📥 Batch submit result:`, result);
-
- const refreshStartTime = performance.now();
- const pickOrderId = filterArgs?.pickOrderId
- ? Number(filterArgs.pickOrderId)
- : undefined;
- await fetchJobOrderData(pickOrderId);
- const refreshTime = performance.now() - refreshStartTime;
- console.log(
- ` Data refresh time: ${refreshTime.toFixed(2)}ms (${(
- refreshTime / 1000
- ).toFixed(3)}s)`,
- );
-
- const totalTime = performance.now() - startTime;
- console.log(` [BATCH SUBMIT END]`);
- console.log(
- ` Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(
- 3,
- )}s)`,
- );
- console.log(` End time: ${new Date().toISOString()}`);
-
- if (result && result.code === "SUCCESS") {
- setQrScanSuccess(true);
- setTimeout(() => {
- setQrScanSuccess(false);
- checkAndAutoAssignNext();
- if (onBackToList) {
- onBackToList();
- }
- }, 2000);
- } else {
- console.error("Batch submit failed:", result);
- setQrScanError(true);
- }
- } catch (error) {
- console.error("Error submitting all scanned items:", error);
- setQrScanError(true);
- } finally {
- setIsSubmittingAll(false);
- }
- }, [
- combinedLotData,
- fetchJobOrderData,
- checkAndAutoAssignNext,
- currentUserId,
- onBackToList,
- workbenchStoreId,
- filterArgs?.pickOrderId,
- ]);
-
- const scannedItemsCount = useMemo(() => {
- const filtered = combinedLotData.filter((lot) => {
- const status = lot.stockOutLineStatus;
- const statusLower = String(status || "").toLowerCase();
- if (statusLower === "completed" || statusLower === "complete") {
- return false;
- }
- if (
- lot.noLot === true ||
- isLotAvailabilityExpired(lot) ||
- isInventoryLotLineUnavailable(lot)
- ) {
- return true;
- }
- return false;
- });
-
- const noLotCount = filtered.filter((l) => l.noLot === true).length;
- const normalCount = filtered.filter((l) => l.noLot !== true).length;
- console.log(
- `📊 scannedItemsCount calculation: total=${filtered.length}, noLot=${noLotCount}, normal=${normalCount}`,
- );
- console.log(`📊 All items breakdown:`, {
- total: combinedLotData.length,
- noLot: combinedLotData.filter((l) => l.noLot === true).length,
- normal: combinedLotData.filter((l) => l.noLot !== true).length,
- });
-
- return filtered.length;
- }, [combinedLotData]);
-
- // 先定义 filteredByFloor 和 availableFloors
- const availableFloors = useMemo(() => {
- const floors = new Set<string>();
- combinedLotData.forEach((lot) => {
- const f = extractFloor(lot);
- if (f) floors.add(f);
- });
- return Array.from(floors).sort(
- (a, b) => floorSortOrder(b) - floorSortOrder(a),
- );
- }, [combinedLotData]);
-
- const filteredByFloor = useMemo(() => {
- if (!selectedFloor) return combinedLotData;
- return combinedLotData.filter((lot) => extractFloor(lot) === selectedFloor);
- }, [combinedLotData, selectedFloor]);
-
- // 與批量篩選一致:noLot / 過期 的 pending 也算已處理(對齊 GoodPickExecutiondetail)
- const progress = useMemo(() => {
- const data = selectedFloor ? filteredByFloor : combinedLotData;
- if (data.length === 0) return { completed: 0, total: 0 };
- const nonPendingCount = data.filter((lot) => {
- const status = lot.stockOutLineStatus?.toLowerCase();
- if (status !== "pending") return true;
- if (
- lot.noLot === true ||
- isLotAvailabilityExpired(lot) ||
- isInventoryLotLineUnavailable(lot)
- )
- return true;
- return false;
- }).length;
- return { completed: nonPendingCount, total: data.length };
- }, [selectedFloor, filteredByFloor, combinedLotData]);
- // Handle reject lot
- const handleRejectLot = useCallback(
- async (lot: any) => {
- if (!lot.stockOutLineId) {
- console.error("No stock out line found for this lot");
- return;
- }
-
- try {
- await updateStockOutLineStatus({
- id: lot.stockOutLineId,
- status: "rejected",
- qty: 0,
- });
-
- const pickOrderId = filterArgs?.pickOrderId
- ? Number(filterArgs.pickOrderId)
- : undefined;
- await fetchJobOrderData(pickOrderId);
- console.log("Lot rejected successfully!");
-
- setTimeout(() => {
- checkAndAutoAssignNext();
- }, 1000);
- } catch (error) {
- console.error("Error rejecting lot:", error);
- }
- },
- [fetchJobOrderData, checkAndAutoAssignNext],
- );
-
- // Handle pick execution form
- const handlePickExecutionForm = useCallback((lot: any) => {
- console.log("=== Pick Execution Form ===");
- console.log("Lot data:", lot);
-
- if (!lot) {
- console.warn("No lot data provided for pick execution form");
- return;
- }
-
- console.log("Opening pick execution form for lot:", lot.lotNo);
-
- setSelectedLotForExecutionForm(lot);
- setPickExecutionFormOpen(true);
-
- console.log("Pick execution form opened for lot ID:", lot.lotId);
- }, []);
-
-
- // Calculate remaining required quantity
- const calculateRemainingRequiredQty = useCallback((lot: any) => {
- const requiredQty = lot.requiredQty || 0;
- const stockOutLineQty = lot.stockOutLineQty || 0;
- return Math.max(0, requiredQty - stockOutLineQty);
- }, []);
-
-
-
- const handlePageChange = useCallback((event: unknown, newPage: number) => {
- setPaginationController((prev) => ({
- ...prev,
- pageNum: newPage,
- }));
- }, []);
-
- const handlePageSizeChange = useCallback(
- (event: React.ChangeEvent<HTMLInputElement>) => {
- const newPageSize = parseInt(event.target.value, 10);
- setPaginationController({
- pageNum: 0,
- pageSize: newPageSize,
- });
- },
- [],
- );
-
- // Pagination data: align DO workbench grouping display
- const paginatedData = useMemo(() => {
- const sourceData = selectedFloor ? filteredByFloor : combinedLotData;
-
- /** 同 pick_order_line 內「無庫位 / no-lot」列 extractFloor 為空,若仍用 0 會被排到全表最後,編號欄也變成新群組。繼承該行最大樓層權重並以 polId 相鄰排序。 */
- const lineMaxFloorOrder = new Map<number, number>();
- for (const lot of sourceData) {
- const polId = Number(lot.pickOrderLineId);
- if (!Number.isFinite(polId) || polId <= 0) continue;
- const o = floorSortOrder(extractFloor(lot));
- const prev = lineMaxFloorOrder.get(polId) ?? 0;
- if (o > prev) lineMaxFloorOrder.set(polId, o);
- }
- const effectiveFloorOrder = (lot: any): number => {
- const own = floorSortOrder(extractFloor(lot));
- if (own > 0) return own;
- const polId = Number(lot.pickOrderLineId);
- if (Number.isFinite(polId) && polId > 0) {
- const inherited = lineMaxFloorOrder.get(polId);
- if (inherited != null && inherited > 0) return inherited;
- }
- return 0;
- };
- const isNoLotTailRow = (lot: any) =>
- lot.noLot === true || lot.lotId == null || lot.lotId === undefined;
-
- const sortedData = [...sourceData].sort((a, b) => {
- const efA = effectiveFloorOrder(a);
- const efB = effectiveFloorOrder(b);
- if (efA !== efB) return efB - efA; // 4F, 3F, 2F(含繼承樓層的缺口列)
-
- const polA = Number(a.pickOrderLineId);
- const polB = Number(b.pickOrderLineId);
- const hasPolA = Number.isFinite(polA) && polA > 0;
- const hasPolB = Number.isFinite(polB) && polB > 0;
- if (hasPolA && hasPolB && polA !== polB) return polA - polB;
-
- const aItem = String(a.itemCode || "");
- const bItem = String(b.itemCode || "");
- if (aItem !== bItem) return aItem.localeCompare(bItem);
-
- const aName = String(a.itemName || "");
- const bName = String(b.itemName || "");
- if (aName !== bName) return aName.localeCompare(bName);
-
- const tailA = isNoLotTailRow(a) ? 1 : 0;
- const tailB = isNoLotTailRow(b) ? 1 : 0;
- if (tailA !== tailB) return tailA - tailB;
-
- const aIndex = Number(a.routerIndex ?? 0);
- const bIndex = Number(b.routerIndex ?? 0);
- if (aIndex !== bIndex) return aIndex - bIndex;
-
- return (a.lotNo || "").localeCompare(b.lotNo || "");
- });
-
- const flattened = sortedData.map((lot, idx, arr) => {
- const key = `${lot.itemId ?? "null"}-${lot.itemCode ?? ""}`;
- const prev = idx > 0 ? arr[idx - 1] : undefined;
- const prevKey = prev
- ? `${prev.itemId ?? "null"}-${prev.itemCode ?? ""}`
- : null;
- const isGroupFirst = key !== prevKey;
- const groupDisplayIndex =
- arr
- .slice(0, idx + 1)
- .filter((row, rowIdx, all) => {
- const rowKey = `${row.itemId ?? "null"}-${row.itemCode ?? ""}`;
- const before = rowIdx > 0 ? all[rowIdx - 1] : undefined;
- const beforeKey = before
- ? `${before.itemId ?? "null"}-${before.itemCode ?? ""}`
- : null;
- return rowKey !== beforeKey;
- }).length;
- return { lot, isGroupFirst, groupDisplayIndex };
- });
-
- const startIndex =
- paginationController.pageNum * paginationController.pageSize;
- const endIndex = startIndex + paginationController.pageSize;
- return flattened.slice(startIndex, endIndex);
- }, [selectedFloor, filteredByFloor, combinedLotData, paginationController]);
-
- // Add these functions for manual scanning
- const handleStartScan = useCallback(() => {
- console.log(" Starting manual QR scan...");
- setIsManualScanning(true);
- setProcessedQrCodes(new Set());
- setLastProcessedQr("");
- setQrScanError(false);
- setQrScanSuccess(false);
- startScan();
- }, [startScan]);
-
- const handleStopScan = useCallback(() => {
- console.log(" Stopping manual QR scan...");
- setIsManualScanning(false);
- setQrScanError(false);
- setQrScanSuccess(false);
- stopScan();
- resetScan();
- }, [stopScan, resetScan]);
- useEffect(() => {
- return () => {
- // Cleanup when component unmounts (e.g., when switching tabs)
- if (isManualScanning) {
- console.log("🧹 Component unmounting, stopping QR scanner...");
- stopScan();
- resetScan();
- }
- };
- }, [isManualScanning, stopScan, resetScan]);
- // 勿在 combinedLotData 仍為空時自動停掃:API 未回傳前會誤觸,與 GoodPickExecutiondetail(已註解掉同段)一致。
- // 無資料時 qrValues effect 本來就不會處理掃碼;真正無單據可再手動按停止。
-
- const getStatusMessage = useCallback(
- (lot: any) => {
- if (
- lot?.noLot === true ||
- lot?.lotAvailability === "insufficient_stock"
- ) {
- return t("This order is insufficient, please pick another lot.");
- }
- switch (lot.stockOutLineStatus?.toLowerCase()) {
- case "pending":
- return t("Please finish QR code scan and pick order.");
- case "checked":
- return t("Please submit the pick order.");
- case "partially_completed":
- return t(
- "Partial quantity submitted. Please submit more or complete the order.",
- );
- case "completed":
- return t("Pick order completed successfully!");
- case "rejected":
- return t("Lot has been rejected and marked as unavailable.");
- case "unavailable":
- return t("This order is insufficient, please pick another lot.");
- default:
- return t("Please finish QR code scan and pick order.");
- }
- },
- [t],
- );
-
- return (
- <TestQrCodeProvider
- lotData={combinedLotData}
- onScanLot={handleQrCodeSubmit}
- filterActive={(lot) =>
- lot.lotAvailability !== "rejected" &&
- lot.stockOutLineStatus !== "rejected" &&
- lot.stockOutLineStatus !== "completed"
- }
- >
- <FormProvider {...formProps}>
- <Stack spacing={2}>
- {/* Progress bar + scan status fixed at top */}
- <Box
- sx={{
- position: "fixed",
- top: 0,
- left: 0,
- right: 0,
- zIndex: 1100,
- backgroundColor: "background.paper",
- pt: 2,
- pb: 1,
- px: 2,
- borderBottom: "1px solid",
- borderColor: "divider",
- boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
- }}
- >
- <LinearProgressWithLabel
- completed={progress.completed}
- total={progress.total}
- label={t("Progress")}
- />
- <ScanStatusAlert
- error={qrScanError}
- success={qrScanSuccess}
- errorMessage={
- qrScanErrorMsg ||
- t("QR code does not match any item in current orders.")
- }
- successMessage={t("QR code verified.")}
- />
- </Box>
- <Box
- sx={{
- display: "flex",
- gap: 1,
- alignItems: "center",
- flexWrap: "wrap",
- }}
- >
- <Button
- variant={selectedFloor === null ? "contained" : "outlined"}
- size="small"
- onClick={() => setSelectedFloor(null)}
- >
- {t("All")}
- </Button>
- {availableFloors.map((floor) => (
- <Button
- key={floor}
- variant={selectedFloor === floor ? "contained" : "outlined"}
- size="small"
- onClick={() => setSelectedFloor(floor)}
- >
- {floor}
- </Button>
- ))}
- </Box>
- <Box sx={{ display: "flex", gap: 1, alignItems: "center", flexWrap: "wrap" }}>
-
-
- <Typography variant="body2" sx={{ minWidth: "fit-content", mr: 1 }}>
- {t("Select Printer")}:
- </Typography>
- <Autocomplete
- options={printerOptions}
- getOptionLabel={(option) =>
- option.name || option.label || option.code || `Printer ${option.id}`
- }
- value={selectedPrinter}
- onChange={(_, newValue) => setSelectedPrinter(newValue)}
- sx={{ minWidth: 220 }}
- size="small"
- renderInput={(params) => (
- <TextField
- {...params}
- placeholder={t("Printer")}
- inputProps={{ ...params.inputProps, readOnly: true }}
- />
- )}
- />
- <Typography variant="body2" sx={{ minWidth: "fit-content", ml: 1 }}>
- {t("Print Quantity")}:
- </Typography>
- <TextField
- type="number"
- label={t("Print Quantity")}
- value={printQty}
- onChange={(e) => {
- const value = parseInt(e.target.value) || 1;
- setPrintQty(Math.max(1, value));
- }}
- inputProps={{ min: 1, step: 1 }}
- sx={{ width: 120 }}
- size="small"
- />
-
- <Button
- variant="contained"
- color="primary"
- size="small"
- onClick={() => handlePickRecord("ALL")}
- >
- {t("Print Pick Record")} ALL
- </Button>
- <Button
- variant="contained"
- color="primary"
- size="small"
- onClick={() => handlePickRecord("2F")}
- >
- {t("Print Pick Record")} 2F
- </Button>
- <Button
- variant="contained"
- color="primary"
- size="small"
- onClick={() => handlePickRecord("3F")}
- >
- {t("Print Pick Record")} 3F
- </Button>
- <Button
- variant="contained"
- color="primary"
- size="small"
- onClick={() => handlePickRecord("4F")}
- >
- {t("Print Pick Record")} 4F
- </Button>
- {isPrinterComboMissing && (
- <Alert severity="warning" sx={{ py: 0, px: 1 }}>
- {t("Printer list is empty")}
- </Alert>
- )}
- </Box>
- {/* Job Order Header */}
- {jobOrderData && (
- <Paper sx={{ p: 2 }}>
- <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
- <Typography variant="subtitle1">
- <strong>{t("Item Name")}:</strong>{" "}
- {jobOrderData.pickOrder.jobOrder.itemCode || "-"}{" "}{jobOrderData.pickOrder.jobOrder.itemName || "-"}
- </Typography>
- <Typography variant="subtitle1">
- <strong>{t("Job Order")}:</strong>{" "}
- {jobOrderData.pickOrder?.jobOrder?.code || "-"}
- </Typography>
- <Typography variant="subtitle1">
- <strong>{t("Pick Order Code")}:</strong>{" "}
- {jobOrderData.pickOrder?.code || "-"}
- </Typography>
- <Typography variant="subtitle1">
- <strong>{t("Target Date")}:</strong>{" "}
- {jobOrderData.pickOrder?.targetDate || "-"}
- </Typography>
- </Stack>
- </Paper>
- )}
-
- {/* Combined Lot Table */}
- <Box sx={{ mt: 10 }}>
- <Box
- sx={{
- display: "flex",
- justifyContent: "space-between",
- alignItems: "center",
- mb: 2,
- }}
- >
- <Box sx={{ display: "flex", gap: 2, alignItems: "center" }}>
- {!isManualScanning ? (
- <Button
- variant="contained"
- startIcon={<QrCodeIcon />}
- onClick={handleStartScan}
- color="primary"
- sx={{ minWidth: "120px" }}
- >
- {t("Start QR Scan")}
- </Button>
- ) : (
- <Button
- variant="outlined"
- startIcon={<QrCodeIcon />}
- onClick={handleStopScan}
- color="secondary"
- sx={{ minWidth: "120px" }}
- >
- {t("Stop QR Scan")}
- </Button>
- )}
- {/* ADD THIS: Submit All Scanned Button */}
- <Button
- variant="contained"
- color="success"
- onClick={handleSubmitAllScanned}
- disabled={scannedItemsCount === 0 || isSubmittingAll}
- sx={{ minWidth: "160px" }}
- >
- {isSubmittingAll ? (
- <>
- <CircularProgress size={16} sx={{ mr: 1 }} />
- {t("Submitting...")}
- </>
- ) : (
- `${t("Submit All Scanned")} (${scannedItemsCount})`
- )}
- </Button>
- </Box>
- </Box>
-
- <TableContainer component={Paper}>
- <Table>
- <TableHead>
- <TableRow>
- <TableCell>{t("Index")}</TableCell>
- <TableCell>{t("Item Code")}</TableCell>
- <TableCell>{t("Route")}</TableCell>
- <TableCell>{t("Handler")}</TableCell>
-
- <TableCell>{t("Lot No")}</TableCell>
- <TableCell align="right">
- {t("Lot Required Pick Qty")}
- </TableCell>
- <TableCell align="right">{t("Available Qty")}</TableCell>
- <TableCell align="center">{t("Scan Result")}</TableCell>
- <TableCell align="center">{t("Qty will submit")}</TableCell>
- <TableCell align="center">
- {t("Submit Required Pick Qty")}
- </TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {paginatedData.length === 0 ? (
- <TableRow>
- <TableCell colSpan={11} align="center">
- <Typography variant="body2" color="text.secondary">
- {t("No data available")}
- </Typography>
- </TableCell>
- </TableRow>
- ) : (
- paginatedData.map((row) => {
- const lot = row.lot;
- const solIdForKey = Number(lot.stockOutLineId) || 0;
- const lotKeyForSubmitQty =
- Number.isFinite(solIdForKey) && solIdForKey > 0
- ? `sol:${solIdForKey}`
- : `${lot.pickOrderLineId}-${lot.lotId}`;
- const submitQtyStatus = String(
- lot.stockOutLineStatus || "",
- ).toLowerCase();
- const isSubmitQtyCompleted =
- submitQtyStatus === "completed" ||
- submitQtyStatus === "partially_completed" ||
- submitQtyStatus === "partially_complete";
- const lockedSubmitQtyDisplay =
- isInventoryLotLineUnavailable(lot) &&
- !isSubmitQtyCompleted
- ? 0
- : resolveSingleSubmitQty(lot);
- const hasPickOverride =
- Object.prototype.hasOwnProperty.call(
- pickQtyData,
- lotKeyForSubmitQty,
- );
- const fromPickRow = hasPickOverride
- ? pickQtyData[lotKeyForSubmitQty]
- : undefined;
- const workbenchSubmitQtyDisplay =
- hasPickOverride &&
- fromPickRow !== undefined &&
- fromPickRow !== null &&
- !Number.isNaN(Number(fromPickRow))
- ? Number(fromPickRow)
- : lockedSubmitQtyDisplay;
- const totalAvail = Number(lot.itemTotalAvailableQty ?? 0);
- const isLastLotUnavailable = Number.isFinite(totalAvail) && totalAvail === 0;
- return (
- <TableRow
- key={`${lot.pickOrderLineId}-${lot.lotId}`}
- sx={{
- // backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
- //opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
- "& .MuiTableCell-root": {
- // color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit'
- },
- }}
- >
- <TableCell>
- <Typography variant="body2" fontWeight="bold">
- {row.isGroupFirst ? row.groupDisplayIndex : ""}
- </Typography>
- </TableCell>
- <TableCell>
- {row.isGroupFirst ? (
- <>
- {lot.itemCode} <br />
- {lot.itemName} <br />
- {lot.uomDesc}
- </>
- ) : ""}
- </TableCell>
-
- <TableCell>
- <Typography variant="body2">
- {lot.routerRoute || "-"}
- </Typography>
- </TableCell>
- <TableCell>{lot.handler || "-"}</TableCell>
-
- <TableCell>
- <Stack
- direction="row"
- spacing={1}
- alignItems="center"
- justifyContent="space-between"
- >
- <Box>
- <Typography
- sx={{
- color:
- isInventoryLotLineUnavailable(lot) &&
- !(
- String(
- lot.stockOutLineStatus || "",
- ).toLowerCase() === "completed" ||
- String(
- lot.stockOutLineStatus || "",
- ).toLowerCase() ===
- "partially_completed" ||
- String(
- lot.stockOutLineStatus || "",
- ).toLowerCase() ===
- "partially_complete"
- )
- ? "error.main"
- : lot.lotAvailability === "expired"
- ? "warning.main"
- : "inherit",
- }}
- >
- {lot.lotNo ? (
- /*
-
- */
- lot.lotAvailability === "expired" ? (
- <>
- {lot.lotNo}{" "}
- <Box
- component="span"
- sx={{ fontSize: "0.85rem", lineHeight: 1.4 }}
- >
- {t(
- "is expired. Please check around have available QR code or not.",
- )}
-
- </Box>
- {isLastLotUnavailable && (
- <Box
- component="span"
- sx={{ fontSize: "0.85rem", lineHeight: 1.4 }}
- >
- {t("This is last lot, so no available lot.")}
- </Box>
- )}
- </>
- ) : isInventoryLotLineUnavailable(lot) &&
- !(
- String(
- lot.stockOutLineStatus || "",
- ).toLowerCase() === "completed" ||
- String(
- lot.stockOutLineStatus || "",
- ).toLowerCase() === "partially_completed" ||
- String(
- lot.stockOutLineStatus || "",
- ).toLowerCase() === "partially_complete"
- ) ? (
- <>
- {lot.lotNo}{" "}
- <Box
- component="span"
- sx={{ fontSize: "0.85rem", lineHeight: 1.4 }}
- >
- {t(
- "is unavable. Please check around have available QR code or not.",
- )}
- </Box>
- </>
-
- ) : (
- lot.lotNo
- )
- ) : (
- <Box
- component="span"
- sx={{ fontSize: "0.85rem", lineHeight: 1.4 }}
- >
- {t(
- "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.",
- )}
- </Box>
- )}
- </Typography>
- </Box>
- {Number(lot.stockOutLineId) > 0 &&
- Number(lot.itemId) > 0 ? (
- <Button
- variant="outlined"
- size="small"
- onClick={() =>
- openWorkbenchLotLabelModalForLot(lot)
- }
- disabled={
-
- (Number(lot.stockOutLineId) > 0 &&
- actionBusyBySolId[
- Number(lot.stockOutLineId)
- ] === true)
- }
- sx={{
- flexShrink: 0,
- fontSize: "0.7rem",
- py: 0.25,
- minWidth: "auto",
- px: 1,
- whiteSpace: "nowrap",
- }}
- >
- {tPick("挑號 QR 碼")}
- </Button>
- ) : null}
- </Stack>
- </TableCell>
- <TableCell align="right">
- {(() => {
- const requiredQty = lot.requiredQty || 0;
- const unit =
- lot.noLot === true || !lot.lotId
- ? lot.uomDesc || ""
- : lot.uomDesc || "";
- return `${requiredQty.toLocaleString()}(${unit})`;
- })()}
- </TableCell>
- <TableCell align="right">
- {(() => {
- const avail = lot.itemTotalAvailableQty;
- if (avail == null) return "-";
- const unit = lot.uomDesc || "";
- return `${Number(
- avail,
- ).toLocaleString()}(${unit})`;
- })()}
- </TableCell>
-
- <TableCell align="center">
- {(() => {
- const status =
- lot.stockOutLineStatus?.toLowerCase();
- const isRejected =
- status === "rejected" ||
- lot.lotAvailability === "rejected";
- const isNoLot = !lot.lotNo;
-
- if (isRejected && !isNoLot) {
- return (
- <Box
- sx={{
- display: "flex",
- justifyContent: "center",
- alignItems: "center",
- }}
- >
- <Checkbox
- checked={true}
- disabled={true}
- readOnly={true}
- size="large"
- sx={{
- color: "error.main",
- "&.Mui-checked": {
- color: "error.main",
- },
- transform: "scale(1.3)",
- }}
- />
- </Box>
- );
- }
-
- if (
- isLotAvailabilityExpired(lot) &&
- status !== "rejected"
- ) {
- return (
- <Box
- sx={{
- display: "flex",
- justifyContent: "center",
- alignItems: "center",
- }}
- >
- <Checkbox
- checked={true}
- disabled={true}
- readOnly={true}
- size="large"
- sx={{
- color: "warning.main",
- "&.Mui-checked": {
- color: "warning.main",
- },
- transform: "scale(1.3)",
- }}
- />
- </Box>
- );
- }
-
- if (
- !isNoLot &&
- status !== "pending" &&
- status !== "rejected"
- ) {
- return (
- <Box
- sx={{
- display: "flex",
- justifyContent: "center",
- alignItems: "center",
- }}
- >
- <Checkbox
- checked={true}
- disabled={true}
- readOnly={true}
- size="large"
- sx={{
- color: "success.main",
- "&.Mui-checked": {
- color: "success.main",
- },
- transform: "scale(1.3)",
- }}
- />
- </Box>
- );
- }
-
- if (
- isNoLot &&
- (status === "partially_completed" ||
- status === "completed")
- ) {
- return (
- <Box
- sx={{
- display: "flex",
- justifyContent: "center",
- alignItems: "center",
- }}
- >
- <Checkbox
- checked={true}
- disabled={true}
- readOnly={true}
- size="large"
- sx={{
- color: "error.main",
- "&.Mui-checked": {
- color: "error.main",
- },
- transform: "scale(1.3)",
- }}
- />
- </Box>
- );
- }
-
- return null;
- })()}
- </TableCell>
-
- <TableCell align="center">
- {workbenchSubmitQtyDisplay}
- </TableCell>
-
- <TableCell align="center">
- <Box
- sx={{ display: "flex", justifyContent: "center" }}
- >
- {(() => {
- const status =
- lot.stockOutLineStatus?.toLowerCase();
- const isRejected =
- status === "rejected" ||
- lot.lotAvailability === "rejected";
- const isNoLot = !lot.lotNo;
- const isUnavailableLot =
- isInventoryLotLineUnavailable(lot);
-
- if (isRejected && !isNoLot) {
- const rejectDisplay = buildLotRejectDisplayMessage(
- lot,
- scanRejectMessageBySolId,
- tPick,
- );
- return (
- <Typography
- variant="body2"
- color="error.main"
- sx={{
- textAlign: "center",
- whiteSpace: "normal",
- wordBreak: "break-word",
- maxWidth: "200px",
- lineHeight: 1.5,
- }}
- >
- {rejectDisplay ??
- t(
- "This lot is rejected, please scan another lot.",
- )}
- </Typography>
- );
- }
-
- const lotKey = lotKeyForSubmitQty;
- const qtyFieldEnabled =
- workbenchSubmitQtyFieldEnabledByLotKey[
- lotKey
- ] === true;
- const displayedSubmitQty =
- workbenchSubmitQtyDisplay;
- const hasPickOverrideRow =
- Object.prototype.hasOwnProperty.call(
- pickQtyData,
- lotKey,
- );
- const textFieldValue = qtyFieldEnabled
- ? hasPickOverrideRow
- ? String(pickQtyData[lotKey])
- : String(displayedSubmitQty)
- : String(displayedSubmitQty);
-
- return (
- <Stack
- direction="row"
- spacing={1}
- alignItems="center"
- >
- <TextField
- type="number"
- size="small"
- disabled={!qtyFieldEnabled}
- value={textFieldValue}
- onKeyDown={(e) => {
- if (!qtyFieldEnabled) return;
- if (e.key !== "{") return;
- e.preventDefault();
- setWorkbenchSubmitQtyFieldEnabledByLotKey(
- (prev) => ({
- ...prev,
- [lotKey]: false,
- }),
- );
- (
- e.currentTarget as HTMLInputElement
- ).blur();
- }}
- onChange={(e) => {
- if (!qtyFieldEnabled) return;
- handlePickQtyChange(
- lotKey,
- e.target.value,
- );
- }}
- inputProps={{ min: 0, step: 1 }}
- sx={{
- width: 96,
- "& .MuiInputBase-input": {
- fontSize: "0.75rem",
- py: 0.5,
- textAlign: "center",
- },
- }}
- />
- <Button
- variant="outlined"
- size="small"
- onClick={() => {
- setWorkbenchSubmitQtyFieldEnabledByLotKey(
- (prev) => ({
- ...prev,
- [lotKey]: !(prev[lotKey] === true),
- }),
- );
- }}
- disabled={
- lot.stockOutLineStatus ===
- "completed" ||
- (Number(lot.stockOutLineId) > 0 &&
- actionBusyBySolId[
- Number(lot.stockOutLineId)
- ] === true)
- }
- sx={{
- fontSize: "0.7rem",
- py: 0.5,
- minHeight: "28px",
- minWidth: "60px",
- borderColor: "warning.main",
- color: "warning.main",
- }}
- title={
- qtyFieldEnabled
- ? tPick("Lock quantity")
- : tPick("Edit quantity")
- }
- >
- {tPick("Edit")}
- </Button>
- <Button
- variant="outlined"
- size="small"
- onClick={async () => {
- const solId =
- Number(lot.stockOutLineId) || 0;
- if (solId > 0) {
- setActionBusyBySolId((prev) => ({
- ...prev,
- [solId]: true,
- }));
- }
- try {
- if (
- currentUserId &&
- lot.pickOrderId &&
- lot.itemId
- ) {
- try {
- await updateHandledBy(
- lot.pickOrderId,
- lot.itemId,
- );
- } catch (error) {
- console.error(
- "❌ Error updating handler (non-critical):",
- error,
- );
- }
- }
- await handleSubmitPickQtyWithQty(
- lot,
- 0,
- "justComplete",
- );
- } finally {
- if (solId > 0) {
- setActionBusyBySolId((prev) => ({
- ...prev,
- [solId]: false,
- }));
- }
- }
- }}
- disabled={
- (Number(lot.stockOutLineId) > 0 &&
- actionBusyBySolId[
- Number(lot.stockOutLineId)
- ] === true) ||
- lot.stockOutLineStatus ===
- "completed" ||
- lot.stockOutLineStatus === "checked" ||
- lot.stockOutLineStatus ===
- "partially_completed" ||
- lot.stockOutLineStatus ===
- "partially_complete" ||
- // isUnavailableLot ||
- (Number(lot.stockOutLineId) > 0 &&
- issuePickedQtyBySolId[
- Number(lot.stockOutLineId)
- ] !== undefined)
- }
- title={
- isUnavailableLot
- ? t(
- "is unavable. Please check around have available QR code or not.",
- )
- : undefined
- }
- sx={{
- fontSize: "0.7rem",
- py: 0.5,
- minHeight: "28px",
- minWidth: "90px",
- }}
- >
- {tPick("Just Completed")}
- </Button>
- </Stack>
- );
- })()}
- </Box>
- </TableCell>
- </TableRow>
- );
- })
- )}
- </TableBody>
- </Table>
- </TableContainer>
-
- <TablePagination
- component="div"
- count={
- selectedFloor ? filteredByFloor.length : combinedLotData.length
- }
- page={paginationController.pageNum}
- rowsPerPage={paginationController.pageSize}
- onPageChange={handlePageChange}
- onRowsPerPageChange={handlePageSizeChange}
- rowsPerPageOptions={[10, 25, 50]}
- labelRowsPerPage={t("Rows per page")}
- labelDisplayedRows={({ from, to, count }) =>
- `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
- }
- />
- </Box>
- </Stack>
-
- {/* QR Code Modal */}
- <QrCodeModal
- open={qrModalOpen}
- onClose={() => {
- setQrModalOpen(false);
- setSelectedLotForQr(null);
- // Keep scanner active like GoodPickExecutiondetail.
- resetScan();
- }}
- lot={selectedLotForQr}
- combinedLotData={combinedLotData}
- onQrCodeSubmit={handleQrCodeSubmitFromModal}
- />
- <WorkbenchLotLabelPrintModal
- open={workbenchLotLabelModalOpen}
- onClose={() => {
- setWorkbenchLotLabelModalOpen(false);
- setWorkbenchLotLabelContextLot(null);
- setWorkbenchLotLabelInitialPayload(null);
- setWorkbenchLotLabelReminderText(null);
- }}
- initialPayload={workbenchLotLabelInitialPayload}
- initialItemId={
- workbenchLotLabelContextLot != null
- ? Number(workbenchLotLabelContextLot.itemId)
- : null
- }
- defaultPrinterName={defaultLabelPrinterName}
- hideScanSection={
- workbenchLotLabelInitialPayload != null ||
- workbenchLotLabelContextLot != null
- }
- warehouseCodePrefixFilter={lotFloorPrefixFilter}
- triggerLotAvailableQty={
- workbenchLotLabelContextLot != null
- ? Number(workbenchLotLabelContextLot.availableQty)
- : null
- }
- triggerLotUom={
- workbenchLotLabelContextLot != null
- ? String(workbenchLotLabelContextLot.uomDesc ?? "").trim() || null
- : null
- }
- disableScanPick={workbenchLotLabelScanPickDisabled}
- onWorkbenchScanPick={handleWorkbenchLotLabelScanPick}
- submitQty={workbenchLotLabelSubmitQty}
- onSubmitQtyChange={handleWorkbenchLotLabelSubmitQtyChange}
- reminderText={workbenchLotLabelReminderText ?? undefined}
- />
- {/* Manual Lot Confirmation Modal (test shortcut {2fic}); JO Workbench 不使用 LotConfirmationModal,直接走 handleLotConfirmation */}
- <ManualLotConfirmationModal
- open={manualLotConfirmationOpen}
- onClose={() => setManualLotConfirmationOpen(false)}
- onConfirm={(currentLotNo, newLotNo) => {
- setManualLotConfirmationOpen(false);
- const row = selectedLotForQr;
- if (
- !row?.stockOutLineId ||
- !row?.pickOrderLineId ||
- row.itemId == null
- ) {
- alert(
- t(
- "Open 挑號 QR 碼 on a pick line first, then scan {2fic} to use manual lot substitution.",
- ),
- );
- return;
- }
- setExpectedLotData({
- ...row,
- lotNo: currentLotNo,
- });
- setScannedLotData({
- lotNo: newLotNo,
- itemCode: String(row.itemCode ?? ""),
- itemName: String(row.itemName ?? ""),
- inventoryLotLineId: null,
- stockInLineId: null,
- });
- window.setTimeout(() => {
- void handleLotConfirmation();
- }, 0);
- }}
- expectedLot={expectedLotData}
- scannedLot={scannedLotData}
- isLoading={isConfirmingLot}
- />
-
- </FormProvider>
- </TestQrCodeProvider>
- );
- };
-
- export default JobPickExecution;
|