|
- "use client";
-
- import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
- import {
- Alert,
- Box,
- Button,
- Checkbox,
- CircularProgress,
- Grid,
- Modal,
- Paper,
- Stack,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TablePagination,
- TableRow,
- TextField,
- Typography,
- } from "@mui/material";
- import { useSession } from "next-auth/react";
- import { useTranslation } from "react-i18next";
- import dayjs from "dayjs";
- import arraySupport from "dayjs/plugin/arraySupport";
- import SearchBox, { Criterion } from "../SearchBox";
- import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil";
- import { SessionWithTokens } from "@/config/authConfig";
- import {
- fetchPickOrderWithStockClient,
- fetchWorkbenchPickOrderLineDetailV2,
- confirmLotSubstitution,
- suggestPickOrderWorkbenchV2,
- type PickOrderLotDetailResponse,
- } from "@/app/api/pickOrder/actions";
- import { workbenchScanPick } from "@/app/api/doworkbench/actions";
- import { workbenchScanPickResponseNeedsFullRefresh } from "@/app/api/doworkbench/workbenchScanPickUtils";
- import { fetchStockInLineInfo } from "@/app/api/po/actions";
- import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal";
- import TestQrCodeProvider from "../QrCodeScannerProvider/TestQrCodeProvider";
- import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
- import ScanStatusAlert from "@/components/common/ScanStatusAlert";
-
- dayjs.extend(arraySupport);
-
- type TopRow = {
- rowKey: string;
- pickOrderId: number;
- pickOrderLineId: number;
- pickOrderCode: string;
- itemCode: string;
- itemName: string;
- requiredQty: number;
- currentStock: number;
- pickedQty: number;
- stockUnit: string;
- targetDate: string | number[];
- status: string;
- };
-
- type LotRow = {
- key: string;
- pickOrderId: number;
- pickOrderLineId: number;
- pickOrderCode: string;
- itemCode: string;
- itemName: string;
- uomDesc: string;
- requiredQty: number;
- availableQty: number;
- itemTotalAvailableQty?: number | null;
- stockOutLineId: number;
- status: string;
- pickedQty: number;
- lotNo: string;
- location: string;
- itemId?: number;
- stockInLineId?: number;
- suggestedPickLotId?: number;
- lotAvailability?: string;
- lotStatus?: string;
- expiryDate?: string;
- stockOutLineRejectMessage?: string;
- };
-
- type ConfirmLotState = {
- lotNo: string;
- itemCode: string;
- itemName: string;
- stockInLineId?: number;
- row: LotRow;
- };
-
- type LotRowIndexes = {
- byItemId: Map<number, LotRow[]>;
- byStockInLineId: Map<number, LotRow[]>;
- activeLotsByItemId: Map<number, LotRow[]>;
- };
-
- 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("pickOrder");
-
- const [expectedLotInput, setExpectedLotInput] = useState("");
- const [scannedLotInput, setScannedLotInput] = useState("");
- const [error, setError] = useState("");
- useEffect(() => {
- if (!open) return;
- setExpectedLotInput(expectedLot?.lotNo || "");
- setScannedLotInput(scannedLot?.lotNo || "");
- setError("");
- }, [expectedLot, open, scannedLot]);
- 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>
- <TextField
- fullWidth
- size="small"
- label={t("Expected Lot Number")}
- value={expectedLotInput}
- onChange={(e) => {
- setExpectedLotInput(e.target.value);
- setError("");
- }}
- sx={{ mb: 2 }}
- />
- <TextField
- fullWidth
- size="small"
- label={t("Scanned Lot Number")}
- value={scannedLotInput}
- onChange={(e) => {
- setScannedLotInput(e.target.value);
- setError("");
- }}
- />
- {error ? (
- <Box sx={{ mt: 2, p: 1, borderRadius: 1, bgcolor: "#ffebee" }}>
- <Typography variant="body2" color="error">
- {error}
- </Typography>
- </Box>
- ) : null}
- <Box sx={{ mt: 2, display: "flex", justifyContent: "flex-end", gap: 2 }}>
- <Button onClick={onClose} variant="outlined" disabled={isLoading}>
- {t("Cancel")}
- </Button>
- <Button
- onClick={() => {
- 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());
- }}
- variant="contained"
- color="warning"
- disabled={isLoading}
- >
- {isLoading ? t("Processing...") : t("Confirm")}
- </Button>
- </Box>
- </Box>
- </Modal>
- );
- };
-
- interface Props {
- filterArgs?: Record<string, unknown>;
- }
-
- const toNum = (v: unknown, d = 0): number => {
- const n = Number(v);
- return Number.isFinite(n) ? n : d;
- };
-
- const toStr = (v: unknown): string => (typeof v === "string" ? v : "");
-
- const isCompletedStatus = (status: string | undefined): boolean => {
- const s = String(status || "").toLowerCase();
- return s === "completed" || s === "partially_completed" || s === "partially_complete";
- };
-
- const isCheckedStatus = (status: string | undefined): boolean =>
- String(status || "").toLowerCase() === "checked";
-
- const isRejectedStatus = (status: string | undefined): boolean =>
- String(status || "").toLowerCase() === "rejected";
-
- const isInventoryLotLineUnavailable = (row: LotRow): boolean => {
- const solSt = String(row.status || "").toLowerCase();
- if (solSt === "completed" || solSt === "partially_completed" || solSt === "partially_complete") return false;
- if (String(row.lotAvailability || "").toLowerCase() === "status_unavailable") return true;
- return String(row.lotStatus || "").toLowerCase() === "unavailable";
- };
-
- const isLotExpired = (row: LotRow): boolean => {
- if (String(row.lotAvailability || "").toLowerCase() === "expired") return true;
- if (!row.expiryDate) return false;
- const d = dayjs(row.expiryDate).startOf("day");
- return d.isValid() && d.isBefore(dayjs().startOf("day"));
- };
-
- const isNonBlockingSwitchLotReject = (code: unknown, message: unknown): boolean => {
- const c = String(code || "").toUpperCase();
- const m = String(message || "");
- if (c === "SUCCESS_UNAVAILABLE" || c === "BOUND_UNAVAILABLE") return true;
- if (/^Reject switch lot:/i.test(m)) return true;
- if (/available\s*=\s*\d+(\.\d+)?\s*<\s*required\s*=\s*\d+(\.\d+)?/i.test(m)) return true;
- return false;
- };
-
- function safeDisplayTargetDate(targetDate: string | number[]): string {
- try {
- if (Array.isArray(targetDate) && targetDate.length >= 3) {
- return arrayToDayjs(targetDate).format(OUTPUT_DATE_FORMAT);
- }
- const value = typeof targetDate === "string" ? targetDate : String(targetDate ?? "");
- const d = dayjs(value);
- return d.isValid() ? d.format(OUTPUT_DATE_FORMAT) : "-";
- } catch {
- return "-";
- }
- }
-
- function lineHasStockOutOrSuggestion(details: PickOrderLotDetailResponse[]): boolean {
- if (!details.length) return false;
- return details.some((d) => {
- const sol = toNum(d.stockOutLineId);
- const spl = toNum(d.suggestedPickLotId);
- return sol > 0 || spl > 0 || d.noLot === true;
- });
- }
-
- function mapLotDetailsToRows(
- details: PickOrderLotDetailResponse[],
- ctx: {
- pickOrderId: number;
- pickOrderLineId: number;
- pickOrderCode: string;
- itemCode: string;
- itemName: string;
- totalAvailableQty?: number | null;
- },
- ): LotRow[] {
- return details.map((d, i) => {
- const solId = toNum(d.stockOutLineId);
- const lotId = toNum(d.lotId, i);
- const stockInLineId = toNum(d.stockInLineId);
- return {
- key: solId > 0 ? `sol:${solId}` : `lot:${lotId}:${i}`,
- pickOrderId: ctx.pickOrderId,
- pickOrderLineId: ctx.pickOrderLineId,
- pickOrderCode: ctx.pickOrderCode,
- itemCode: ctx.itemCode,
- itemName: ctx.itemName,
- uomDesc: toStr(d.stockUnit),
- requiredQty: toNum(d.requiredQty),
- availableQty: toNum(d.remainingAfterAllPickOrders ?? d.availableQty),
- itemTotalAvailableQty: toNum(ctx.totalAvailableQty),
- stockOutLineId: solId,
- status: toStr(d.stockOutLineStatus ?? "pending"),
- pickedQty: toNum(d.actualPickQty ?? d.stockOutLineQty),
- lotNo: toStr(d.lotNo),
- location: toStr(d.location),
- itemId: toNum(d.itemId) || undefined,
- stockInLineId: stockInLineId > 0 ? stockInLineId : undefined,
- suggestedPickLotId: toNum(d.suggestedPickLotId) || undefined,
- lotAvailability: toStr((d as any).lotAvailability),
- lotStatus: toStr((d as any).lotStatus),
- expiryDate: toStr((d as any).expiryDate),
- stockOutLineRejectMessage: toStr((d as any).stockOutLineRejectMessage),
- };
- });
- }
-
- const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
- const { t } = useTranslation("pickOrder");
- const { data: session } = useSession() as { data: SessionWithTokens | null };
- const userId = session?.id ? parseInt(session.id, 10) : 0;
-
- const [originalTopRows, setOriginalTopRows] = useState<TopRow[]>([]);
- const [filteredTopRows, setFilteredTopRows] = useState<TopRow[]>([]);
- const [pickOrderLoading, setPickOrderLoading] = useState(false);
- const [pagingController, setPagingController] = useState({ pageNum: 1, pageSize: 10 });
- const [totalCountItems, setTotalCountItems] = useState(0);
- const localizeBackendMessage = (msg: unknown, fallbackKey: string) => {
- const text = typeof msg === "string" ? msg.trim() : "";
- if (!text) return t(fallbackKey);
- return t(text, { defaultValue: text });
- };
- const [selectedPickOrderLineId, setSelectedPickOrderLineId] = useState<number | null>(null);
- const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | null>(null);
- const [selectedTopMeta, setSelectedTopMeta] = useState<{
- pickOrderCode: string;
- itemCode: string;
- itemName: string;
- totalAvailableQty?: number;
- } | null>(null);
-
- const [lotRows, setLotRows] = useState<LotRow[]>([]);
- const [qtyBySolId, setQtyBySolId] = useState<Record<number, number>>({});
- const [qtyEditableBySolId, setQtyEditableBySolId] = useState<Record<number, boolean>>({});
- const [lotPagingController, setLotPagingController] = useState({ pageNum: 0, pageSize: 10 });
- const [loading, setLoading] = useState(false);
- const [submittingSolId, setSubmittingSolId] = useState<number | null>(null);
- const [message, setMessage] = useState("");
- const [error, setError] = useState("");
- const [workbenchLotLabelModalOpen, setWorkbenchLotLabelModalOpen] = useState(false);
- const [workbenchLotLabelContextLot, setWorkbenchLotLabelContextLot] = useState<LotRow | null>(null);
- const [workbenchLotLabelInitialPayload, setWorkbenchLotLabelInitialPayload] =
- useState<{ itemId: number; stockInLineId: number } | null>(null);
- const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
- const [lotConfirmationError, setLotConfirmationError] = useState<string | null>(null);
- const [expectedLotData, setExpectedLotData] = useState<ConfirmLotState | null>(null);
- const [scannedLotData, setScannedLotData] = useState<ConfirmLotState | null>(null);
- const [isConfirmingLot, setIsConfirmingLot] = useState(false);
- const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false);
- const [qrScanError, setQrScanError] = useState(false);
- const [qrScanSuccess, setQrScanSuccess] = useState(false);
- const [qrScanErrorMsg, setQrScanErrorMsg] = useState("");
- const [qrScanSuccessMsg, setQrScanSuccessMsg] = useState("");
- const lastProcessedQrRef = useRef<string>("");
- const processedQrCodesRef = useRef<Set<string>>(new Set());
- const lotConfirmLastQrRef = useRef<string>("");
- const lotConfirmSkipNextScanRef = useRef<boolean>(false);
- const lotConfirmOpenedAtRef = useRef<number>(0);
-
- const { values: qrValues, isScanning, startScan, resetScan } = useQrCodeScannerContext();
-
- const paginatedTopRows = useMemo(() => {
- const start = (pagingController.pageNum - 1) * pagingController.pageSize;
- return filteredTopRows.slice(start, start + pagingController.pageSize);
- }, [filteredTopRows, pagingController]);
-
- const paginatedLotRows = useMemo(() => {
- const start = lotPagingController.pageNum * lotPagingController.pageSize;
- return lotRows.slice(start, start + lotPagingController.pageSize);
- }, [lotRows, lotPagingController]);
-
- const lotRowIndexes = useMemo<LotRowIndexes>(() => {
- const byItemId = new Map<number, LotRow[]>();
- const byStockInLineId = new Map<number, LotRow[]>();
- const activeLotsByItemId = new Map<number, LotRow[]>();
-
- for (const row of lotRows) {
- const itemId = Number(row.itemId);
- const stockInLineId = Number(row.stockInLineId);
- const isActive =
- row.stockOutLineId > 0 &&
- !isCompletedStatus(row.status) &&
- !isCheckedStatus(row.status);
-
- if (Number.isFinite(itemId) && itemId > 0) {
- if (!byItemId.has(itemId)) byItemId.set(itemId, []);
- byItemId.get(itemId)!.push(row);
- if (isActive) {
- if (!activeLotsByItemId.has(itemId)) activeLotsByItemId.set(itemId, []);
- activeLotsByItemId.get(itemId)!.push(row);
- }
- }
-
- if (Number.isFinite(stockInLineId) && stockInLineId > 0) {
- if (!byStockInLineId.has(stockInLineId)) byStockInLineId.set(stockInLineId, []);
- byStockInLineId.get(stockInLineId)!.push(row);
- }
- }
-
- return { byItemId, byStockInLineId, activeLotsByItemId };
- }, [lotRows]);
-
- const fetchNewPageItems = useCallback(
- async (paging: { pageNum: number; pageSize: number }, extra: Record<string, unknown>) => {
- if (!userId) return;
- setPickOrderLoading(true);
- setError("");
- try {
- const params = {
- ...extra,
- pageNum: 0,
- pageSize: 9999,
- status: "released",
- type: "consumable",
- assignTo: userId,
- };
- const res = await fetchPickOrderWithStockClient(params);
- const records = Array.isArray(res?.records) ? res.records : [];
- const rows: TopRow[] = records.flatMap((r: any) => {
- const pickOrderId = toNum(r?.id);
- const code = toStr(r?.code);
- const status = toStr(r?.status);
- const targetDate = r?.targetDate;
- const lines = Array.isArray(r?.pickOrderLines) ? r.pickOrderLines : [];
- return lines.map((line: any, idx: number) => ({
- rowKey: `po:${pickOrderId}:line:${toNum(line?.id, idx)}`,
- pickOrderId,
- pickOrderLineId: toNum(line?.id),
- pickOrderCode: code,
- itemCode: toStr(line?.itemCode),
- itemName: toStr(line?.itemName),
- requiredQty: toNum(line?.requiredQty),
- currentStock: toNum(line?.availableQty),
- pickedQty: toNum(line?.pickedQty),
- stockUnit: toStr(line?.uomDesc ?? line?.uomShortDesc),
- targetDate: targetDate ?? "",
- status,
- }));
- });
- setOriginalTopRows(rows);
- setFilteredTopRows(rows);
- const pageSize = paging.pageSize || 10;
- const pageNum = paging.pageNum || 1;
- setTotalCountItems(rows.length);
- setPagingController({ pageNum, pageSize });
- return rows;
- } catch (e) {
- console.error(e);
- setError(t("Load released pick orders failed"));
- setOriginalTopRows([]);
- setFilteredTopRows([]);
- setTotalCountItems(0);
- return [] as TopRow[];
- } finally {
- setPickOrderLoading(false);
- }
- },
- [t, userId],
- );
-
- const refreshReleasedTopRowsAfterMutation = useCallback(async () => {
- const latestRows =
- (await fetchNewPageItems(
- pagingController,
- (filterArgs || {}) as Record<string, unknown>,
- )) || [];
- if (
- selectedPickOrderLineId != null &&
- !latestRows.some((r) => r.pickOrderLineId === selectedPickOrderLineId)
- ) {
- setSelectedPickOrderLineId(null);
- setSelectedPickOrderId(null);
- setSelectedTopMeta(null);
- setLotRows([]);
- setQtyBySolId({});
- setQtyEditableBySolId({});
- setLotPagingController({ pageNum: 0, pageSize: 10 });
- }
- }, [fetchNewPageItems, filterArgs, pagingController, selectedPickOrderLineId]);
-
- const searchCriteria: Criterion<any>[] = useMemo(
- () => [
- { label: t("Item Code"), paramName: "itemCode", type: "text" },
- { label: t("Pick Order Code"), paramName: "pickOrderCode", type: "text" },
- { label: t("Item Name"), paramName: "itemName", type: "text" },
- { label: t("Target Date From"), label2: t("Target Date To"), paramName: "targetDate", type: "dateRange" },
- ],
- [t],
- );
-
- const handleSearch = useCallback(
- (query: Record<string, string>) => {
- const filtered = originalTopRows.filter((row) => {
- const itemCodeMatch = !query.itemCode || row.itemCode.toLowerCase().includes(query.itemCode.toLowerCase());
- const pickOrderCodeMatch =
- !query.pickOrderCode || row.pickOrderCode.toLowerCase().includes(query.pickOrderCode.toLowerCase());
- const itemNameMatch = !query.itemName || row.itemName.toLowerCase().includes(query.itemName.toLowerCase());
- const targetDate = Array.isArray(row.targetDate)
- ? arrayToDayjs(row.targetDate)
- : dayjs(typeof row.targetDate === "string" ? row.targetDate : "");
- let dateMatch = true;
- if (query.targetDate || query.targetDateTo) {
- const fromDate = query.targetDate ? dayjs(query.targetDate) : null;
- const toDate = query.targetDateTo ? dayjs(query.targetDateTo) : null;
- if (targetDate.isValid()) {
- if (fromDate && fromDate.isValid()) dateMatch = dateMatch && (targetDate.isSame(fromDate, "day") || targetDate.isAfter(fromDate, "day"));
- if (toDate && toDate.isValid()) dateMatch = dateMatch && (targetDate.isSame(toDate, "day") || targetDate.isBefore(toDate, "day"));
- }
- }
- return itemCodeMatch && pickOrderCodeMatch && itemNameMatch && dateMatch;
- });
- setFilteredTopRows(filtered);
- setTotalCountItems(filtered.length);
- setPagingController((prev) => ({ ...prev, pageNum: 1 }));
- },
- [originalTopRows],
- );
-
- const handleReset = useCallback(() => {
- setFilteredTopRows(originalTopRows);
- setTotalCountItems(originalTopRows.length);
- setPagingController((prev) => ({ ...prev, pageNum: 1 }));
- }, [originalTopRows]);
-
- useEffect(() => {
- if (userId) fetchNewPageItems(pagingController, (filterArgs || {}) as Record<string, unknown>);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [userId, filterArgs, fetchNewPageItems]);
-
- const loadLineDetailV2 = useCallback(
- async (
- pickOrderId: number,
- pickOrderLineId: number,
- meta: {
- pickOrderCode: string;
- itemCode: string;
- itemName: string;
- totalAvailableQty?: number;
- },
- ) => {
- if (!userId || pickOrderLineId <= 0) return;
- setLoading(true);
- setError("");
- setMessage("");
- try {
- let details = await fetchWorkbenchPickOrderLineDetailV2(pickOrderLineId);
- let list = Array.isArray(details) ? details : [];
- if (!lineHasStockOutOrSuggestion(list)) {
- const suggestRes = await suggestPickOrderWorkbenchV2(pickOrderId, userId);
- if (suggestRes.code !== "SUCCESS") {
- setError(t("Suggest pick failed"));
- setLotRows([]);
- return;
- }
- details = await fetchWorkbenchPickOrderLineDetailV2(pickOrderLineId);
- list = Array.isArray(details) ? details : [];
- setMessage(t("Suggestion success"));
- }
- setLotRows(
- mapLotDetailsToRows(list, {
- pickOrderId,
- pickOrderLineId,
- pickOrderCode: meta.pickOrderCode,
- itemCode: meta.itemCode,
- itemName: meta.itemName,
- totalAvailableQty: meta.totalAvailableQty,
- }),
- );
- setQtyEditableBySolId({});
- } catch (e) {
- console.error(e);
- setError(t("Load workbench data failed"));
- setLotRows([]);
- } finally {
- setLoading(false);
- }
- },
- [t, userId],
- );
-
- const submitRow = useCallback(
- async (row: LotRow, forceQty?: number) => {
- if (!userId) return;
- if (!row.stockOutLineId) {
- setError(t("No stock out line for this lot"));
- return;
- }
- const qtyInput = qtyBySolId[row.stockOutLineId];
- const qtyValue = forceQty ?? (typeof qtyInput === "number" && Number.isFinite(qtyInput) ? qtyInput : undefined);
- setSubmittingSolId(row.stockOutLineId);
- setError("");
- setMessage("");
- try {
- const res = await workbenchScanPick({
- stockOutLineId: row.stockOutLineId,
- lotNo: row.lotNo.trim(),
- ...(Number.isFinite(Number(row.stockInLineId)) && Number(row.stockInLineId) > 0
- ? { stockInLineId: Number(row.stockInLineId) }
- : {}),
- ...(typeof qtyValue === "number" && Number.isFinite(qtyValue) ? { qty: qtyValue } : {}),
- userId,
- });
- const errMsg = localizeBackendMessage(res.message, "Scan pick failed");
- setError(errMsg);
- setQrScanErrorMsg(errMsg);
-
- const okMsg = localizeBackendMessage(res.message, "Scan pick success");
- setMessage(okMsg);
- setQrScanSuccessMsg(okMsg);
- if (workbenchScanPickResponseNeedsFullRefresh(res)) {
- if (selectedPickOrderId != null && selectedPickOrderLineId != null && selectedTopMeta) {
- await loadLineDetailV2(selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta);
- }
- } else {
- const entity = res.entity as any;
- setLotRows((prev) =>
- prev.map((r) =>
- r.stockOutLineId === row.stockOutLineId
- ? { ...r, status: toStr(entity?.status || r.status), pickedQty: toNum(entity?.qty, r.pickedQty) }
- : r,
- ),
- );
- }
- setWorkbenchLotLabelModalOpen(false);
- setWorkbenchLotLabelContextLot(null);
- setWorkbenchLotLabelInitialPayload(null);
- await refreshReleasedTopRowsAfterMutation();
- } catch (e) {
- console.error(e);
- setError(t("Scan pick failed"));
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(t("Scan pick failed"));
- });
- } finally {
- setSubmittingSolId(null);
- }
- },
- [qtyBySolId, loadLineDetailV2, refreshReleasedTopRowsAfterMutation, selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta, t, userId],
- );
-
- const hasQtyOverrideBySolId = useCallback(
- (stockOutLineId: number) => Object.prototype.hasOwnProperty.call(qtyBySolId, stockOutLineId),
- [qtyBySolId],
- );
-
- const resolveSingleSubmitQty = useCallback(
- (lot: LotRow): number => {
- const override = qtyBySolId[lot.stockOutLineId];
- if (typeof override === "number" && Number.isFinite(override) && override >= 0) {
- return override;
- }
- return Number(lot.requiredQty) || 0;
- },
- [qtyBySolId],
- );
-
- const workbenchScanPickQtyFromLot = useCallback(
- (lot: LotRow) => {
- const hasExplicitOverride = hasQtyOverrideBySolId(lot.stockOutLineId);
- const n = Number(resolveSingleSubmitQty(lot));
- if (hasExplicitOverride && Number.isFinite(n) && n === 0) return { qty: 0 } as const;
- if (!Number.isFinite(n) || n <= 0) return {};
- return { qty: n } as const;
- },
- [hasQtyOverrideBySolId, resolveSingleSubmitQty],
- );
-
- const handleJustComplete = useCallback(
- async (row: LotRow) => {
- if (!row.stockOutLineId) {
- setError(t("No stock out line for this lot"));
- return;
- }
-
- const lotNo = String(row.lotNo || "").trim();
- const isUnavailable = isInventoryLotLineUnavailable(row);
- const isExpired = isLotExpired(row);
- const hasExplicitOverride = hasQtyOverrideBySolId(row.stockOutLineId);
- const explicitQty = hasExplicitOverride ? Number(qtyBySolId[row.stockOutLineId]) : NaN;
- const qtyPayload = workbenchScanPickQtyFromLot(row);
- const wbJustQty = qtyPayload.qty;
-
- const canPostScanPick =
- isUnavailable ||
- (lotNo !== "" &&
- ((hasExplicitOverride && Number.isFinite(explicitQty) && explicitQty === 0) ||
- (wbJustQty != null && wbJustQty > 0)));
-
- if (!canPostScanPick) {
- const msg = t(
- "Just Completed (workbench): requires a valid lot number and quantity; expired rows must not use this button.",
- );
- setError(msg);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(msg);
- });
- return;
- }
-
- if (isExpired && !isUnavailable) {
- const msg = t(
- "Just Completed (workbench): requires a valid lot number and quantity; expired rows must not use this button.",
- );
- setError(msg);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(msg);
- });
- return;
- }
-
- const qtyToSend =
- isUnavailable || (hasExplicitOverride && Number.isFinite(explicitQty) && explicitQty === 0)
- ? 0
- : Number(wbJustQty);
-
- await submitRow(row, qtyToSend);
- },
- [hasQtyOverrideBySolId, qtyBySolId, submitRow, t, workbenchScanPickQtyFromLot],
- );
-
- const handleLineSelect = useCallback(
- async (row: TopRow, checked: boolean) => {
- if (!checked) {
- if (selectedPickOrderLineId === row.pickOrderLineId) {
- setSelectedPickOrderLineId(null);
- setSelectedPickOrderId(null);
- setSelectedTopMeta(null);
- setLotRows([]);
- setQtyBySolId({});
- setQtyEditableBySolId({});
- setLotPagingController({ pageNum: 0, pageSize: 10 });
- }
- return;
- }
- setSelectedPickOrderLineId(row.pickOrderLineId);
- setSelectedPickOrderId(row.pickOrderId);
- setSelectedTopMeta({
- pickOrderCode: row.pickOrderCode,
- itemCode: row.itemCode,
- itemName: row.itemName,
- totalAvailableQty: row.currentStock,
- });
- setLotRows([]);
- setQtyBySolId({});
- setQtyEditableBySolId({});
- setLotPagingController({ pageNum: 0, pageSize: 10 });
- setMessage("");
- await loadLineDetailV2(row.pickOrderId, row.pickOrderLineId, {
- pickOrderCode: row.pickOrderCode,
- itemCode: row.itemCode,
- itemName: row.itemName,
- totalAvailableQty: row.currentStock,
- });
- },
- [loadLineDetailV2, selectedPickOrderLineId],
- );
-
- const openWorkbenchLotLabelModalForLot = useCallback((lot: LotRow) => {
- const itemId = Number(lot.itemId);
- const stockInLineId = Number(lot.stockInLineId);
- setWorkbenchLotLabelContextLot(lot);
- if (Number.isFinite(itemId) && itemId > 0 && Number.isFinite(stockInLineId) && stockInLineId > 0) {
- setWorkbenchLotLabelInitialPayload({ itemId, stockInLineId });
- } else {
- setWorkbenchLotLabelInitialPayload(null);
- }
- setWorkbenchLotLabelModalOpen(true);
- }, []);
-
- const handleWorkbenchLotLabelScanPick = useCallback(
- async ({ inventoryLotLineId, lotNo, qty }: { inventoryLotLineId: number; lotNo: string; qty?: number }) => {
- if (!userId) throw new Error(t("User not found"));
- if (!workbenchLotLabelContextLot?.stockOutLineId) {
- throw new Error(t("No stock out line for this lot"));
- }
- const fallbackQty = Number(
- resolveSingleSubmitQty(workbenchLotLabelContextLot),
- );
- const res = await workbenchScanPick({
- stockOutLineId: workbenchLotLabelContextLot.stockOutLineId,
- inventoryLotLineId,
- lotNo,
- ...(typeof qty === "number" && Number.isFinite(qty)
- ? { qty }
- : Number.isFinite(fallbackQty) && fallbackQty >= 0
- ? { qty: fallbackQty }
- : {}),
- userId,
- });
- if (res.code !== "SUCCESS") {
- throw new Error((res.message as string) || t("Scan pick failed"));
- }
- if (selectedPickOrderId != null && selectedPickOrderLineId != null && selectedTopMeta) {
- await loadLineDetailV2(selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta);
- }
- setWorkbenchLotLabelModalOpen(false);
- setWorkbenchLotLabelContextLot(null);
- setWorkbenchLotLabelInitialPayload(null);
- },
- [
- loadLineDetailV2,
- qtyBySolId,
- selectedPickOrderId,
- selectedPickOrderLineId,
- selectedTopMeta,
- t,
- userId,
- workbenchLotLabelContextLot,
- ],
- );
-
- const handleScanLotByLotNo = useCallback(
- async (lotNo: string) => {
- const normalized = String(lotNo || "").trim();
- if (!normalized) return;
- const target = lotRows.find(
- (r) =>
- String(r.lotNo || "").trim() === normalized &&
- r.stockOutLineId > 0 &&
- !isCompletedStatus(r.status) &&
- !isCheckedStatus(r.status),
- );
- if (!target) {
- setError(t("Lot not found in current line"));
- return;
- }
- await submitRow(target);
- },
- [lotRows, submitRow, t],
- );
-
- const resolveScanCandidate = useCallback(
- (rawQr: string): ConfirmLotState | null => {
- const latest = String(rawQr || "").trim();
- if (!latest) return null;
- try {
- const parsed = JSON.parse(latest);
- const stockInLineId = toNum(parsed?.stockInLineId);
- if (stockInLineId > 0) {
- const row = lotRows.find((r) => Number(r.stockInLineId) === stockInLineId && r.stockOutLineId > 0);
- if (!row) return null;
- return {
- lotNo: String(row.lotNo || "").trim(),
- itemCode: row.itemCode,
- itemName: row.itemName,
- stockInLineId,
- row,
- };
- }
- } catch {
- // non-json; fallback to lotNo match
- }
- const lotNo = latest.replace(/[{}]/g, "").trim();
- if (!lotNo) return null;
- const row = lotRows.find((r) => String(r.lotNo || "").trim() === lotNo && r.stockOutLineId > 0);
- if (!row) return null;
- return {
- lotNo,
- itemCode: row.itemCode,
- itemName: row.itemName,
- stockInLineId: row.stockInLineId,
- row,
- };
- },
- [lotRows],
- );
-
- const toConfirmLotState = useCallback((row: LotRow): ConfirmLotState => {
- return {
- lotNo: String(row.lotNo || "").trim(),
- itemCode: row.itemCode,
- itemName: row.itemName,
- stockInLineId: row.stockInLineId,
- row,
- };
- }, []);
-
- const toConfirmLotStateWithOverrides = useCallback(
- (row: LotRow, override: { lotNo?: string; stockInLineId?: number }): ConfirmLotState => {
- return {
- lotNo: String(override.lotNo ?? row.lotNo ?? "").trim(),
- itemCode: row.itemCode,
- itemName: row.itemName,
- stockInLineId: override.stockInLineId ?? row.stockInLineId,
- row,
- };
- },
- [],
- );
-
- const pickExpectedRowForSubstitution = useCallback((rows: LotRow[]): LotRow | null => {
- if (!rows.length) return null;
- const withLotNo = rows.filter((r) => String(r.lotNo || "").trim() !== "");
- if (withLotNo.length === 1) return withLotNo[0];
- if (withLotNo.length > 1) {
- const pending = withLotNo.find((r) => String(r.status || "").toLowerCase() === "pending");
- return pending || withLotNo[0];
- }
- return rows[0];
- }, []);
-
- const clearLotConfirmationState = useCallback((clearProcessedRefs = false) => {
- setLotConfirmationOpen(false);
- setLotConfirmationError(null);
- setExpectedLotData(null);
- setScannedLotData(null);
- lotConfirmLastQrRef.current = "";
- lotConfirmSkipNextScanRef.current = false;
- lotConfirmOpenedAtRef.current = 0;
- if (clearProcessedRefs) {
- setTimeout(() => {
- lastProcessedQrRef.current = "";
- processedQrCodesRef.current.clear();
- }, 100);
- }
- }, []);
-
- const handleLotConfirmation = useCallback(
- async (overrideScanned?: ConfirmLotState, overrideExpected?: ConfirmLotState) => {
- const expected = overrideExpected ?? expectedLotData;
- const scanned = overrideScanned ?? scannedLotData;
- if (!expected || !scanned) return;
- setIsConfirmingLot(true);
- setLotConfirmationError(null);
- setError("");
- setMessage("");
- try {
- const originalSuggestedPickLotId = Number(expected.row.suggestedPickLotId || 0);
- let switchedToUnavailable = false;
- if (originalSuggestedPickLotId > 0) {
- const res = await confirmLotSubstitution({
- pickOrderLineId: expected.row.pickOrderLineId,
- stockOutLineId: expected.row.stockOutLineId,
- originalSuggestedPickLotId,
- newInventoryLotNo: scanned.lotNo,
- newStockInLineId: Number(scanned.stockInLineId ?? 0),
- });
- switchedToUnavailable = res.code === "SUCCESS_UNAVAILABLE" || res.code === "BOUND_UNAVAILABLE";
- const nonBlockingReject = isNonBlockingSwitchLotReject(res.code, res.message);
- if (res.code !== "SUCCESS" && !switchedToUnavailable && !nonBlockingReject) {
- const msg = (res.message as string) || t("Lot switch failed");
- setLotConfirmationError(msg);
- setError(msg);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(msg);
- });
- return;
- }
- if (nonBlockingReject && !switchedToUnavailable) {
- const warnMsg = (res.message as string) || t("Lot switch rejected. Continue with scan-pick.");
- setMessage(warnMsg);
- }
- }
-
- if (!switchedToUnavailable) {
- const res = await workbenchScanPick({
- stockOutLineId: expected.row.stockOutLineId,
- lotNo: scanned.lotNo,
- ...(Number.isFinite(Number(scanned.stockInLineId)) && Number(scanned.stockInLineId) > 0
- ? { stockInLineId: Number(scanned.stockInLineId) }
- : {}),
- ...workbenchScanPickQtyFromLot(expected.row),
- userId,
- });
- if (res.code !== "SUCCESS") {
- const msg = (res.message as string) || t("Workbench scan-pick failed.");
- setLotConfirmationError(msg);
- setError(msg);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(msg);
- });
- return;
- }
- }
- setMessage(t("Scan pick success"));
- startTransition(() => {
- setQrScanError(false);
- setQrScanSuccess(true);
- setQrScanSuccessMsg(t("Scan pick success"));
- });
- if (selectedPickOrderId != null && selectedPickOrderLineId != null && selectedTopMeta) {
- await loadLineDetailV2(selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta);
- }
- await refreshReleasedTopRowsAfterMutation();
- clearLotConfirmationState(true);
- } catch (e) {
- console.error(e);
- const msg = t("Lot confirmation failed. Please try again.");
- setLotConfirmationError(msg);
- setError(msg);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(msg);
- });
- } finally {
- setIsConfirmingLot(false);
- }
- },
- [
- clearLotConfirmationState,
- expectedLotData,
- loadLineDetailV2,
- refreshReleasedTopRowsAfterMutation,
- scannedLotData,
- selectedPickOrderId,
- selectedPickOrderLineId,
- selectedTopMeta,
- t,
- userId,
- workbenchScanPickQtyFromLot,
- ],
- );
-
- const handleLotConfirmationByRescan = useCallback(
- async (rawQr: string): Promise<boolean> => {
- if (!lotConfirmationOpen || !expectedLotData || !scannedLotData) return false;
- const latest = String(rawQr || "").trim();
- if (!latest) return false;
-
- let parsed: any;
- try {
- parsed = JSON.parse(latest);
- } catch {
- return false;
- }
-
- const rescannedItemId = toNum(parsed?.itemId);
- const rescannedStockInLineId = toNum(parsed?.stockInLineId);
- if (rescannedItemId <= 0 || rescannedStockInLineId <= 0) return false;
-
- const expectedItemId = Number(expectedLotData.row.itemId || 0);
- if (expectedItemId > 0 && rescannedItemId !== expectedItemId) return false;
-
- const expectedStockInLineId = Number(expectedLotData.stockInLineId || expectedLotData.row.stockInLineId || 0);
- const scannedStockInLineId = Number(scannedLotData.stockInLineId || scannedLotData.row.stockInLineId || 0);
-
- if (expectedStockInLineId > 0 && rescannedStockInLineId === expectedStockInLineId) {
- clearLotConfirmationState(false);
- await submitRow(expectedLotData.row);
- return true;
- }
-
- if (scannedStockInLineId > 0 && rescannedStockInLineId === scannedStockInLineId) {
- await handleLotConfirmation();
- return true;
- }
-
- const itemRows = lotRowIndexes.byItemId.get(rescannedItemId) || [];
- const rowByStockInLineId = itemRows.find(
- (r) =>
- Number(r.stockInLineId) === rescannedStockInLineId &&
- r.stockOutLineId > 0 &&
- !isCompletedStatus(r.status) &&
- !isCheckedStatus(r.status),
- );
-
- if (rowByStockInLineId) {
- await handleLotConfirmation(toConfirmLotState(rowByStockInLineId));
- return true;
- }
-
- try {
- const info = await fetchStockInLineInfo(rescannedStockInLineId);
- const rescannedLotNo = String(info?.lotNo || "").trim();
- if (!rescannedLotNo) return false;
- await handleLotConfirmation(
- toConfirmLotStateWithOverrides(expectedLotData.row, {
- lotNo: rescannedLotNo,
- stockInLineId: rescannedStockInLineId,
- }),
- );
- } catch {
- return false;
- }
- return true;
- },
- [
- clearLotConfirmationState,
- expectedLotData,
- handleLotConfirmation,
- lotConfirmationOpen,
- lotRowIndexes,
- scannedLotData,
- submitRow,
- toConfirmLotState,
- toConfirmLotStateWithOverrides,
- ],
- );
-
- const processOutsideQrCode = useCallback(
- async (rawQr: string) => {
- const latest = String(rawQr || "").trim();
- if (!latest) return;
- setError("");
- setMessage("");
- startTransition(() => {
- setQrScanError(false);
- setQrScanSuccess(false);
- });
-
- if (latest === "{2fic}") {
- setManualLotConfirmationOpen(true);
- return;
- }
-
- if (lotConfirmationOpen) {
- const handled = await handleLotConfirmationByRescan(latest);
- if (handled) return;
- }
-
- let parsed: any;
- try {
- parsed = JSON.parse(latest);
- } catch {
- setError(t("Invalid QR format. Expected JSON with itemId and stockInLineId."));
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(t("Invalid QR format. Expected JSON with itemId and stockInLineId."));
- });
- resetScan();
- return;
- }
-
- const scannedItemId = toNum(parsed?.itemId);
- const scannedStockInLineId = toNum(parsed?.stockInLineId);
- if (scannedItemId <= 0 || scannedStockInLineId <= 0) {
- setError(t("Invalid QR data. itemId and stockInLineId are required."));
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(t("Invalid QR data. itemId and stockInLineId are required."));
- });
- resetScan();
- return;
- }
-
- const activeSuggestedLots = lotRowIndexes.activeLotsByItemId.get(scannedItemId) || [];
- const allLotsForItem = lotRowIndexes.byItemId.get(scannedItemId) || [];
- if (allLotsForItem.length === 0) {
- setError(t("Scanned item is not found in current line"));
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(t("Scanned item is not found in current line"));
- });
- resetScan();
- return;
- }
-
- const expectedPool = activeSuggestedLots.length > 0 ? activeSuggestedLots : allLotsForItem;
- const expectedRow = pickExpectedRowForSubstitution(expectedPool) || allLotsForItem[0];
-
- const scannedRows = lotRowIndexes.byStockInLineId.get(scannedStockInLineId) || [];
- const scannedRowInItem =
- scannedRows.find(
- (r) =>
- Number(r.itemId) === scannedItemId &&
- r.stockOutLineId > 0,
- ) ||
- null;
-
- if (scannedRowInItem && isRejectedStatus(scannedRowInItem.status)) {
- const rejectMsg =
- scannedRowInItem.stockOutLineRejectMessage ||
- t("This lot is rejected. Please scan another lot.");
- setError(rejectMsg);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(rejectMsg);
- });
- return;
- }
-
- if (scannedRowInItem && isInventoryLotLineUnavailable(scannedRowInItem)) {
- startTransition(() => {
- setQrScanError(false);
- setQrScanSuccess(false);
- });
- setMessage(t("This lot is unavailable, please scan another lot."));
- openWorkbenchLotLabelModalForLot(scannedRowInItem);
- return;
- }
-
- if (scannedRowInItem && isLotExpired(scannedRowInItem)) {
- const expiredMsg = t("Lot is expired");
- setError(expiredMsg);
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(
- scannedRowInItem.expiryDate
- ? `${expiredMsg} (expiry=${scannedRowInItem.expiryDate})`
- : expiredMsg,
- );
- });
- openWorkbenchLotLabelModalForLot(scannedRowInItem);
- return;
- }
-
- if (scannedRowInItem && (isCompletedStatus(scannedRowInItem.status) || isCheckedStatus(scannedRowInItem.status))) {
- setError(t("Scanned lot is already completed or checked"));
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(t("Scanned lot is already completed or checked"));
- });
- return;
- }
-
- let scannedState: ConfirmLotState | null = null;
- if (scannedRowInItem) {
- scannedState = toConfirmLotState(scannedRowInItem);
- } else {
- try {
- const info = await fetchStockInLineInfo(scannedStockInLineId);
- const scannedLotNo = String(info?.lotNo || "").trim();
- if (!scannedLotNo) {
- setError(t("Scanned lot is not found for current item"));
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(t("Scanned lot is not found for current item"));
- });
- resetScan();
- return;
- }
- scannedState = toConfirmLotStateWithOverrides(expectedRow, {
- lotNo: scannedLotNo,
- stockInLineId: scannedStockInLineId,
- });
- } catch {
- setError(t("Scanned lot is not found for current item"));
- startTransition(() => {
- setQrScanError(true);
- setQrScanSuccess(false);
- setQrScanErrorMsg(t("Scanned lot is not found for current item"));
- });
- resetScan();
- return;
- }
- }
-
- if (
- Number(expectedRow.stockInLineId) > 0 &&
- Number(scannedState.stockInLineId) > 0 &&
- Number(expectedRow.stockInLineId) === Number(scannedState.stockInLineId)
- ) {
- await submitRow(expectedRow);
- return;
- }
-
- await handleLotConfirmation(scannedState, toConfirmLotState(expectedRow));
- },
- [
- handleLotConfirmation,
- handleLotConfirmationByRescan,
- lotConfirmationOpen,
- pickExpectedRowForSubstitution,
- lotRowIndexes,
- resetScan,
- submitRow,
- t,
- toConfirmLotState,
- toConfirmLotStateWithOverrides,
- ],
- );
-
- useEffect(() => {
- if (!userId) return;
- if (!isScanning) startScan();
- }, [isScanning, startScan, userId]);
-
- useEffect(() => {
- if (!selectedPickOrderLineId) {
- lastProcessedQrRef.current = "";
- processedQrCodesRef.current.clear();
- }
- }, [selectedPickOrderLineId]);
-
- useEffect(() => {
- if (!qrValues.length || lotRows.length === 0) return;
- const latest = String(qrValues[qrValues.length - 1] || "");
- if (!latest) return;
-
- if (lotConfirmationOpen) {
- if (isConfirmingLot) return;
- if (lotConfirmSkipNextScanRef.current) {
- lotConfirmSkipNextScanRef.current = false;
- lotConfirmLastQrRef.current = latest;
- return;
- }
- const sameQr = latest === lotConfirmLastQrRef.current;
- const justOpened =
- lotConfirmOpenedAtRef.current > 0 &&
- Date.now() - lotConfirmOpenedAtRef.current < 800;
- if (sameQr && justOpened) return;
- lotConfirmLastQrRef.current = latest;
- void (async () => {
- try {
- const handled = await handleLotConfirmationByRescan(latest);
- if (handled) resetScan();
- } catch (e) {
- console.error("Lot confirmation rescan failed:", e);
- }
- })();
- return;
- }
-
- if (latest === lastProcessedQrRef.current || processedQrCodesRef.current.has(latest)) return;
- lastProcessedQrRef.current = latest;
- processedQrCodesRef.current.add(latest);
- if (processedQrCodesRef.current.size > 100) {
- const firstValue = processedQrCodesRef.current.values().next().value;
- if (firstValue !== undefined) processedQrCodesRef.current.delete(firstValue);
- }
-
- const run = async () => {
- try {
- // JO shortcut: {2fitestx,y} -> simulate JSON qr
- if (
- (latest.startsWith("{2fitest") || latest.startsWith("{2fittest")) &&
- latest.endsWith("}")
- ) {
- let content = "";
- if (latest.startsWith("{2fittest")) content = latest.substring(9, latest.length - 1);
- else content = latest.substring(8, latest.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 (!Number.isNaN(itemId) && !Number.isNaN(stockInLineId)) {
- await processOutsideQrCode(JSON.stringify({ itemId, stockInLineId }));
- return;
- }
- }
- }
- await processOutsideQrCode(latest);
- } finally {
- resetScan();
- }
- };
- void run();
- }, [
- handleLotConfirmationByRescan,
- isConfirmingLot,
- lotConfirmationOpen,
- lotRows.length,
- processOutsideQrCode,
- qrValues,
- resetScan,
- ]);
-
- return (
- <TestQrCodeProvider
- lotData={lotRows}
- onScanLot={handleScanLotByLotNo}
- filterActive={(lot) =>
- lot.stockOutLineId > 0 &&
- !isCompletedStatus(lot.status) &&
- !isCheckedStatus(lot.status) &&
- String(lot.lotNo || "").trim() !== ""
- }
- >
- <Stack spacing={2}>
- <Paper variant="outlined" sx={{ p: 2 }}>
- <Stack spacing={1}>
- <SearchBox criteria={searchCriteria} onSearch={handleSearch} onReset={handleReset} />
- <Grid container rowGap={1}>
- <Grid item xs={12}>
- {pickOrderLoading ? (
- <CircularProgress size={40} />
- ) : (
- <TableContainer component={Paper}>
- <Table size="small">
- <TableHead>
- <TableRow>
- <TableCell>{t("Selected")}</TableCell>
- <TableCell>{t("Pick Order Code")}</TableCell>
- <TableCell>{t("Item Code")}</TableCell>
- <TableCell>{t("Item Name")}</TableCell>
- <TableCell align="right">{t("Order Quantity")}</TableCell>
- <TableCell align="right">{t("Current Stock")}</TableCell>
- <TableCell align="right">{t("Picked Qty")}</TableCell>
- <TableCell>{t("Stock Unit")}</TableCell>
- <TableCell>{t("Target Date")}</TableCell>
- <TableCell>{t("Status")}</TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {paginatedTopRows.length === 0 ? (
- <TableRow>
- <TableCell colSpan={10}>
- <Typography variant="body2" color="text.secondary">
- {t("No data available")}
- </Typography>
- </TableCell>
- </TableRow>
- ) : (
- paginatedTopRows.map((row) => (
- <TableRow key={row.rowKey}>
- <TableCell padding="checkbox">
- <Checkbox
- checked={selectedPickOrderLineId === row.pickOrderLineId}
- onChange={(_, checked) => void handleLineSelect(row, checked)}
- />
- </TableCell>
- <TableCell>{row.pickOrderCode || "-"}</TableCell>
- <TableCell>{row.itemCode || "-"}</TableCell>
- <TableCell>{row.itemName || "-"}</TableCell>
- <TableCell align="right">{row.requiredQty.toLocaleString()}</TableCell>
- <TableCell align="right">{row.currentStock.toLocaleString()}</TableCell>
- <TableCell align="right">{row.pickedQty.toLocaleString()}</TableCell>
- <TableCell>{row.stockUnit || "-"}</TableCell>
- <TableCell>{safeDisplayTargetDate(row.targetDate)}</TableCell>
- <TableCell>{t(row.status || "-")}</TableCell>
- </TableRow>
- ))
- )}
- </TableBody>
- </Table>
- </TableContainer>
- )}
- </Grid>
- <Grid item xs={12}>
- <TablePagination
- component="div"
- count={totalCountItems}
- page={pagingController.pageNum - 1}
- rowsPerPage={pagingController.pageSize}
- onPageChange={(_e, newPage) => setPagingController((prev) => ({ ...prev, pageNum: newPage + 1 }))}
- onRowsPerPageChange={(e) =>
- setPagingController({
- pageNum: 1,
- pageSize: parseInt(e.target.value, 10),
- })
- }
- rowsPerPageOptions={[10, 25, 50, 100]}
- labelRowsPerPage={t("Rows per page")}
- />
- </Grid>
- </Grid>
- </Stack>
- </Paper>
-
- {loading ? (
- <Stack direction="row" alignItems="center" spacing={1}>
- <CircularProgress size={24} />
- <Typography variant="body2">{t("Loading")}</Typography>
- </Stack>
- ) : null}
- <ScanStatusAlert
- error={qrScanError}
- success={qrScanSuccess}
- errorMessage={qrScanErrorMsg || t("QR code does not match any item in current orders.")}
- successMessage={qrScanSuccessMsg || t("QR code verified.")}
- />
- {error ? <Alert severity="error">{error}</Alert> : null}
- {message ? <Alert severity="success">{message}</Alert> : null}
-
- <Paper variant="outlined">
- <TableContainer>
- <Table size="small">
- <TableHead>
- <TableRow>
- <TableCell>{t("Index")}</TableCell>
- <TableCell>{t("Item Code")}</TableCell>
- <TableCell>{t("Route")}</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>
- {paginatedLotRows.map((r, idx) => (
- <TableRow key={r.key}>
- <TableCell>{idx === 0 ? lotPagingController.pageNum * lotPagingController.pageSize + 1 : ""}</TableCell>
- <TableCell>
- {idx === 0 ? (
- <>
- {r.itemCode || "-"} <br />
- {r.itemName || "-"} <br />
- {r.uomDesc || "-"}
- </>
- ) : (
- ""
- )}
- </TableCell>
- <TableCell>{r.location || "-"}</TableCell>
- <TableCell>
- <Stack direction="row" spacing={1} alignItems="center" justifyContent="space-between">
- <Typography variant="body2">{r.lotNo || "-"}</Typography>
- {r.stockOutLineId > 0 ? (
- <Button
- variant="outlined"
- size="small"
- onClick={() => openWorkbenchLotLabelModalForLot(r)}
- sx={{ flexShrink: 0, fontSize: "0.7rem", py: 0.25, minWidth: "auto", px: 1, whiteSpace: "nowrap" }}
- >
- {t("挑號 QR 碼")}
- </Button>
- ) : null}
- </Stack>
- </TableCell>
- <TableCell align="right">{`${r.requiredQty.toLocaleString()}(${r.uomDesc || ""})`}</TableCell>
- <TableCell align="right">
- {`${Number(
- r.itemTotalAvailableQty ?? r.availableQty ?? 0,
- ).toLocaleString()}(${r.uomDesc || ""})`}
- </TableCell>
- <TableCell align="center">
- <Checkbox
- checked={isCompletedStatus(r.status) || isCheckedStatus(r.status)}
- disabled
- size="small"
- sx={{
- color: isCompletedStatus(r.status) ? "success.main" : isCheckedStatus(r.status) ? "warning.main" : "action.disabled",
- "&.Mui-checked": {
- color: isCompletedStatus(r.status) ? "success.main" : isCheckedStatus(r.status) ? "warning.main" : "action.disabled",
- },
- }}
- />
- </TableCell>
- <TableCell align="center">
- <Stack direction="row" spacing={1} justifyContent="center" alignItems="center">
- <TextField
- size="small"
- type="number"
- value={qtyBySolId[r.stockOutLineId] ?? Number(r.requiredQty)}
- onKeyDown={(e) => {
- const editable = qtyEditableBySolId[r.stockOutLineId] === true;
- if (!editable) return;
- if (e.key !== "{") return;
- e.preventDefault();
- setQtyEditableBySolId((prev) => ({
- ...prev,
- [r.stockOutLineId]: false,
- }));
- (e.currentTarget as HTMLInputElement).blur();
- }}
- onChange={(e) => {
- const v = e.target.value;
- setQtyBySolId((prev) => {
- if (v === "" || v == null) {
- if (!Object.prototype.hasOwnProperty.call(prev, r.stockOutLineId)) return prev;
- const next = { ...prev };
- delete next[r.stockOutLineId];
- return next;
- }
- const n = Number(v);
- if (!Number.isFinite(n) || n < 0) {
- if (!Object.prototype.hasOwnProperty.call(prev, r.stockOutLineId)) return prev;
- const next = { ...prev };
- delete next[r.stockOutLineId];
- return next;
- }
- return { ...prev, [r.stockOutLineId]: n };
- });
- }}
- sx={{ width: 96 }}
- disabled={!r.stockOutLineId || qtyEditableBySolId[r.stockOutLineId] !== true}
- inputProps={{ min: 0, step: 1 }}
- />
- <Button
- variant="outlined"
- size="small"
- onClick={() =>
- setQtyEditableBySolId((prev) => ({
- ...prev,
- [r.stockOutLineId]: !(prev[r.stockOutLineId] === true),
- }))
- }
- disabled={!r.stockOutLineId || isCompletedStatus(r.status)}
- sx={{ fontSize: "0.7rem", py: 0.5, minHeight: "28px", minWidth: "60px", borderColor: "warning.main", color: "warning.main" }}
- >
- {t("Edit")}
- </Button>
- </Stack>
- </TableCell>
- <TableCell align="center">
- <Stack direction="row" spacing={1} justifyContent="center">
- <Button
- size="small"
- variant="outlined"
- disabled={!r.stockOutLineId || isCompletedStatus(r.status) || isCheckedStatus(r.status)}
- onClick={() => void handleJustComplete(r)}
- >
- {t("Just Complete")}
- </Button>
- </Stack>
- </TableCell>
- </TableRow>
- ))}
- {lotRows.length === 0 ? (
- <TableRow>
- <TableCell colSpan={9}>
- <Typography variant="body2" color="text.secondary">
- {t("No lot rows. Select a line in the table above.")}
- </Typography>
- </TableCell>
- </TableRow>
- ) : null}
- </TableBody>
- </Table>
- </TableContainer>
- <TablePagination
- component="div"
- count={lotRows.length}
- page={lotPagingController.pageNum}
- rowsPerPage={lotPagingController.pageSize}
- onPageChange={(_e, newPage) => setLotPagingController((prev) => ({ ...prev, pageNum: newPage }))}
- onRowsPerPageChange={(e) =>
- setLotPagingController({
- pageNum: 0,
- pageSize: parseInt(e.target.value, 10),
- })
- }
- rowsPerPageOptions={[10, 25, 50]}
- labelRowsPerPage={t("Rows per page")}
- />
- </Paper>
- <WorkbenchLotLabelPrintModal
- open={workbenchLotLabelModalOpen}
- onClose={() => {
- setWorkbenchLotLabelModalOpen(false);
- setWorkbenchLotLabelContextLot(null);
- setWorkbenchLotLabelInitialPayload(null);
- }}
- initialPayload={workbenchLotLabelInitialPayload}
- initialItemId={workbenchLotLabelContextLot?.itemId ?? null}
- hideScanSection={workbenchLotLabelInitialPayload != null || workbenchLotLabelContextLot != null}
- triggerLotAvailableQty={workbenchLotLabelContextLot?.availableQty ?? null}
- triggerLotUom={workbenchLotLabelContextLot?.uomDesc ?? null}
- submitQty={
- workbenchLotLabelContextLot?.stockOutLineId
- ? Number(resolveSingleSubmitQty(workbenchLotLabelContextLot))
- : null
- }
- onSubmitQtyChange={(qty) => {
- const solId = Number(workbenchLotLabelContextLot?.stockOutLineId);
- if (!Number.isFinite(solId) || solId <= 0) return;
- if (!Number.isFinite(qty) || qty < 0) {
- setQtyBySolId((prev) => {
- if (!Object.prototype.hasOwnProperty.call(prev, solId)) return prev;
- const next = { ...prev };
- delete next[solId];
- return next;
- });
- return;
- }
- setQtyBySolId((prev) => ({ ...prev, [solId]: qty }));
- }}
- onWorkbenchScanPick={handleWorkbenchLotLabelScanPick}
- />
- <ManualLotConfirmationModal
- open={manualLotConfirmationOpen}
- onClose={() => setManualLotConfirmationOpen(false)}
- onConfirm={(expectedLotNo, scannedLotNo) => {
- const expected = resolveScanCandidate(expectedLotNo);
- const scanned = resolveScanCandidate(scannedLotNo);
- if (!expected || !scanned) {
- setError(t("Lot not found in current line"));
- return;
- }
- setManualLotConfirmationOpen(false);
- void handleLotConfirmation(scanned, expected);
- }}
- expectedLot={
- expectedLotData
- ? {
- lotNo: expectedLotData.lotNo,
- itemCode: expectedLotData.itemCode,
- itemName: expectedLotData.itemName,
- }
- : null
- }
- scannedLot={
- scannedLotData
- ? {
- lotNo: scannedLotData.lotNo,
- itemCode: scannedLotData.itemCode,
- itemName: scannedLotData.itemName,
- }
- : null
- }
- isLoading={isConfirmingLot}
- />
- </Stack>
- </TestQrCodeProvider>
- );
- };
-
- export default WorkbenchPickExecution;
|