@@ -25,15 +25,13 @@ import { useSession } from "next-auth/react";
import { useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next";
import dayjs from "dayjs";
import dayjs from "dayjs";
import arraySupport from "dayjs/plugin/arraySupport";
import arraySupport from "dayjs/plugin/arraySupport";
import SearchBox, { Criterion } from "../SearchBox";
import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil";
import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil";
import { SessionWithTokens } from "@/config/authConfig";
import { SessionWithTokens } from "@/config/authConfig";
import {
import {
fetchPickOrderWithStockClient ,
fetchWorkbenchPickOrderLineDetailV2 ,
fetchConsumableWorkbenchPickOrderLotsHierarchical ,
reloadConsumableWorkbenchPickOrderLotsHierarchical ,
confirmLotSubstitution,
confirmLotSubstitution,
suggestPickOrderWorkbenchV2,
suggestPickOrderWorkbenchV2,
type PickOrderLotDetailResponse,
} from "@/app/api/pickOrder/actions";
} from "@/app/api/pickOrder/actions";
import { workbenchScanPick } from "@/app/api/doworkbench/actions";
import { workbenchScanPick } from "@/app/api/doworkbench/actions";
import { fetchStockInLineInfo } from "@/app/api/po/actions";
import { fetchStockInLineInfo } from "@/app/api/po/actions";
@@ -48,6 +46,7 @@ import {
isExpiredWorkbenchReminderMessage,
isExpiredWorkbenchReminderMessage,
isInventoryLotLineUnavailable,
isInventoryLotLineUnavailable,
isLotAvailabilityExpired,
isLotAvailabilityExpired,
isNoLotWorkbenchRow,
isWorkbenchSourceLotExpired,
isWorkbenchSourceLotExpired,
isWorkbenchZeroCompleteLot,
isWorkbenchZeroCompleteLot,
translateWorkbenchRejectMessage,
translateWorkbenchRejectMessage,
@@ -56,19 +55,30 @@ import {
dayjs.extend(arraySupport);
dayjs.extend(arraySupport);
type Top Row = {
type Line Row = {
rowKey: string;
rowKey: string;
pickOrderId: number;
pickOrderId: number;
pickOrderLineId: number;
pickOrderLineId: number;
pickOrderCode: string;
pickOrderCode: string;
itemCode: string;
itemCode: string;
itemName: string;
itemName: string;
itemId?: number;
requiredQty: number;
requiredQty: number;
currentStock: number;
pickedQty: number;
pickedQty: number;
stockUnit: string;
stockUnit: string;
targetDate: string | number[];
status: string;
status: string;
lotsRaw: unknown[];
};
type PickOrderTopRow = {
rowKey: string;
pickOrderId: number;
pickOrderCode: string;
targetDate: string;
status: string;
lineCount: number;
completedLineCount: number;
lines: LineRow[];
};
};
type LotRow = {
type LotRow = {
@@ -80,11 +90,13 @@ type LotRow = {
itemName: string;
itemName: string;
uomDesc: string;
uomDesc: string;
requiredQty: number;
requiredQty: number;
pickOrderLineRequiredQty?: number;
availableQty: number;
availableQty: number;
itemTotalAvailableQty?: number | null;
itemTotalAvailableQty?: number | null;
stockOutLineId: number;
stockOutLineId: number;
status: string;
status: string;
pickedQty: number;
pickedQty: number;
stockOutLineQty?: number;
lotNo: string;
lotNo: string;
location: string;
location: string;
itemId?: number;
itemId?: number;
@@ -93,6 +105,7 @@ type LotRow = {
lotAvailability?: string;
lotAvailability?: string;
lotStatus?: string;
lotStatus?: string;
expiryDate?: string;
expiryDate?: string;
noLot?: boolean;
stockOutLineRejectMessage?: string;
stockOutLineRejectMessage?: string;
};
};
@@ -235,53 +248,105 @@ function safeDisplayTargetDate(targetDate: string | number[]): string {
}
}
}
}
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 lineHierarchicalNeedsSuggest(line: LineRow): boolean {
if (isCompletedStatus(line.status)) return false;
if (!line.lotsRaw.length) return true;
return line.lotsRaw.every((lotRaw) => {
const lot = lotRaw as Record<string, unknown>;
return toNum(lot.stockOutLineId) <= 0;
});
}
function flattenLotsFromPickOrder(po: PickOrderTopRow): LotRow[] {
const rows: LotRow[] = [];
po.lines.forEach((line) => {
line.lotsRaw.forEach((lotRaw, i) => {
const lot = lotRaw as Record<string, unknown>;
const solId = toNum(lot.stockOutLineId);
const lotId = toNum(lot.id ?? lot.lotId, i);
const stockInLineId = toNum(lot.stockInLineId);
rows.push({
key: solId > 0 ? `sol:${solId}` : `line:${line.pickOrderLineId}:lot:${lotId}:${i}`,
pickOrderId: po.pickOrderId,
pickOrderLineId: line.pickOrderLineId,
pickOrderCode: po.pickOrderCode,
itemCode: line.itemCode,
itemName: line.itemName,
uomDesc: toStr(lot.stockUnit) || line.stockUnit,
requiredQty: toNum(lot.requiredQty, line.requiredQty),
pickOrderLineRequiredQty: line.requiredQty,
availableQty: toNum(lot.availableQty),
stockOutLineId: solId,
status: toStr(lot.stockOutLineStatus ?? lot.processingStatus ?? "pending"),
pickedQty: toNum(lot.actualPickQty ?? lot.stockOutLineQty),
stockOutLineQty: toNum(lot.stockOutLineQty ?? lot.actualPickQty),
lotNo: toStr(lot.lotNo),
location: toStr(lot.location),
itemId: line.itemId,
stockInLineId: stockInLineId > 0 ? stockInLineId : undefined,
suggestedPickLotId: toNum(lot.suggestedPickLotId) > 0 ? toNum(lot.suggestedPickLotId) : undefined,
lotAvailability: toStr(lot.lotAvailability),
lotStatus: toStr(lot.lotStatus),
expiryDate: toStr(lot.expiryDate),
noLot: lot.noLot === true || (solId > 0 && !toStr(lot.lotNo)),
});
});
});
});
return rows;
}
function sumLinePickedQtyFromLots(lots: unknown[]): number {
return lots.reduce<number>((acc, lot) => {
const row = lot as Record<string, unknown>;
const st = String(row?.stockOutLineStatus ?? row?.processingStatus ?? "").toLowerCase();
const counted =
st === "completed" ||
st === "complete" ||
st === "partially_completed" ||
st === "partially_complete";
if (!counted) return acc;
return acc + toNum(row?.actualPickQty ?? row?.stockOutLineQty);
}, 0);
}
}
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);
function mapHierarchicalToPickOrders(data: unknown): PickOrderTopRow[] {
const root = data as { pickOrders?: unknown[] };
const pos = Array.isArray(root?.pickOrders) ? root.pickOrders : [];
return pos.map((poRaw) => {
const po = poRaw as Record<string, unknown>;
const pickOrderId = toNum(po?.pickOrderId);
const pickOrderCode = toStr(po?.pickOrderCode);
const rawLines = Array.isArray(po?.pickOrderLines) ? po.pickOrderLines : [];
const lines: LineRow[] = rawLines.map((lineRaw, idx) => {
const line = lineRaw as Record<string, unknown>;
const item = (line?.item as Record<string, unknown>) || {};
const lots = Array.isArray(line?.lots) ? line.lots : [];
const pickOrderLineId = toNum(line?.id, idx);
return {
rowKey: `po:${pickOrderId}:line:${pickOrderLineId}`,
pickOrderId,
pickOrderLineId,
pickOrderCode,
itemCode: toStr(item?.code),
itemName: toStr(item?.name),
itemId: toNum(item?.id) || undefined,
requiredQty: toNum(line?.requiredQty),
pickedQty: sumLinePickedQtyFromLots(lots),
stockUnit: toStr(item?.uomDesc ?? item?.uomCode),
status: toStr(line?.status),
lotsRaw: lots,
};
});
const completedLineCount = lines.filter((l) => isCompletedStatus(l.status)).length;
return {
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),
rowKey: `po:${pickOrderId}`,
pickOrderId,
pickOrderCode,
targetDate: toStr(po?.targetDate ?? ""),
status: toStr(po?.status),
lineCount: lines.length,
completedLineCount,
lines,
};
};
});
});
}
}
@@ -291,29 +356,21 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
const { data: session } = useSession() as { data: SessionWithTokens | null };
const { data: session } = useSession() as { data: SessionWithTokens | null };
const userId = session?.id ? parseInt(session.id, 10) : 0;
const userId = session?.id ? parseInt(session.id, 10) : 0;
const [originalTopRows, setOriginalTopRows] = useState<TopRow[]>([]);
const [filteredTopRows, setFilteredTopRows] = useState<TopRow[]>([]);
const [pickOrders, setPickOrders] = useState<PickOrderTopRow[]>([]);
const [pickOrderLoading, setPickOrderLoading] = useState(false);
const [pickOrderLoading, setPickOrderLoading] = useState(false);
const [pagingController, setPagingController] = useState({ pageNum: 1, pageSize: 10 });
const [totalCountItems, setTotalCountItem s] = useState(0);
const [poP agingController, setPo PagingController] = useState({ pageNum: 1, pageSize: 10 });
const [totalCountPickOrders, setTotalCountPickOrder s] = useState(0);
const localizeBackendMessage = (msg: unknown, fallbackKey: string) => {
const localizeBackendMessage = (msg: unknown, fallbackKey: string) => {
const text = typeof msg === "string" ? msg.trim() : "";
const text = typeof msg === "string" ? msg.trim() : "";
if (!text) return t(fallbackKey);
if (!text) return t(fallbackKey);
return t(text, { defaultValue: text });
return t(text, { defaultValue: text });
};
};
const [selectedPickOrderLineId, setSelectedPickOrderLineId] = useState<number | null>(null);
const [selectedPickOrderId, setSelectedPickOrderId] = 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 [lotRows, setLotRows] = useState<LotRow[]>([]);
const [qtyBySolId, setQtyBySolId] = useState<Record<number, number>>({});
const [qtyBySolId, setQtyBySolId] = useState<Record<number, number>>({});
const [qtyEditableBySolId, setQtyEditableBySolId] = useState<Record<number, boolean>>({});
const [qtyEditableBySolId, setQtyEditableBySolId] = useState<Record<number, boolean>>({});
const [lotPagingController, setLotPagingController] = useState({ pageNum: 0, pageSize: 10 });
const [lotPagingController, setLotPagingController] = useState({ pageNum: 0, pageSize: -1 });
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(false);
const [submittingSolId, setSubmittingSolId] = useState<number | null>(null);
const [submittingSolId, setSubmittingSolId] = useState<number | null>(null);
const [message, setMessage] = useState("");
const [message, setMessage] = useState("");
@@ -341,12 +398,13 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
const { values: qrValues, isScanning, startScan, resetScan } = useQrCodeScannerContext();
const { values: qrValues, isScanning, startScan, resetScan } = useQrCodeScannerContext();
const paginatedTopRow s = useMemo(() => {
const start = (pagingController.pageNum - 1) * pagingController.pageSize;
return filteredTopRows.slice(start, start + p agingController.pageSize);
}, [filteredTopRows, p agingController]);
const paginatedPickOrder s = useMemo(() => {
const start = (poP agingController.pageNum - 1) * poP agingController.pageSize;
return pickOrders.slice(start, start + poP agingController.pageSize);
}, [pickOrders, poP agingController]);
const paginatedLotRows = useMemo(() => {
const paginatedLotRows = useMemo(() => {
if (lotPagingController.pageSize === -1) return lotRows;
const start = lotPagingController.pageNum * lotPagingController.pageSize;
const start = lotPagingController.pageNum * lotPagingController.pageSize;
return lotRows.slice(start, start + lotPagingController.pageSize);
return lotRows.slice(start, start + lotPagingController.pageSize);
}, [lotRows, lotPagingController]);
}, [lotRows, lotPagingController]);
@@ -388,57 +446,25 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
return { byItemId, byStockInLineId, activeLotsByItemId };
return { byItemId, byStockInLineId, activeLotsByItemId };
}, [lotRows]);
}, [lotRows]);
const fetchNewPageItem s = useCallback(
async (paging: { pageNum: number; pageSize: number }, extra: Record<string, unknown> ) => {
if (!userId) return;
const loadHierarchicalPickOrder s = useCallback(
async (reload = false ) => {
if (!userId) return [] as PickOrderTopRow[] ;
setPickOrderLoading(true);
setPickOrderLoading(true);
setError("");
setError("");
try {
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;
const data = reload
? await reloadConsumableWorkbenchPickOrderLotsHierarchical(userId)
: await fetchConsumableWorkbenchPickOrderLotsHierarchical(userId);
const orders = mapHierarchicalToPickOrders(data);
setPickOrders(orders);
setTotalCountPickOrders(orders.length);
return orders;
} catch (e) {
} catch (e) {
console.error(e);
console.error(e);
setError(t("Load released pick orders failed"));
setError(t("Load released pick orders failed"));
setOriginalTopRows([]);
setFilteredTopRows([]);
setTotalCountItems(0);
return [] as TopRow[];
setPickOrders([]);
setTotalCountPickOrders(0);
return [] as PickOrderTopRow[];
} finally {
} finally {
setPickOrderLoading(false);
setPickOrderLoading(false);
}
}
@@ -446,125 +472,62 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
[t, userId],
[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]);
const applyLotsFromPickOrder = useCallback((po: PickOrderTopRow) => {
setLotRows(flattenLotsFromPickOrder(po));
setQtyEditableBySolId({});
}, []);
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;
const ensurePickOrderLotsIfNeeded = useCallback(
async (po: PickOrderTopRow) => {
if (!userId || !po.lines.some(lineHierarchicalNeedsSuggest)) return;
setLoading(true);
setLoading(true);
setError("");
setError("");
setMessage("");
try {
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 : [];
const suggestRes = await suggestPickOrderWorkbenchV2(po.pickOrderId, userId);
if (suggestRes.code !== "SUCCESS") {
setError(t("Suggest pick failed"));
return;
}
}
setLotRows(
mapLotDetailsToRows(list, {
pickOrderId,
pickOrderLineId,
pickOrderCode: meta.pickOrderCode,
itemCode: meta.itemCode,
itemName: meta.itemName,
totalAvailableQty: meta.totalAvailableQty,
}),
);
setQtyEditableBySolId({});
const data = await reloadConsumableWorkbenchPickOrderLotsHierarchical(userId);
const orders = mapHierarchicalToPickOrders(data);
setPickOrders(orders);
setTotalCountPickOrders(orders.length);
const refreshed = orders.find((p) => p.pickOrderId === po.pickOrderId);
if (refreshed) applyLotsFromPickOrder(refreshed);
} catch (e) {
} catch (e) {
console.error(e);
console.error(e);
setError(t("Load workbench data failed"));
setError(t("Load workbench data failed"));
setLotRows([]);
} finally {
} finally {
setLoading(false);
setLoading(false);
}
}
},
},
[t, userId],
[applyLotsFromPickOrder, t, userId],
);
);
const refreshWorkbenchAfterMutation = useCallback(async () => {
const latestOrders = (await loadHierarchicalPickOrders(true)) || [];
if (
selectedPickOrderId != null &&
!latestOrders.some((p) => p.pickOrderId === selectedPickOrderId)
) {
setSelectedPickOrderId(null);
setLotRows([]);
setQtyBySolId({});
setQtyEditableBySolId({});
setLotPagingController({ pageNum: 0, pageSize: -1 });
return;
}
if (selectedPickOrderId != null) {
const po = latestOrders.find((p) => p.pickOrderId === selectedPickOrderId);
if (po) applyLotsFromPickOrder(po);
}
}, [applyLotsFromPickOrder, loadHierarchicalPickOrders, selectedPickOrderId]);
useEffect(() => {
if (userId) void loadHierarchicalPickOrders(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, filterArgs, loadHierarchicalPickOrders]);
const submitRow = useCallback(
const submitRow = useCallback(
async (row: LotRow, forceQty?: number) => {
async (row: LotRow, forceQty?: number) => {
if (!userId) return;
if (!userId) return;
@@ -605,13 +568,10 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
setQrScanError(false);
setQrScanError(false);
setQrScanSuccess(true);
setQrScanSuccess(true);
});
});
if (selectedPickOrderId != null && selectedPickOrderLineId != null && selectedTopMeta) {
await loadLineDetailV2(selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta);
}
setWorkbenchLotLabelModalOpen(false);
setWorkbenchLotLabelModalOpen(false);
setWorkbenchLotLabelContextLot(null);
setWorkbenchLotLabelContextLot(null);
setWorkbenchLotLabelInitialPayload(null);
setWorkbenchLotLabelInitialPayload(null);
await refreshReleasedTopRows AfterMutation();
await refreshWorkbench AfterMutation();
} catch (e) {
} catch (e) {
console.error(e);
console.error(e);
setError(t("Scan pick failed"));
setError(t("Scan pick failed"));
@@ -624,7 +584,7 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
setSubmittingSolId(null);
setSubmittingSolId(null);
}
}
},
},
[qtyBySolId, loadLineDetailV2, refreshReleasedTopRowsAfterMutation, selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta , t, userId],
[qtyBySolId, refreshWorkbenchAfterMutation , t, userId],
);
);
const hasQtyOverrideBySolId = useCallback(
const hasQtyOverrideBySolId = useCallback(
@@ -638,6 +598,15 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
if (typeof override === "number" && Number.isFinite(override) && override >= 0) {
if (typeof override === "number" && Number.isFinite(override) && override >= 0) {
return override;
return override;
}
}
const st = String(lot.status || "").toLowerCase();
if (
st === "completed" ||
st === "partially_completed" ||
st === "partially_complete"
) {
return Number(lot.pickedQty ?? lot.stockOutLineQty ?? 0);
}
if (isWorkbenchZeroCompleteLot(lot)) return 0;
return Number(lot.requiredQty) || 0;
return Number(lot.requiredQty) || 0;
},
},
[qtyBySolId],
[qtyBySolId],
@@ -657,6 +626,14 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
const resolveLockedSubmitQtyDisplay = useCallback(
const resolveLockedSubmitQtyDisplay = useCallback(
(lot: LotRow): number => {
(lot: LotRow): number => {
if (isWorkbenchZeroCompleteLot(lot)) return 0;
if (isWorkbenchZeroCompleteLot(lot)) return 0;
const st = String(lot.status || "").toLowerCase();
if (
st === "completed" ||
st === "partially_completed" ||
st === "partially_complete"
) {
return Number(lot.pickedQty ?? lot.stockOutLineQty ?? 0);
}
return resolveSingleSubmitQty(lot);
return resolveSingleSubmitQty(lot);
},
},
[resolveSingleSubmitQty],
[resolveSingleSubmitQty],
@@ -686,6 +663,8 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
const lotNo = String(row.lotNo || "").trim();
const lotNo = String(row.lotNo || "").trim();
const isZeroComplete = isWorkbenchZeroCompleteLot(row);
const isZeroComplete = isWorkbenchZeroCompleteLot(row);
const isNoLot = isNoLotWorkbenchRow(row);
const isUnavailable = isInventoryLotLineUnavailable(row);
const hasExplicitOverride = hasQtyOverrideBySolId(row.stockOutLineId);
const hasExplicitOverride = hasQtyOverrideBySolId(row.stockOutLineId);
const explicitQty = hasExplicitOverride ? Number(qtyBySolId[row.stockOutLineId]) : NaN;
const explicitQty = hasExplicitOverride ? Number(qtyBySolId[row.stockOutLineId]) : NaN;
const qtyPayload = workbenchScanPickQtyFromLot(row);
const qtyPayload = workbenchScanPickQtyFromLot(row);
@@ -693,6 +672,8 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
const canPostScanPick =
const canPostScanPick =
isZeroComplete ||
isZeroComplete ||
isNoLot ||
isUnavailable ||
(lotNo !== "" &&
(lotNo !== "" &&
((hasExplicitOverride && Number.isFinite(explicitQty) && explicitQty === 0) ||
((hasExplicitOverride && Number.isFinite(explicitQty) && explicitQty === 0) ||
(wbJustQty != null && wbJustQty > 0)));
(wbJustQty != null && wbJustQty > 0)));
@@ -709,7 +690,10 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}
const qtyToSend =
const qtyToSend =
isZeroComplete || (hasExplicitOverride && Number.isFinite(explicitQty) && explicitQty === 0)
isZeroComplete ||
isNoLot ||
isUnavailable ||
(hasExplicitOverride && Number.isFinite(explicitQty) && explicitQty === 0)
? 0
? 0
: Number(wbJustQty);
: Number(wbJustQty);
@@ -718,41 +702,28 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
[hasQtyOverrideBySolId, qtyBySolId, submitRow, t, workbenchScanPickQtyFromLot],
[hasQtyOverrideBySolId, qtyBySolId, submitRow, t, workbenchScanPickQtyFromLot],
);
);
const handleLine Select = useCallback(
async (row: TopRow, checked: boolean) => {
const handlePickOrder Select = useCallback(
async (row: PickOrder TopRow, checked: boolean) => {
if (!checked) {
if (!checked) {
if (selectedPickOrderLineId === row.pickOrderLineId) {
setSelectedPickOrderLineId(null);
if (selectedPickOrderId === row.pickOrderId) {
setSelectedPickOrderId(null);
setSelectedPickOrderId(null);
setSelectedTopMeta(null);
setLotRows([]);
setLotRows([]);
setQtyBySolId({});
setQtyBySolId({});
setQtyEditableBySolId({});
setQtyEditableBySolId({});
setLotPagingController({ pageNum: 0, pageSize: 10 });
setLotPagingController({ pageNum: 0, pageSize: - 1 });
}
}
return;
return;
}
}
setSelectedPickOrderLineId(row.pickOrderLineId);
setSelectedPickOrderId(row.pickOrderId);
setSelectedPickOrderId(row.pickOrderId);
setSelectedTopMeta({
pickOrderCode: row.pickOrderCode,
itemCode: row.itemCode,
itemName: row.itemName,
totalAvailableQty: row.currentStock,
});
setLotRows([]);
setLotRows([]);
setQtyBySolId({});
setQtyBySolId({});
setQtyEditableBySolId({});
setQtyEditableBySolId({});
setLotPagingController({ pageNum: 0, pageSize: 10 });
setLotPagingController({ pageNum: 0, pageSize: -1 });
setMessage("");
setMessage("");
await loadLineDetailV2(row.pickOrderId, row.pickOrderLineId, {
pickOrderCode: row.pickOrderCode,
itemCode: row.itemCode,
itemName: row.itemName,
totalAvailableQty: row.currentStock,
});
applyLotsFromPickOrder(row);
await ensurePickOrderLotsIfNeeded(row);
},
},
[loadLineDetailV2, selectedPickOrderLine Id],
[applyLotsFromPickOrder, ensurePickOrderLotsIfNeeded, selectedPickOrderId],
);
);
const openWorkbenchLotLabelModalForLot = useCallback(
const openWorkbenchLotLabelModalForLot = useCallback(
@@ -854,20 +825,15 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
if (res.code !== "SUCCESS") {
if (res.code !== "SUCCESS") {
throw new Error((res.message as string) || t("Scan pick failed"));
throw new Error((res.message as string) || t("Scan pick failed"));
}
}
if (selectedPickOrderId != null && selectedPickOrderLineId != null && selectedTopMeta) {
await loadLineDetailV2(selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta);
}
await refreshWorkbenchAfterMutation();
setWorkbenchLotLabelModalOpen(false);
setWorkbenchLotLabelModalOpen(false);
setWorkbenchLotLabelReminderText(null);
setWorkbenchLotLabelReminderText(null);
setWorkbenchLotLabelContextLot(null);
setWorkbenchLotLabelContextLot(null);
setWorkbenchLotLabelInitialPayload(null);
setWorkbenchLotLabelInitialPayload(null);
},
},
[
[
loadLineDetailV2 ,
refreshWorkbenchAfterMutation ,
resolveLockedSubmitQtyDisplay,
resolveLockedSubmitQtyDisplay,
selectedPickOrderId,
selectedPickOrderLineId,
selectedTopMeta,
t,
t,
userId,
userId,
workbenchLotLabelContextLot,
workbenchLotLabelContextLot,
@@ -1047,10 +1013,7 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
setQrScanSuccess(true);
setQrScanSuccess(true);
setQrScanSuccessMsg(t("Scan pick success"));
setQrScanSuccessMsg(t("Scan pick success"));
});
});
if (selectedPickOrderId != null && selectedPickOrderLineId != null && selectedTopMeta) {
await loadLineDetailV2(selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta);
}
await refreshReleasedTopRowsAfterMutation();
await refreshWorkbenchAfterMutation();
clearLotConfirmationState(true);
clearLotConfirmationState(true);
} catch (e) {
} catch (e) {
console.error(e);
console.error(e);
@@ -1069,12 +1032,8 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
[
[
clearLotConfirmationState,
clearLotConfirmationState,
expectedLotData,
expectedLotData,
loadLineDetailV2,
refreshReleasedTopRowsAfterMutation,
refreshWorkbenchAfterMutation,
scannedLotData,
scannedLotData,
selectedPickOrderId,
selectedPickOrderLineId,
selectedTopMeta,
t,
t,
userId,
userId,
workbenchScanPickQtyFromLot,
workbenchScanPickQtyFromLot,
@@ -1347,11 +1306,11 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
}, [isScanning, startScan, userId]);
}, [isScanning, startScan, userId]);
useEffect(() => {
useEffect(() => {
if (!selectedPickOrderLine Id) {
if (!selectedPickOrderId) {
lastProcessedQrRef.current = "";
lastProcessedQrRef.current = "";
processedQrCodesRef.current.clear();
processedQrCodesRef.current.clear();
}
}
}, [selectedPickOrderLine Id]);
}, [selectedPickOrderId]);
useEffect(() => {
useEffect(() => {
if (!qrValues.length || lotRows.length === 0) return;
if (!qrValues.length || lotRows.length === 0) return;
@@ -1443,7 +1402,9 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
<Stack spacing={2}>
<Stack spacing={2}>
<Paper variant="outlined" sx={{ p: 2 }}>
<Paper variant="outlined" sx={{ p: 2 }}>
<Stack spacing={1}>
<Stack spacing={1}>
<SearchBox criteria={searchCriteria} onSearch={handleSearch} onReset={handleReset} />
<Typography variant="subtitle2" color="text.secondary">
{t("Pick Orders")}
</Typography>
<Grid container rowGap={1}>
<Grid container rowGap={1}>
<Grid item xs={12}>
<Grid item xs={12}>
{pickOrderLoading ? (
{pickOrderLoading ? (
@@ -1455,41 +1416,33 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
<TableRow>
<TableRow>
<TableCell>{t("Selected")}</TableCell>
<TableCell>{t("Selected")}</TableCell>
<TableCell>{t("Pick Order Code")}</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("Pick Order Lines")}</TableCell>
<TableCell>{t("Target Date")}</TableCell>
<TableCell>{t("Target Date")}</TableCell>
<TableCell>{t("Status")}</TableCell>
<TableCell>{t("Status")}</TableCell>
</TableRow>
</TableRow>
</TableHead>
</TableHead>
<TableBody>
<TableBody>
{paginatedTopRow s.length === 0 ? (
{paginatedPickOrder s.length === 0 ? (
<TableRow>
<TableRow>
<TableCell colSpan={10 }>
<TableCell colSpan={5 }>
<Typography variant="body2" color="text.secondary">
<Typography variant="body2" color="text.secondary">
{t("No data available")}
{t("No data available")}
</Typography>
</Typography>
</TableCell>
</TableCell>
</TableRow>
</TableRow>
) : (
) : (
paginatedTopRow s.map((row) => (
paginatedPickOrder s.map((row) => (
<TableRow key={row.rowKey}>
<TableRow key={row.rowKey}>
<TableCell padding="checkbox">
<TableCell padding="checkbox">
<Checkbox
<Checkbox
checked={selectedPickOrderLine Id === row.pickOrderLine Id}
onChange={(_, checked) => void handleLine Select(row, checked)}
checked={selectedPickOrderId === row.pickOrderId}
onChange={(_, checked) => void handlePickOrder Select(row, checked)}
/>
/>
</TableCell>
</TableCell>
<TableCell>{row.pickOrderCode || "-"}</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>
{row.completedLineCount}/{row.lineCount}
</TableCell>
<TableCell>{safeDisplayTargetDate(row.targetDate)}</TableCell>
<TableCell>{safeDisplayTargetDate(row.targetDate)}</TableCell>
<TableCell>{t(row.status || "-")}</TableCell>
<TableCell>{t(row.status || "-")}</TableCell>
</TableRow>
</TableRow>
@@ -1503,12 +1456,14 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
<Grid item xs={12}>
<Grid item xs={12}>
<TablePagination
<TablePagination
component="div"
component="div"
count={totalCountItems}
page={pagingController.pageNum - 1}
rowsPerPage={pagingController.pageSize}
onPageChange={(_e, newPage) => setPagingController((prev) => ({ ...prev, pageNum: newPage + 1 }))}
count={totalCountPickOrders}
page={poPagingController.pageNum - 1}
rowsPerPage={poPagingController.pageSize}
onPageChange={(_e, newPage) =>
setPoPagingController((prev) => ({ ...prev, pageNum: newPage + 1 }))
}
onRowsPerPageChange={(e) =>
onRowsPerPageChange={(e) =>
setPagingController({
setPoP agingController({
pageNum: 1,
pageNum: 1,
pageSize: parseInt(e.target.value, 10),
pageSize: parseInt(e.target.value, 10),
})
})
@@ -1560,6 +1515,7 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
? Number(qtyBySolId[r.stockOutLineId])
? Number(qtyBySolId[r.stockOutLineId])
: lockedSubmitQty;
: lockedSubmitQty;
const rowStatus = String(r.status || "").toLowerCase();
const rowStatus = String(r.status || "").toLowerCase();
const isNoLot = isNoLotWorkbenchRow(r);
const isRowRejected =
const isRowRejected =
rowStatus === "rejected" || String(r.lotAvailability || "").toLowerCase() === "rejected";
rowStatus === "rejected" || String(r.lotAvailability || "").toLowerCase() === "rejected";
const isRowExpired =
const isRowExpired =
@@ -1568,17 +1524,15 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
return (
return (
<TableRow key={r.key}>
<TableRow key={r.key}>
<TableCell>{idx === 0 ? lotPagingController.pageNum * lotPagingController.pageSize + 1 : ""}</TableCell>
<TableCell>
<TableCell>
{idx === 0 ? (
<>
{r.itemCode || "-"} <br />
{r.itemName || "-"} <br />
{r.uomDesc || "-"}
</>
) : (
""
)}
{lotPagingController.pageSize === -1
? idx + 1
: lotPagingController.pageNum * lotPagingController.pageSize + idx + 1}
</TableCell>
<TableCell>
{r.itemCode || "-"} <br />
{r.itemName || "-"} <br />
{r.uomDesc || "-"}
</TableCell>
</TableCell>
<TableCell>{r.location || "-"}</TableCell>
<TableCell>{r.location || "-"}</TableCell>
<TableCell>
<TableCell>
@@ -1608,7 +1562,11 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
r.lotNo
r.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>
</Typography>
{r.stockOutLineId > 0 ? (
{r.stockOutLineId > 0 ? (
@@ -1657,6 +1615,27 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
/>
/>
);
);
}
}
if (
isNoLot &&
(rowStatus === "partially_completed" ||
rowStatus === "partially_complete" ||
rowStatus === "completed")
) {
return (
<Checkbox
checked
disabled
size="small"
sx={{
color: "error.main",
"&.Mui-checked": { color: "error.main" },
}}
/>
);
}
if (isNoLot) {
return null;
}
return (
return (
<Checkbox
<Checkbox
checked={isCompletedStatus(r.status) || isCheckedStatus(r.status)}
checked={isCompletedStatus(r.status) || isCheckedStatus(r.status)}
@@ -1759,7 +1738,7 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
<TableRow>
<TableRow>
<TableCell colSpan={9} align="center" sx={{ textAlign: "center" }}>
<TableCell colSpan={9} align="center" sx={{ textAlign: "center" }}>
<Typography variant="body2" color="text.secondary" align="center">
<Typography variant="body2" color="text.secondary" align="center">
{t("No lot rows. Select a line in the table above.")}
{t("No lot rows. Select a pick order above.")}
</Typography>
</Typography>
</TableCell>
</TableCell>
</TableRow>
</TableRow>
@@ -1773,13 +1752,14 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
page={lotPagingController.pageNum}
page={lotPagingController.pageNum}
rowsPerPage={lotPagingController.pageSize}
rowsPerPage={lotPagingController.pageSize}
onPageChange={(_e, newPage) => setLotPagingController((prev) => ({ ...prev, pageNum: newPage }))}
onPageChange={(_e, newPage) => setLotPagingController((prev) => ({ ...prev, pageNum: newPage }))}
onRowsPerPageChange={(e) =>
onRowsPerPageChange={(e) => {
const newPageSize = parseInt(e.target.value, 10);
setLotPagingController({
setLotPagingController({
pageNum: 0,
pageNum: 0,
pageSize: parseInt(e.target.value, 10) ,
})
}
rowsPerPageOptions={[10, 25, 50]}
pageSize: newPageSize === -1 ? -1 : newPageSize ,
});
}}
rowsPerPageOptions={[10, 25, 50, -1 ]}
labelRowsPerPage={t("Rows per page")}
labelRowsPerPage={t("Rows per page")}
/>
/>
</Paper>
</Paper>