diff --git a/src/app/(main)/po/edit/page.tsx b/src/app/(main)/po/edit/page.tsx index e7f0667d..5eeabac3 100644 --- a/src/app/(main)/po/edit/page.tsx +++ b/src/app/(main)/po/edit/page.tsx @@ -16,11 +16,11 @@ type Props = {} & SearchParams; const PoEdit: React.FC = async ({ searchParams }) => { const type = "purchaseOrder"; const { t } = await getServerI18n(type); - console.log(searchParams["id"]); + //console.log(searchParams["id"]); const id = isString(searchParams["id"]) ? parseInt(searchParams["id"]) : undefined; - console.log(id); + //console.log(id); if (!id) { notFound(); } diff --git a/src/app/api/inventory/actions.ts b/src/app/api/inventory/actions.ts index 5da0eeb6..b03440c9 100644 --- a/src/app/api/inventory/actions.ts +++ b/src/app/api/inventory/actions.ts @@ -172,7 +172,7 @@ async function fetchInventoriesLatestImpl(data: SearchInventory) { export const fetchInventories = cache(fetchInventoriesImpl); /** - * FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.0 | 2026-07-17 + * FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.1 | 2026-09-07 * Inventory search page: latest inventory row per item (no baseUnit/uomId filter). */ export const fetchInventoriesLatest = cache(fetchInventoriesLatestImpl); diff --git a/src/app/api/settings/item/actions.ts b/src/app/api/settings/item/actions.ts index f6b91807..4ade10f9 100644 --- a/src/app/api/settings/item/actions.ts +++ b/src/app/api/settings/item/actions.ts @@ -46,6 +46,16 @@ export type CreateItemInputs = { isFee?: boolean | undefined; isBag?: boolean | undefined; qcType?: string | undefined; + averageUnitPrice?: string | undefined; + averageUnitPriceEditable?: boolean; + stockUnitLabel?: string | undefined; +}; + +export const recalculateAverageUnitPrices = async () => { + return serverFetchJson<{ updatedItemCount: number }>( + `${BASE_API_URL}/items/averageUnitPrice/recalculate`, + { method: "POST" }, + ); }; export const saveItem = async (data: CreateItemInputs) => { @@ -138,6 +148,23 @@ export const fetchItemsWithDetails = cache(async (searchParams?: Record) => { + const params = new URLSearchParams(); + if (searchParams) { + Object.entries(searchParams).forEach(([key, value]) => { + if (value !== undefined && value !== null && String(value) !== "") { + params.append(key, String(value)); + } + }); + } + const qs = params.toString(); + return serverFetchJson>( + qs ? `${BASE_API_URL}/items/getRecordByPage?${qs}` : `${BASE_API_URL}/items/getRecordByPage`, + { next: { tags: ["items"] } }, + ); +}); + export const fetchAllItemsInClient = cache(async () => { return serverFetchJson(`${BASE_API_URL}/items/consumables`, { next: { tags: ["items"] }, diff --git a/src/app/api/settings/item/index.ts b/src/app/api/settings/item/index.ts index e85933e0..2ee12b97 100644 --- a/src/app/api/settings/item/index.ts +++ b/src/app/api/settings/item/index.ts @@ -66,12 +66,17 @@ export type ItemsResult = { latestMarketUnitPrice?: number; latestMupUpdatedDate?: string; purchaseUnit?: string; + purchaseCurrencyId?: number; + purchaseUnitPrice?: number; + purchaseFxRate?: number; }; export type Result = { item: ItemsResult; qcChecks: ItemQc[]; qcType?: string; + averageUnitPriceEditable?: boolean; + stockUnitLabel?: string; }; export const fetchAllItems = cache(async () => { return serverFetchJson(`${BASE_API_URL}/items`, { diff --git a/src/app/api/stockIssue/actions.ts b/src/app/api/stockIssue/actions.ts index 5461e1ec..ec95c153 100644 --- a/src/app/api/stockIssue/actions.ts +++ b/src/app/api/stockIssue/actions.ts @@ -17,12 +17,15 @@ export interface ExpiryItemResult { storeLocation: string | null; expiryDate: string | null; remainingQty: number; + uomDesc?: string | null; + /** True when expiryDate is today or earlier. */ + canHandle?: boolean; } export interface ExpiryItemFilter { - expiryDate?: string; itemCode?: string; itemName?: string; + lotNo?: string; } export interface HandleBadItemRequest { @@ -54,6 +57,8 @@ export interface StockIssueHandleRecord { export interface SearchStockIssueRecordParams { startDate?: string; endDate?: string; + handledStartDate?: string; + handledEndDate?: string; itemCode?: string; itemName?: string; lotNo?: string; @@ -63,9 +68,9 @@ export interface SearchStockIssueRecordParams { export const fetchExpiryItemList = cache(async (filters?: ExpiryItemFilter) => { const params = new URLSearchParams(); - if (filters?.expiryDate) params.set("expiryDate", filters.expiryDate); if (filters?.itemCode) params.set("itemCode", filters.itemCode); if (filters?.itemName) params.set("itemName", filters.itemName); + if (filters?.lotNo) params.set("lotNo", filters.lotNo); const queryString = params.toString(); const url = `${BASE_API_URL}/pickExecution/issues/expiryItem${queryString ? `?${queryString}` : ""}`; return serverFetchJson(url, { @@ -107,6 +112,8 @@ export async function fetchExpiryItemRecords(params: SearchStockIssueRecordParam const qs = new URLSearchParams(); if (params.startDate) qs.set("startDate", params.startDate); if (params.endDate) qs.set("endDate", params.endDate); + if (params.handledStartDate) qs.set("handledStartDate", params.handledStartDate); + if (params.handledEndDate) qs.set("handledEndDate", params.handledEndDate); if (params.itemCode) qs.set("itemCode", params.itemCode); if (params.itemName) qs.set("itemName", params.itemName); if (params.lotNo) qs.set("lotNo", params.lotNo); diff --git a/src/app/api/stockIssue/client.ts b/src/app/api/stockIssue/client.ts new file mode 100644 index 00000000..357b02da --- /dev/null +++ b/src/app/api/stockIssue/client.ts @@ -0,0 +1,66 @@ +"use client"; + +import { NEXT_PUBLIC_API_URL } from "@/config/api"; +import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; +import type { ExpiryItemFilter } from "@/app/api/stockIssue/actions"; + +/** Result tab currently shown; backend may filter the workbook by this bucket. */ +export type ExpiryExportBucket = "expired" | "today" | "upcoming"; + +export interface ExportExpiryItemExcelParams extends ExpiryItemFilter { + bucket?: ExpiryExportBucket; +} + +/** + * Partner backend contract (not implemented here): + * GET /pickExecution/issues/expiryItem/excel + * Query: itemCode, itemName, lotNo, bucket (expired|today|upcoming) + * Response: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + * Optional Content-Disposition filename. + */ +export async function exportExpiryItemExcel( + filters: ExportExpiryItemExcelParams, +): Promise { + const params = new URLSearchParams(); + if (filters.itemCode) params.set("itemCode", filters.itemCode); + if (filters.itemName) params.set("itemName", filters.itemName); + if (filters.lotNo) params.set("lotNo", filters.lotNo); + if (filters.bucket) params.set("bucket", filters.bucket); + + const queryString = params.toString(); + const url = `${NEXT_PUBLIC_API_URL}/pickExecution/issues/expiryItem/excel${queryString ? `?${queryString}` : ""}`; + + const response = await clientAuthFetch(url, { + method: "GET", + headers: { + Accept: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }, + }); + + if (response.status === 401 || response.status === 403) { + throw new Error("Unauthorized"); + } + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const blob = await response.blob(); + const downloadUrl = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = downloadUrl; + + const contentDisposition = response.headers.get("Content-Disposition"); + let fileName = "expiry-items.xlsx"; + if (contentDisposition?.includes("filename=")) { + fileName = contentDisposition + .split("filename=")[1] + .split(";")[0] + .replace(/"/g, ""); + } + + link.setAttribute("download", fileName); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(downloadUrl); +} diff --git a/src/components/CreateItem/CreateItem.tsx b/src/components/CreateItem/CreateItem.tsx index 583c1104..ddcc6729 100644 --- a/src/components/CreateItem/CreateItem.tsx +++ b/src/components/CreateItem/CreateItem.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useTranslation } from "react-i18next"; import { CreateItemInputs, saveItem } from "@/app/api/settings/item/actions"; @@ -42,6 +42,7 @@ type Props = { warehouses: WarehouseResult[]; }; +/** FP-MTMS Version Checklist | Functions Ref. No. 71 | v1.0.0 | 2026-09-07 */ const CreateItem: React.FC = ({ isEditMode, // type, @@ -56,6 +57,8 @@ const CreateItem: React.FC = ({ console.log(params.get("id")); const [serverError, setServerError] = useState(""); const [tabIndex, setTabIndex] = useState(0); + const [isSaving, setIsSaving] = useState(false); + const inFlightRef = useRef(false); const { t } = useTranslation("items"); const router = useRouter(); const title = "Product / Material"; @@ -104,10 +107,11 @@ const CreateItem: React.FC = ({ }; const onSubmit = useCallback>( async (data, event) => { + if (inFlightRef.current) return; + inFlightRef.current = true; + setIsSaving(true); const hasErrors = false; console.log(errors); - // console.log(apiRef.current.getCellValue(2, "lowerLimit")) - // apiRef.current. try { if (hasErrors) { setServerError(t("An error has occurred. Please try again later.")); @@ -191,6 +195,9 @@ const CreateItem: React.FC = ({ // backend error setServerError(t("An error has occurred. Please try again later.")); console.log(e); + } finally { + setIsSaving(false); + inFlightRef.current = false; } }, [apiRef, router, t], @@ -244,6 +251,7 @@ const CreateItem: React.FC = ({ qcCategoryCombo={qcCategoryCombo} warehouses={warehouses} defaultValues={defaultValues} + isSaving={isSaving} /> )} {tabIndex === 1 && } diff --git a/src/components/CreateItem/CreateItemWrapper.tsx b/src/components/CreateItem/CreateItemWrapper.tsx index 2a4b4683..2e5cc769 100644 --- a/src/components/CreateItem/CreateItemWrapper.tsx +++ b/src/components/CreateItem/CreateItemWrapper.tsx @@ -16,6 +16,7 @@ type Props = { // type: TypeEnum; }; +/** FP-MTMS Version Checklist | Functions Ref. No. 71 | v1.0.0 | 2026-09-07 */ const CreateItemWrapper: React.FC & SubComponents = async ({ id }) => { let result; let defaultValues: Partial | undefined; @@ -29,7 +30,7 @@ const CreateItemWrapper: React.FC & SubComponents = async ({ id }) => { // Normalize LocationCode field (handle case sensitivity from MySQL) const locationCode = item?.LocationCode || item?.locationCode; - + /* console.log("Fetched item data for edit:", { id: item?.id, code: item?.code, @@ -37,7 +38,7 @@ const CreateItemWrapper: React.FC & SubComponents = async ({ id }) => { LocationCode: locationCode, rawItem: item }); - + */ defaultValues = { type: item?.type, id: item?.id, @@ -60,6 +61,9 @@ const CreateItemWrapper: React.FC & SubComponents = async ({ id }) => { isEgg: item?.isEgg, isFee: item?.isFee, isBag: item?.isBag, + averageUnitPrice: item?.averageUnitPrice != null ? String(item.averageUnitPrice) : undefined, + averageUnitPriceEditable: result.averageUnitPriceEditable ?? true, + stockUnitLabel: result.stockUnitLabel, }; } diff --git a/src/components/CreateItem/ProductDetails.tsx b/src/components/CreateItem/ProductDetails.tsx index 2be33c13..6d0f7fc3 100644 --- a/src/components/CreateItem/ProductDetails.tsx +++ b/src/components/CreateItem/ProductDetails.tsx @@ -42,9 +42,11 @@ type Props = { qcChecks?: ItemQc[]; qcCategoryCombo: QcCategoryCombo[]; warehouses: WarehouseResult[]; + isSaving?: boolean; }; -const ProductDetails: React.FC = ({ isEditMode, qcCategoryCombo, warehouses, defaultValues: initialDefaultValues }) => { +/** FP-MTMS Version Checklist | Functions Ref. No. 71 | v1.0.0 | 2026-09-07 */ +const ProductDetails: React.FC = ({ isEditMode, qcCategoryCombo, warehouses, defaultValues: initialDefaultValues, isSaving }) => { const [qcItems, setQcItems] = useState([]); const [qcItemsLoading, setQcItemsLoading] = useState(false); @@ -315,6 +317,19 @@ const ProductDetails: React.FC = ({ isEditMode, qcCategoryCombo, warehous )} /> + + + {t("Special Type")} @@ -359,7 +374,7 @@ const ProductDetails: React.FC = ({ isEditMode, qcCategoryCombo, warehous variant="contained" startIcon={} type="submit" - // disabled={submitDisabled} + disabled={isSaving} > {isEditMode ? t("Save") : t("Confirm")} diff --git a/src/components/InventorySearch/InventorySearch.tsx b/src/components/InventorySearch/InventorySearch.tsx index 778e6d66..6586ea10 100644 --- a/src/components/InventorySearch/InventorySearch.tsx +++ b/src/components/InventorySearch/InventorySearch.tsx @@ -2,7 +2,7 @@ import { InventoryLotLineResult, InventoryResult } from '@/app/api/inventory'; import { useTranslation } from 'react-i18next'; import SearchBox, { Criterion } from '../SearchBox'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { uniq, uniqBy } from 'lodash'; import InventoryTable from './InventoryTable'; import { defaultPagingController } from '../SearchResults/SearchResults'; @@ -16,7 +16,7 @@ import { fetchInventoryLotLines, } from '@/app/api/inventory/actions'; import { PrinterCombo } from '@/app/api/settings/printer'; -import { ItemCombo, fetchItemsWithDetails, ItemWithDetails } from '@/app/api/settings/item/actions'; +import { ItemCombo, fetchItemsByPage } from '@/app/api/settings/item/actions'; import { Button, Dialog, @@ -56,42 +56,68 @@ type SearchQuery = Partial< >; type SearchParamNames = keyof SearchQuery; -/** FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.0 | 2026-07-17 */ +type ItemLookupRow = { + id: number; + code: string; + name: string; + type?: string; + uom?: string; + uomDesc?: string; + purchaseUnit?: string; +}; + +type OpeningItemRow = ItemCombo & { code: string; name: string; type?: string }; + +const extractItemRecords = (res: unknown): ItemLookupRow[] => { + if (!res) return []; + if (Array.isArray(res)) return res as ItemLookupRow[]; + if (typeof res === 'object' && Array.isArray((res as { records?: unknown }).records)) { + return (res as { records: ItemLookupRow[] }).records; + } + return []; +}; + +/** FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.1 | 2026-09-07 */ const InventorySearch: React.FC = ({ inventories, printerCombo }) => { const { t } = useTranslation(['inventory', 'common', 'item']); + const searchInFlightRef = useRef(false); + const openingSearchInFlightRef = useRef(false); - const buildSyntheticInventory = useCallback( - (item: ItemWithDetails): InventoryResult => ({ + const buildSyntheticInventory = useCallback((item: ItemLookupRow): InventoryResult => { + const uom = item.uomDesc || item.uom || item.purchaseUnit || ''; + return { id: 0, - itemId: item.id, + itemId: Number(item.id), itemCode: item.code, itemName: item.name, - itemType: 'Material', + itemType: item.type || 'mat', onHandQty: 0, onHoldQty: 0, unavailableQty: 0, availableQty: 0, - uomCode: item.uom, - uomUdfudesc: item.uomDesc, - uomShortDesc: item.uom, + uomCode: item.uom || uom, + uomUdfudesc: uom, + uomShortDesc: item.uom || uom, qtyPerSmallestUnit: 1, - baseUom: item.uom, + baseUom: uom, price: 0, currencyName: '', status: 'active', latestMarketUnitPrice: undefined, latestMupUpdatedDate: undefined, - }), - [], - ); - - const getFirstItemRecord = useCallback((res: any): ItemWithDetails | null => { - if (!res) return null; - if (Array.isArray(res)) return (res[0] as ItemWithDetails) ?? null; - if (Array.isArray(res?.records)) return (res.records[0] as ItemWithDetails) ?? null; - return null; + }; }, []); + const lookupItemsByCodeOrName = useCallback(async (code?: string, name?: string) => { + const trimmedCode = code?.trim(); + const trimmedName = name?.trim(); + if (!trimmedCode && !trimmedName) return []; + const params: Record = { pageSize: 50, pageNum: 1 }; + if (trimmedCode) params.code = trimmedCode; + else params.name = trimmedName as string; + const itemRes = await fetchItemsByPage(params); + return extractItemRecords(itemRes); + }, []); // Inventory const [filteredInventories, setFilteredInventories] = useState([]); @@ -104,6 +130,20 @@ const InventorySearch: React.FC = ({ inventories, printerCombo }) => { const [inventoryLotLinesPagingController, setInventoryLotLinesPagingController] = useState(defaultPagingController) const [inventoryLotLinesTotalCount, setInventoryLotLinesTotalCount] = useState(0) + const applyItemsAsSyntheticInventories = useCallback( + (items: ItemLookupRow[]) => { + if (!items.length) return false; + const synthetics = items.map(buildSyntheticInventory); + setFilteredInventories(synthetics); + setInventoriesTotalCount(synthetics.length); + setSelectedInventory(synthetics[0]); + setFilteredInventoryLotLines([]); + setInventoryLotLinesPagingController(() => defaultPagingController); + return true; + }, + [buildSyntheticInventory], + ); + // Scan-mode UI (hardware QR scanner via QrCodeScannerProvider) const qrScanner = useQrCodeScannerContext(); const [scanUiMode, setScanUiMode] = useState<'idle' | 'scanning'>('idle'); @@ -114,9 +154,9 @@ const InventorySearch: React.FC = ({ inventories, printerCombo }) => { const [scannedItemId, setScannedItemId] = useState(null); // Opening inventory (pure opening stock for items without existing inventory) - const [openingItems, setOpeningItems] = useState([]); + const [openingItems, setOpeningItems] = useState([]); const [openingModalOpen, setOpeningModalOpen] = useState(false); - const [openingSelectedItem, setOpeningSelectedItem] = useState(null); + const [openingSelectedItem, setOpeningSelectedItem] = useState(null); const [openingLoading, setOpeningLoading] = useState(false); const [openingSearchText, setOpeningSearchText] = useState(''); @@ -297,46 +337,54 @@ const InventorySearch: React.FC = ({ inventories, printerCombo }) => { // On Search const onSearch = useCallback( async (query: Record) => { - setLotNoFilter(''); - setScannedItemId(null); - setScanUiMode('idle'); - setScanHoverCancel(false); - qrScanner.stopScan(); - qrScanner.resetScan(); - const invRes = await refetchInventoryData(query, 'search', defaultPagingController, ''); - await refetchInventoryLotLineData(null, 'search', defaultPagingController); - - setInputs(() => query); - setInventoriesPagingController(() => defaultPagingController); - setInventoryLotLinesPagingController(() => defaultPagingController); + if (searchInFlightRef.current) return; + searchInFlightRef.current = true; + try { + setLotNoFilter(''); + setScannedItemId(null); + setScanUiMode('idle'); + setScanHoverCancel(false); + qrScanner.stopScan(); + qrScanner.resetScan(); + const invRes = await refetchInventoryData(query, 'search', defaultPagingController, ''); + await refetchInventoryLotLineData(null, 'search', defaultPagingController); + + setInputs(() => query); + setInventoriesPagingController(() => defaultPagingController); + setInventoryLotLinesPagingController(() => defaultPagingController); - // If there are no inventory rows, render a synthetic inventory so the "Stock Adjustment" chip can be used. - if (invRes?.records?.length === 0) { - try { - const code = query.itemCode?.trim?.(); - const name = query.itemName?.trim?.(); - const lookupParams = code ? { code } : name ? { name } : null; - - if (lookupParams) { - const itemRes = await fetchItemsWithDetails(lookupParams); - const firstItem = getFirstItemRecord(itemRes); - if (firstItem) { - setSelectedInventory(buildSyntheticInventory(firstItem)); - setFilteredInventoryLotLines([]); - setInventoryLotLinesPagingController(() => defaultPagingController); + // No inventory rows: look up item master so 0-qty rows can be selected for stock adjustment. + if (invRes?.records?.length === 0) { + try { + const items = await lookupItemsByCodeOrName(query.itemCode, query.itemName); + const typeFilter = query.itemType?.trim(); + let filtered = + typeFilter && typeFilter.toLowerCase() !== 'all' + ? items.filter((it) => (it.type ?? '').toLowerCase() === typeFilter.toLowerCase()) + : items; + const exactCode = query.itemCode?.trim().toLowerCase(); + if (exactCode) { + filtered = [...filtered].sort((a, b) => { + const aExact = a.code?.toLowerCase() === exactCode ? 0 : 1; + const bExact = b.code?.toLowerCase() === exactCode ? 0 : 1; + return aExact - bExact; + }); } + applyItemsAsSyntheticInventories(filtered); + } catch (e) { + console.error('Failed to build synthetic inventory:', e); } - } catch (e) { - console.error('Failed to build synthetic inventory:', e); } + } finally { + searchInFlightRef.current = false; } }, [ qrScanner, refetchInventoryData, refetchInventoryLotLineData, - buildSyntheticInventory, - getFirstItemRecord, + lookupItemsByCodeOrName, + applyItemsAsSyntheticInventories, ], ); @@ -382,14 +430,8 @@ const InventorySearch: React.FC = ({ inventories, printerCombo }) => { onInventoryRowClick(target); } else { refetchInventoryLotLineData(null, 'search', defaultPagingController); - // No inventory rows for this scanned item => show synthetic inventory with the existing chip workflow. - const itemRes = await fetchItemsWithDetails({ code: res?.itemCode }); - const firstItem = getFirstItemRecord(itemRes); - if (firstItem) { - setSelectedInventory(buildSyntheticInventory(firstItem)); - setFilteredInventoryLotLines([]); - setInventoryLotLinesPagingController(() => defaultPagingController); - } else { + const items = await lookupItemsByCodeOrName(res?.itemCode); + if (!applyItemsAsSyntheticInventories(items)) { setSelectedInventory(null); } } @@ -410,8 +452,8 @@ const InventorySearch: React.FC = ({ inventories, printerCombo }) => { qrScanner.result, refetchInventoryData, refetchInventoryLotLineData, - buildSyntheticInventory, - getFirstItemRecord, + lookupItemsByCodeOrName, + applyItemsAsSyntheticInventories, scanUiMode, ]); @@ -430,51 +472,35 @@ const InventorySearch: React.FC = ({ inventories, printerCombo }) => { setOpeningItems([]); return; } - + if (openingSearchInFlightRef.current) return; + openingSearchInFlightRef.current = true; setOpeningLoading(true); try { - const searchParams: Record = { - pageSize: 50, - pageNum: 1, - }; - - // Heuristic: if input contains space, treat as name; otherwise treat as code. - if (trimmed.includes(' ')) { - searchParams.name = trimmed; - } else { - searchParams.code = trimmed; - } - - const response = await fetchItemsWithDetails(searchParams); - - let records: any[] = []; - if (response && typeof response === 'object') { - const anyRes = response as any; - if (Array.isArray(anyRes.records)) { - records = anyRes.records; - } else if (Array.isArray(anyRes)) { - records = anyRes; - } - } - - const combos: ItemCombo[] = records.map((item: any) => ({ - id: item.id, - label: `${item.code} - ${item.name}`, - uomId: item.uomId, - uom: item.uom, - uomDesc: item.uomDesc, - group: item.group, - currentStockBalance: item.currentStockBalance, - })); - + const items = trimmed.includes(' ') + ? await lookupItemsByCodeOrName(undefined, trimmed) + : await lookupItemsByCodeOrName(trimmed); + const combos: OpeningItemRow[] = items.map((item) => { + const uom = item.uomDesc || item.uom || item.purchaseUnit || ''; + return { + id: item.id, + label: `${item.code} - ${item.name}`, + uomId: 0, + uom, + uomDesc: uom, + code: item.code, + name: item.name, + type: item.type, + }; + }); setOpeningItems(combos); } catch (e) { console.error('Failed to search items for opening inventory:', e); setOpeningItems([]); } finally { setOpeningLoading(false); + openingSearchInFlightRef.current = false; } - }, [openingSearchText]); + }, [openingSearchText, lookupItemsByCodeOrName]); const handleConfirmOpeningInventory = useCallback(() => { if (!openingSelectedItem) { @@ -482,39 +508,18 @@ const InventorySearch: React.FC = ({ inventories, printerCombo }) => { return; } - const rawLabel = openingSelectedItem.label ?? ''; - const [codePart, ...nameParts] = rawLabel.split(' - '); - const itemCode = codePart?.trim() || rawLabel; - const itemName = nameParts.join(' - ').trim() || itemCode; - - const syntheticInventory: InventoryResult = { - id: 0, - itemId: Number(openingSelectedItem.id), - itemCode, - itemName, - itemType: 'Material', - onHandQty: 0, - onHoldQty: 0, - unavailableQty: 0, - availableQty: 0, - uomCode: openingSelectedItem.uom, - uomUdfudesc: openingSelectedItem.uomDesc, - uomShortDesc: openingSelectedItem.uom, - qtyPerSmallestUnit: 1, - baseUom: openingSelectedItem.uom, - price: 0, - currencyName: '', - status: 'active', - latestMarketUnitPrice: undefined, - latestMupUpdatedDate: undefined, - }; - - // Use this synthetic inventory to drive the stock adjustment UI - setSelectedInventory(syntheticInventory); - setFilteredInventoryLotLines([]); - setInventoryLotLinesPagingController(() => defaultPagingController); + applyItemsAsSyntheticInventories([ + { + id: Number(openingSelectedItem.id), + code: openingSelectedItem.code, + name: openingSelectedItem.name, + type: openingSelectedItem.type, + uom: openingSelectedItem.uom, + uomDesc: openingSelectedItem.uomDesc, + }, + ]); setOpeningModalOpen(false); - }, [openingSelectedItem]); + }, [openingSelectedItem, applyItemsAsSyntheticInventories]); return ( <> @@ -545,7 +550,6 @@ const InventorySearch: React.FC = ({ inventories, printerCombo }) => { variant="outlined" color="secondary" onClick={handleOpenOpeningInventoryModal} - sx={{ display: 'none' }} > {t('Add entry for items without inventory')} @@ -652,8 +656,6 @@ const InventorySearch: React.FC = ({ inventories, printerCombo }) => { {openingItems.map((it) => { - const [code, ...nameParts] = (it.label ?? '').split(' - '); - const name = nameParts.join(' - '); const selected = openingSelectedItem?.id === it.id; return ( = ({ inventories, printerCombo }) => { - {code} - {name} + {it.code} + {it.name} {it.uomDesc || it.uom} {it.currentStockBalance != null ? it.currentStockBalance : '-'} diff --git a/src/components/PoDetail/PoDetail.tsx b/src/components/PoDetail/PoDetail.tsx index 1b1fb889..36bffeb5 100644 --- a/src/components/PoDetail/PoDetail.tsx +++ b/src/components/PoDetail/PoDetail.tsx @@ -253,7 +253,7 @@ interface PolInputResult { dnQty: string, } -/** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.1 | 2026-08-12 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.2 | 2026-09-07 */ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { const cameras = useContext(CameraContext); const { data: session } = useSession(); @@ -678,11 +678,29 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { }, 200); }; - const exceedOrderBy10Percent = orderQty > 0 && acceptedQty > orderQty * 1.1; - if (exceedOrderBy10Percent) { + const sils = row.stockInLine ?? []; + const alreadyM18 = sils.reduce( + (acc, sil) => acc + Number(sil.purchaseAcceptedQty ?? 0), + 0, + ); + const alreadyStock = sils.reduce( + (acc, sil) => acc + Number(sil.acceptedQty ?? 0), + 0, + ); + const stockDemand = Number(row.stockUom?.stockQty ?? 0); + const thisBatchStock = + orderQty > 0 && stockDemand > 0 + ? acceptedQty * (stockDemand / orderQty) + : acceptedQty; + const exceedByOrderUnit = + orderQty > 0 && alreadyM18 + acceptedQty > orderQty * 1.1; + const exceedByStockUnit = + stockDemand > 0 && + alreadyStock + thisBatchStock > stockDemand * 1.1; + if (exceedByOrderUnit || exceedByStockUnit) { submitDialogWithWarning(doSubmit, t, { title: t("Confirm submit"), - html: t("This batch quantity exceeds order quantity. Do you still want to submit?"), + html: t("qtyExceedsOrderConfirm"), confirmButtonText: t("Submit"), }); } else { @@ -834,6 +852,11 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => {