From e39365f4f8af830a7c235c26866f45218bb39724 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 15 Sep 2026 14:57:47 +0800 Subject: [PATCH] =?UTF-8?q?#24=20/=20#25=20/=20#26=EF=BC=89#27=20#22=20#4/?= =?UTF-8?q?#6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/inventory/actions.ts | 18 ++-- src/app/api/inventory/index.ts | 1 + src/app/api/inventory/inventoryBucket.ts | 82 +++++++++++++++++++ src/app/api/jo/actions.ts | 1 + src/app/api/jo/index.ts | 2 + .../InventorySearch/InventorySearch.tsx | 7 +- src/components/JoSave/JoRelease.tsx | 18 ++-- src/components/JoSave/PickTable.tsx | 18 ++-- src/components/JoSearch/JoSearch.tsx | 18 ++-- .../JoWorkbench/JoWorkbenchSearch.tsx | 18 ++-- .../ProductionProcessJobOrderDetail.tsx | 15 ++-- 11 files changed, 141 insertions(+), 57 deletions(-) create mode 100644 src/app/api/inventory/inventoryBucket.ts diff --git a/src/app/api/inventory/actions.ts b/src/app/api/inventory/actions.ts index fb7d8643..8cca8117 100644 --- a/src/app/api/inventory/actions.ts +++ b/src/app/api/inventory/actions.ts @@ -161,30 +161,22 @@ async function fetchInventoriesImpl(data: SearchInventory) { ); } -async function fetchInventoriesLatestImpl(data: SearchInventory) { - const queryStr = convertObjToURLSearchParams(data); - return serverFetchJson( - `${BASE_API_URL}/inventory/searchLatest/getRecordByPage?${queryStr}`, - { next: { tags: ["inventories"] } }, - ); -} - export const fetchInventories = cache(fetchInventoriesImpl); /** - * FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.3 | 2026-09-07 - * Inventory search page: latest inventory row per item (no baseUnit/uomId filter). + * ChangeList #24 — same as fetchInventories (all stock-UOM buckets). + * Backend `/inventory/searchLatest/getRecordByPage` is deprecated and aliases getRecordByPage. */ -export const fetchInventoriesLatest = cache(fetchInventoriesLatestImpl); +export const fetchInventoriesLatest = cache(fetchInventoriesImpl); /** Bypass React cache() after mutations so lists show fresh qty. */ export async function fetchInventoriesFresh(data: SearchInventory) { return fetchInventoriesImpl(data); } -/** Bypass React cache() for inventory search page latest-inventory search. */ +/** @deprecated Use fetchInventoriesFresh — same full-page search. */ export async function fetchInventoriesLatestFresh(data: SearchInventory) { - return fetchInventoriesLatestImpl(data); + return fetchInventoriesImpl(data); } async function fetchInventoryLotLinesImpl(data: SearchInventoryLotLine) { diff --git a/src/app/api/inventory/index.ts b/src/app/api/inventory/index.ts index 869bed2e..8002224c 100644 --- a/src/app/api/inventory/index.ts +++ b/src/app/api/inventory/index.ts @@ -14,6 +14,7 @@ export interface InventoryResult { onHoldQty: number; unavailableQty: number; availableQty: number; + stockUomId?: number | null; uomCode: string; uomUdfudesc: string; uomShortDesc: string; diff --git a/src/app/api/inventory/inventoryBucket.ts b/src/app/api/inventory/inventoryBucket.ts new file mode 100644 index 00000000..52b6199c --- /dev/null +++ b/src/app/api/inventory/inventoryBucket.ts @@ -0,0 +1,82 @@ +/** Keys used to pick one inventory row when an item has multiple stock-UOM buckets. */ +export type InventoryBucketPick = { + itemId?: number | null; + itemCode?: string | null; + itemName?: string | null; + uomId?: number | null; + stockUomId?: number | null; + uom?: string | null; + shortUom?: string | null; + stockUom?: string | null; +}; + +export type InventoryBucketRow = { + itemId?: number | null; + itemCode?: string | null; + itemName?: string | null; + stockUomId?: number | null; + uomCode?: string | null; + uomUdfudesc?: string | null; + uomShortDesc?: string | null; + availableQty?: number | null; + onHandQty?: number | null; + unavailableQty?: number | null; +}; + +const norm = (s?: string | null) => (s ?? "").trim().toLowerCase(); + +/** Available = onHand − unavailable. Do not treat 0 as missing (`||` is wrong). */ +export function inventoryAvailableQty(inv: InventoryBucketRow): number { + if (inv.availableQty != null && !Number.isNaN(Number(inv.availableQty))) { + return Number(inv.availableQty); + } + return Number(inv.onHandQty ?? 0) - Number(inv.unavailableQty ?? 0); +} + +function itemMatches(inv: InventoryBucketRow, pick: InventoryBucketPick): boolean { + if (pick.itemId != null && inv.itemId != null) { + return Number(inv.itemId) === Number(pick.itemId); + } + if (pick.itemCode && inv.itemCode) { + return inv.itemCode === pick.itemCode; + } + if (pick.itemName && inv.itemName) { + return inv.itemName === pick.itemName; + } + return false; +} + +function uomMatches(inv: InventoryBucketRow, pick: InventoryBucketPick): boolean { + const pickUomId = pick.stockUomId ?? pick.uomId; + if (pickUomId != null && inv.stockUomId != null) { + return Number(inv.stockUomId) === Number(pickUomId); + } + const labels = [pick.uom, pick.shortUom, pick.stockUom].map(norm).filter(Boolean); + if (labels.length === 0) return true; + const invLabels = [inv.uomUdfudesc, inv.uomShortDesc, inv.uomCode].map(norm); + return labels.some((l) => invLabels.includes(l)); +} + +export function matchInventoryBucket( + inventories: InventoryBucketRow[], + pick: InventoryBucketPick, +): InventoryBucketRow | undefined { + const itemHits = inventories.filter((inv) => itemMatches(inv, pick)); + if (itemHits.length === 0) return undefined; + const hasUomPick = + pick.stockUomId != null || + pick.uomId != null || + [pick.uom, pick.shortUom, pick.stockUom].some((s) => Boolean(norm(s))); + if (hasUomPick) { + return itemHits.find((inv) => uomMatches(inv, pick)); + } + return itemHits[0]; +} + +export function getStockAvailableFromInventories( + inventories: InventoryBucketRow[], + pick: InventoryBucketPick, +): number { + const inv = matchInventoryBucket(inventories, pick); + return inv ? inventoryAvailableQty(inv) : 0; +} diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 75efe315..87d6fcfb 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -533,6 +533,7 @@ export interface JobOrderLineInfo { stockUom: string, stockBaseUom: string, + stockUomId?: number | null, availableStatus: string, bomProcessId: number, diff --git a/src/app/api/jo/index.ts b/src/app/api/jo/index.ts index ed8b99f4..67bd46e7 100644 --- a/src/app/api/jo/index.ts +++ b/src/app/api/jo/index.ts @@ -67,6 +67,8 @@ export interface JoDetail { export interface JoDetailPickLine { id: number; + itemId?: number; + uomId?: number; code: string; name: string; type: string; diff --git a/src/components/InventorySearch/InventorySearch.tsx b/src/components/InventorySearch/InventorySearch.tsx index a59ff224..8d278b6a 100644 --- a/src/components/InventorySearch/InventorySearch.tsx +++ b/src/components/InventorySearch/InventorySearch.tsx @@ -12,7 +12,7 @@ import { analyzeQrCode, SearchInventory, SearchInventoryLotLine, - fetchInventoriesLatest, + fetchInventories, fetchInventoryLotLines, } from '@/app/api/inventory/actions'; import { PrinterCombo } from '@/app/api/settings/printer'; @@ -85,6 +85,7 @@ const InventorySearch: React.FC = ({ inventories, printerCombo }) => { uomCode: item.uom || uom, uomUdfudesc: uom, uomShortDesc: item.uom || uom, + stockUomId: undefined, qtyPerSmallestUnit: 1, baseUom: uom, price: 0, @@ -208,7 +209,7 @@ const InventorySearch: React.FC = ({ inventories, printerCombo }) => { pageSize: pagingController.pageSize, }; - const response = await fetchInventoriesLatest(params); + const response = await fetchInventories(params); if (response) { setInventoriesTotalCount(response.total); @@ -220,7 +221,7 @@ const InventorySearch: React.FC = ({ inventories, printerCombo }) => { break; case 'paging': setFilteredInventories((fi) => - uniqBy([...fi, ...response.records], 'itemId'), + uniqBy([...fi, ...response.records], 'id'), ); } } diff --git a/src/components/JoSave/JoRelease.tsx b/src/components/JoSave/JoRelease.tsx index fb704344..2b81b9d8 100644 --- a/src/components/JoSave/JoRelease.tsx +++ b/src/components/JoSave/JoRelease.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { JoDetailPickLine } from "@/app/api/jo"; import { fetchInventories } from "@/app/api/inventory/actions"; import { InventoryResult } from "@/app/api/inventory"; +import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket"; import { useEffect, useState, useMemo } from "react"; import { useFormContext } from "react-hook-form"; import { JoDetail } from "@/app/api/jo"; @@ -50,15 +51,14 @@ const JoRelease: React.FC = ({ }, [pickLines]); const getStockAvailable = (pickLine: JoDetailPickLine) => { - const inventory = inventoryData.find(inventory => - inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name - ); - - if (inventory) { - return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); - } - - return 0; + return getStockAvailableFromInventories(inventoryData, { + itemId: pickLine.itemId, + itemCode: pickLine.code, + itemName: pickLine.name, + uomId: pickLine.uomId, + uom: pickLine.uom, + shortUom: pickLine.shortUom, + }); }; const isStockSufficient = (pickLine: JoDetailPickLine) => { diff --git a/src/components/JoSave/PickTable.tsx b/src/components/JoSave/PickTable.tsx index 91237f8c..ebe63d3e 100644 --- a/src/components/JoSave/PickTable.tsx +++ b/src/components/JoSave/PickTable.tsx @@ -12,6 +12,7 @@ import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutli import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; import { fetchInventories } from "@/app/api/inventory/actions"; import { InventoryResult } from "@/app/api/inventory"; +import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket"; import { useEffect, useState } from "react"; import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded'; @@ -54,15 +55,14 @@ const PickTable: React.FC = ({ }, [pickLines]); const getStockAvailable = (pickLine: JoDetailPickLine) => { - const inventory = inventoryData.find(inventory => - inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name - ); - - if (inventory) { - return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); - } - - return 0; + return getStockAvailableFromInventories(inventoryData, { + itemId: pickLine.itemId, + itemCode: pickLine.code, + itemName: pickLine.name, + uomId: pickLine.uomId, + uom: pickLine.uom, + shortUom: pickLine.shortUom, + }); }; const isStockSufficient = (pickLine: JoDetailPickLine) => { diff --git a/src/components/JoSearch/JoSearch.tsx b/src/components/JoSearch/JoSearch.tsx index c6bbe1f9..c7ed6376 100644 --- a/src/components/JoSearch/JoSearch.tsx +++ b/src/components/JoSearch/JoSearch.tsx @@ -25,6 +25,7 @@ import { msg } from "../Swal/CustomAlerts"; import dayjs from "dayjs"; //import { fetchInventories } from "@/app/api/inventory/actions"; import { InventoryResult } from "@/app/api/inventory"; +import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket"; import { PrinterCombo } from "@/app/api/settings/printer"; import { JobTypeResponse } from "@/app/api/jo/actions"; import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; @@ -132,15 +133,14 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT */ const getStockAvailable = (pickLine: JoDetailPickLine) => { - const inventory = inventoryData.find(inventory => - inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name - ); - - if (inventory) { - return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); - } - - return 0; + return getStockAvailableFromInventories(inventoryData, { + itemId: pickLine.itemId, + itemCode: pickLine.code, + itemName: pickLine.name, + uomId: pickLine.uomId, + uom: pickLine.uom, + shortUom: pickLine.shortUom, + }); }; const isStockSufficient = (pickLine: JoDetailPickLine) => { diff --git a/src/components/JoWorkbench/JoWorkbenchSearch.tsx b/src/components/JoWorkbench/JoWorkbenchSearch.tsx index 54792aa8..def91204 100644 --- a/src/components/JoWorkbench/JoWorkbenchSearch.tsx +++ b/src/components/JoWorkbench/JoWorkbenchSearch.tsx @@ -26,6 +26,7 @@ import { msg } from "../Swal/CustomAlerts"; import dayjs from "dayjs"; //import { fetchInventories } from "@/app/api/inventory/actions"; import { InventoryResult } from "@/app/api/inventory"; +import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket"; import { PrinterCombo } from "@/app/api/settings/printer"; import { JobTypeResponse } from "@/app/api/jo/actions"; import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; @@ -133,15 +134,14 @@ const JoWorkbenchSearch: React.FC = ({ defaultInputs, bomCombo, printerCo */ const getStockAvailable = (pickLine: JoDetailPickLine) => { - const inventory = inventoryData.find(inventory => - inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name - ); - - if (inventory) { - return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); - } - - return 0; + return getStockAvailableFromInventories(inventoryData, { + itemId: pickLine.itemId, + itemCode: pickLine.code, + itemName: pickLine.name, + uomId: pickLine.uomId, + uom: pickLine.uom, + shortUom: pickLine.shortUom, + }); }; const isStockSufficient = (pickLine: JoDetailPickLine) => { diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx index dbb73b76..78818a53 100644 --- a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -36,6 +36,7 @@ import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutli import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded'; import { fetchInventories } from "@/app/api/inventory/actions"; import { InventoryResult } from "@/app/api/inventory"; +import { matchInventoryBucket, inventoryAvailableQty } from "@/app/api/inventory/inventoryBucket"; import { releaseJoForWorkbench } from "@/app/api/jo/workbenchActions"; import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; import ProcessSummaryHeader from "./ProcessSummaryHeader"; @@ -187,13 +188,17 @@ const getStockAvailable = (line: JobOrderLineInfo) => { if (line.type?.toLowerCase() === "consumables" || line.type?.toLowerCase() === "nm") { return line.stockQty || 0; } - const inventory = inventoryData.find(inv => - inv.itemCode === line.itemCode || inv.itemName === line.itemName - ); + const inventory = matchInventoryBucket(inventoryData, { + itemId: line.itemId, + itemCode: line.itemCode, + itemName: line.itemName, + stockUomId: line.stockUomId, + stockUom: line.stockUom, + }); if (inventory) { - return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); + return inventoryAvailableQty(inventory); } - return line.stockQty || 0; + return line.stockQty ?? 0; }; const handleOpenPlanStartDialog = useCallback(() => { // 将 processData.date 转换为 dayjs 对象