| @@ -161,30 +161,22 @@ async function fetchInventoriesImpl(data: SearchInventory) { | |||||
| ); | ); | ||||
| } | } | ||||
| async function fetchInventoriesLatestImpl(data: SearchInventory) { | |||||
| const queryStr = convertObjToURLSearchParams(data); | |||||
| return serverFetchJson<InventoryResultByPage>( | |||||
| `${BASE_API_URL}/inventory/searchLatest/getRecordByPage?${queryStr}`, | |||||
| { next: { tags: ["inventories"] } }, | |||||
| ); | |||||
| } | |||||
| export const fetchInventories = cache(fetchInventoriesImpl); | 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. */ | /** Bypass React cache() after mutations so lists show fresh qty. */ | ||||
| export async function fetchInventoriesFresh(data: SearchInventory) { | export async function fetchInventoriesFresh(data: SearchInventory) { | ||||
| return fetchInventoriesImpl(data); | 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) { | export async function fetchInventoriesLatestFresh(data: SearchInventory) { | ||||
| return fetchInventoriesLatestImpl(data); | |||||
| return fetchInventoriesImpl(data); | |||||
| } | } | ||||
| async function fetchInventoryLotLinesImpl(data: SearchInventoryLotLine) { | async function fetchInventoryLotLinesImpl(data: SearchInventoryLotLine) { | ||||
| @@ -14,6 +14,7 @@ export interface InventoryResult { | |||||
| onHoldQty: number; | onHoldQty: number; | ||||
| unavailableQty: number; | unavailableQty: number; | ||||
| availableQty: number; | availableQty: number; | ||||
| stockUomId?: number | null; | |||||
| uomCode: string; | uomCode: string; | ||||
| uomUdfudesc: string; | uomUdfudesc: string; | ||||
| uomShortDesc: string; | uomShortDesc: string; | ||||
| @@ -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; | |||||
| } | |||||
| @@ -533,6 +533,7 @@ export interface JobOrderLineInfo { | |||||
| stockUom: string, | stockUom: string, | ||||
| stockBaseUom: string, | stockBaseUom: string, | ||||
| stockUomId?: number | null, | |||||
| availableStatus: string, | availableStatus: string, | ||||
| bomProcessId: number, | bomProcessId: number, | ||||
| @@ -67,6 +67,8 @@ export interface JoDetail { | |||||
| export interface JoDetailPickLine { | export interface JoDetailPickLine { | ||||
| id: number; | id: number; | ||||
| itemId?: number; | |||||
| uomId?: number; | |||||
| code: string; | code: string; | ||||
| name: string; | name: string; | ||||
| type: string; | type: string; | ||||
| @@ -12,7 +12,7 @@ import { | |||||
| analyzeQrCode, | analyzeQrCode, | ||||
| SearchInventory, | SearchInventory, | ||||
| SearchInventoryLotLine, | SearchInventoryLotLine, | ||||
| fetchInventoriesLatest, | |||||
| fetchInventories, | |||||
| fetchInventoryLotLines, | fetchInventoryLotLines, | ||||
| } from '@/app/api/inventory/actions'; | } from '@/app/api/inventory/actions'; | ||||
| import { PrinterCombo } from '@/app/api/settings/printer'; | import { PrinterCombo } from '@/app/api/settings/printer'; | ||||
| @@ -85,6 +85,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||||
| uomCode: item.uom || uom, | uomCode: item.uom || uom, | ||||
| uomUdfudesc: uom, | uomUdfudesc: uom, | ||||
| uomShortDesc: item.uom || uom, | uomShortDesc: item.uom || uom, | ||||
| stockUomId: undefined, | |||||
| qtyPerSmallestUnit: 1, | qtyPerSmallestUnit: 1, | ||||
| baseUom: uom, | baseUom: uom, | ||||
| price: 0, | price: 0, | ||||
| @@ -208,7 +209,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||||
| pageSize: pagingController.pageSize, | pageSize: pagingController.pageSize, | ||||
| }; | }; | ||||
| const response = await fetchInventoriesLatest(params); | |||||
| const response = await fetchInventories(params); | |||||
| if (response) { | if (response) { | ||||
| setInventoriesTotalCount(response.total); | setInventoriesTotalCount(response.total); | ||||
| @@ -220,7 +221,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||||
| break; | break; | ||||
| case 'paging': | case 'paging': | ||||
| setFilteredInventories((fi) => | setFilteredInventories((fi) => | ||||
| uniqBy([...fi, ...response.records], 'itemId'), | |||||
| uniqBy([...fi, ...response.records], 'id'), | |||||
| ); | ); | ||||
| } | } | ||||
| } | } | ||||
| @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; | |||||
| import { JoDetailPickLine } from "@/app/api/jo"; | import { JoDetailPickLine } from "@/app/api/jo"; | ||||
| import { fetchInventories } from "@/app/api/inventory/actions"; | import { fetchInventories } from "@/app/api/inventory/actions"; | ||||
| import { InventoryResult } from "@/app/api/inventory"; | import { InventoryResult } from "@/app/api/inventory"; | ||||
| import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket"; | |||||
| import { useEffect, useState, useMemo } from "react"; | import { useEffect, useState, useMemo } from "react"; | ||||
| import { useFormContext } from "react-hook-form"; | import { useFormContext } from "react-hook-form"; | ||||
| import { JoDetail } from "@/app/api/jo"; | import { JoDetail } from "@/app/api/jo"; | ||||
| @@ -50,15 +51,14 @@ const JoRelease: React.FC<Props> = ({ | |||||
| }, [pickLines]); | }, [pickLines]); | ||||
| const getStockAvailable = (pickLine: JoDetailPickLine) => { | 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) => { | const isStockSufficient = (pickLine: JoDetailPickLine) => { | ||||
| @@ -12,6 +12,7 @@ import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutli | |||||
| import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; | import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; | ||||
| import { fetchInventories } from "@/app/api/inventory/actions"; | import { fetchInventories } from "@/app/api/inventory/actions"; | ||||
| import { InventoryResult } from "@/app/api/inventory"; | import { InventoryResult } from "@/app/api/inventory"; | ||||
| import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket"; | |||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||
| import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded'; | import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded'; | ||||
| @@ -54,15 +55,14 @@ const PickTable: React.FC<Props> = ({ | |||||
| }, [pickLines]); | }, [pickLines]); | ||||
| const getStockAvailable = (pickLine: JoDetailPickLine) => { | 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) => { | const isStockSufficient = (pickLine: JoDetailPickLine) => { | ||||
| @@ -25,6 +25,7 @@ import { msg } from "../Swal/CustomAlerts"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| //import { fetchInventories } from "@/app/api/inventory/actions"; | //import { fetchInventories } from "@/app/api/inventory/actions"; | ||||
| import { InventoryResult } from "@/app/api/inventory"; | import { InventoryResult } from "@/app/api/inventory"; | ||||
| import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket"; | |||||
| import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| import { JobTypeResponse } from "@/app/api/jo/actions"; | import { JobTypeResponse } from "@/app/api/jo/actions"; | ||||
| import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | ||||
| @@ -132,15 +133,14 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| */ | */ | ||||
| const getStockAvailable = (pickLine: JoDetailPickLine) => { | 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) => { | const isStockSufficient = (pickLine: JoDetailPickLine) => { | ||||
| @@ -26,6 +26,7 @@ import { msg } from "../Swal/CustomAlerts"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| //import { fetchInventories } from "@/app/api/inventory/actions"; | //import { fetchInventories } from "@/app/api/inventory/actions"; | ||||
| import { InventoryResult } from "@/app/api/inventory"; | import { InventoryResult } from "@/app/api/inventory"; | ||||
| import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket"; | |||||
| import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| import { JobTypeResponse } from "@/app/api/jo/actions"; | import { JobTypeResponse } from "@/app/api/jo/actions"; | ||||
| import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | ||||
| @@ -133,15 +134,14 @@ const JoWorkbenchSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCo | |||||
| */ | */ | ||||
| const getStockAvailable = (pickLine: JoDetailPickLine) => { | 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) => { | const isStockSufficient = (pickLine: JoDetailPickLine) => { | ||||
| @@ -36,6 +36,7 @@ import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutli | |||||
| import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded'; | import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded'; | ||||
| import { fetchInventories } from "@/app/api/inventory/actions"; | import { fetchInventories } from "@/app/api/inventory/actions"; | ||||
| import { InventoryResult } from "@/app/api/inventory"; | import { InventoryResult } from "@/app/api/inventory"; | ||||
| import { matchInventoryBucket, inventoryAvailableQty } from "@/app/api/inventory/inventoryBucket"; | |||||
| import { releaseJoForWorkbench } from "@/app/api/jo/workbenchActions"; | import { releaseJoForWorkbench } from "@/app/api/jo/workbenchActions"; | ||||
| import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; | import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; | ||||
| import ProcessSummaryHeader from "./ProcessSummaryHeader"; | import ProcessSummaryHeader from "./ProcessSummaryHeader"; | ||||
| @@ -187,13 +188,17 @@ const getStockAvailable = (line: JobOrderLineInfo) => { | |||||
| if (line.type?.toLowerCase() === "consumables" || line.type?.toLowerCase() === "nm") { | if (line.type?.toLowerCase() === "consumables" || line.type?.toLowerCase() === "nm") { | ||||
| return line.stockQty || 0; | 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) { | 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(() => { | const handleOpenPlanStartDialog = useCallback(() => { | ||||
| // 将 processData.date 转换为 dayjs 对象 | // 将 processData.date 转换为 dayjs 对象 | ||||