| @@ -58,9 +58,10 @@ const fetchItemPage = async ( | |||||
| }; | }; | ||||
| /** | /** | ||||
| * FP-MTMS Version Checklist | Functions Ref. No. 81 | v1.0.0 | 2026-09-10 | |||||
| * FP-MTMS Version Checklist | Functions Ref. No. 81 | v1.0.1 | 2026-09-11 | |||||
| * Typeahead lookup for report item-code multi-select. | * Typeahead lookup for report item-code multi-select. | ||||
| * Uses the existing paged item API so we never load the full catalog. | * Uses the existing paged item API so we never load the full catalog. | ||||
| * Hits are unique by item code — multi-UoM inventory buckets are expanded only in the downloaded report. | |||||
| */ | */ | ||||
| export const searchItemCodes = async ( | export const searchItemCodes = async ( | ||||
| query: string, | query: string, | ||||
| @@ -419,10 +419,10 @@ export default function ReportPage() { | |||||
| const response = await clientAuthFetch(excelUrl, { | const response = await clientAuthFetch(excelUrl, { | ||||
| method: 'GET', | method: 'GET', | ||||
| headers: { Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, | |||||
| skipAuthRedirect: true, | |||||
| headers: { Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/json' }, | |||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (response.status === 204) { | if (response.status === 204) { | ||||
| setShowNoDataDialog(true); | setShowNoDataDialog(true); | ||||
| return; | return; | ||||
| @@ -482,10 +482,10 @@ export default function ReportPage() { | |||||
| const response = await clientAuthFetch(url, { | const response = await clientAuthFetch(url, { | ||||
| method: 'GET', | method: 'GET', | ||||
| headers: { 'Accept': 'application/pdf' }, | |||||
| skipAuthRedirect: true, | |||||
| headers: { Accept: 'application/pdf, application/json' }, | |||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) return; | |||||
| if (!response.ok) { | if (!response.ok) { | ||||
| const errorText = await response.text(); | const errorText = await response.text(); | ||||
| console.error("Response error:", errorText); | console.error("Response error:", errorText); | ||||
| @@ -179,8 +179,8 @@ async function fetchInventoriesLatestImpl(data: SearchInventory) { | |||||
| export const fetchInventories = cache(fetchInventoriesImpl); | export const fetchInventories = cache(fetchInventoriesImpl); | ||||
| /** | /** | ||||
| * FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.4 | 2026-09-10 | |||||
| * Inventory search page: latest inventory row per item + stock UoM, with optional location filters. | |||||
| * FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.5 | 2026-09-11 | |||||
| * Inventory search page: one row per item + stock UoM, with optional location filters. | |||||
| */ | */ | ||||
| export const fetchInventoriesLatest = cache(fetchInventoriesLatestImpl); | export const fetchInventoriesLatest = cache(fetchInventoriesLatestImpl); | ||||
| @@ -5,7 +5,7 @@ import { cache } from "react"; | |||||
| import "server-only"; | import "server-only"; | ||||
| export interface InventoryResult { | export interface InventoryResult { | ||||
| id: number; | |||||
| id: number | string; | |||||
| itemId: number; | itemId: number; | ||||
| itemCode: string; | itemCode: string; | ||||
| itemName: string; | itemName: string; | ||||
| @@ -9,7 +9,7 @@ import { LOGIN_SESSION_EXPIRED_HREF } from "@/app/utils/authToken"; | |||||
| */ | */ | ||||
| export async function clientAuthFetch( | export async function clientAuthFetch( | ||||
| input: RequestInfo | URL, | input: RequestInfo | URL, | ||||
| init?: RequestInit | |||||
| init?: RequestInit & { skipAuthRedirect?: boolean } | |||||
| ): Promise<Response> { | ): Promise<Response> { | ||||
| const token = | const token = | ||||
| typeof window !== "undefined" ? localStorage.getItem("accessToken") : null; | typeof window !== "undefined" ? localStorage.getItem("accessToken") : null; | ||||
| @@ -18,9 +18,13 @@ export async function clientAuthFetch( | |||||
| headers.set("Authorization", `Bearer ${token}`); | headers.set("Authorization", `Bearer ${token}`); | ||||
| } | } | ||||
| const response = await fetch(input, { ...init, headers }); | |||||
| const { skipAuthRedirect, ...fetchInit } = init ?? {}; | |||||
| const response = await fetch(input, { ...fetchInit, headers }); | |||||
| if (response.status === 401 || response.status === 403) { | |||||
| if ( | |||||
| !skipAuthRedirect && | |||||
| (response.status === 401 || response.status === 403) | |||||
| ) { | |||||
| if (typeof window !== "undefined") { | if (typeof window !== "undefined") { | ||||
| console.warn(`Auth error ${response.status} → redirecting to login`); | console.warn(`Auth error ${response.status} → redirecting to login`); | ||||
| window.location.href = LOGIN_SESSION_EXPIRED_HREF; | window.location.href = LOGIN_SESSION_EXPIRED_HREF; | ||||
| @@ -584,7 +584,7 @@ const prevAdjustmentModalOpenRef = useRef(false); | |||||
| return <> | return <> | ||||
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap', mb: 2 }}> | <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap', mb: 2 }}> | ||||
| <Typography variant="h6"> | <Typography variant="h6"> | ||||
| {inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")} | |||||
| {inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)}) | ${t("UoM ID")}: ${inventory.uomId ?? "-"} | ${inventory.uomUdfudesc || inventory.uomShortDesc || inventory.uomCode || "-"}` : t("No items are selected yet.")} | |||||
| </Typography> | </Typography> | ||||
| {inventory && canStockAdjust && ( | {inventory && canStockAdjust && ( | ||||
| <Chip | <Chip | ||||
| @@ -40,8 +40,11 @@ type SearchQuery = Partial< | |||||
| InventoryResult, | InventoryResult, | ||||
| | "id" | | "id" | ||||
| | "qty" | | "qty" | ||||
| | "uomId" | |||||
| | "uomCode" | | "uomCode" | ||||
| | "uomUdfudesc" | | "uomUdfudesc" | ||||
| | "stockUomId" | |||||
| | "stockUomCode" | |||||
| | "germPerSmallestUnit" | | "germPerSmallestUnit" | ||||
| | "qtyPerSmallestUnit" | | "qtyPerSmallestUnit" | ||||
| | "itemSmallestUnit" | | "itemSmallestUnit" | ||||
| @@ -71,7 +74,7 @@ const extractItemRecords = (res: unknown): ItemLookupRow[] => { | |||||
| return []; | return []; | ||||
| }; | }; | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.4 | 2026-09-10 */ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.5 | 2026-09-11 */ | |||||
| const InventorySearch: React.FC<Props> = ({ | const InventorySearch: React.FC<Props> = ({ | ||||
| inventories, | inventories, | ||||
| printerCombo, | printerCombo, | ||||
| @@ -84,6 +87,11 @@ const InventorySearch: React.FC<Props> = ({ | |||||
| const canStockAdjust = hasAbility(abilities, AUTH.INVENTORY_ADJUST); | const canStockAdjust = hasAbility(abilities, AUTH.INVENTORY_ADJUST); | ||||
| const searchInFlightRef = useRef(false); | const searchInFlightRef = useRef(false); | ||||
| const withUniqueInventoryRowId = useCallback((row: InventoryResult): InventoryResult => ({ | |||||
| ...row, | |||||
| id: `${row.itemId}-${row.uomId ?? 0}-${row.id}`, | |||||
| }), []); | |||||
| const buildSyntheticInventory = useCallback((item: ItemLookupRow): InventoryResult => { | const buildSyntheticInventory = useCallback((item: ItemLookupRow): InventoryResult => { | ||||
| const uom = item.uomDesc || item.uom || item.purchaseUnit || ''; | const uom = item.uomDesc || item.uom || item.purchaseUnit || ''; | ||||
| return { | return { | ||||
| @@ -134,7 +142,7 @@ const InventorySearch: React.FC<Props> = ({ | |||||
| const applyItemsAsSyntheticInventories = useCallback( | const applyItemsAsSyntheticInventories = useCallback( | ||||
| (items: ItemLookupRow[]) => { | (items: ItemLookupRow[]) => { | ||||
| if (!items.length) return false; | if (!items.length) return false; | ||||
| const synthetics = items.map(buildSyntheticInventory); | |||||
| const synthetics = items.map(buildSyntheticInventory).map(withUniqueInventoryRowId); | |||||
| setFilteredInventories(synthetics); | setFilteredInventories(synthetics); | ||||
| setInventoriesTotalCount(synthetics.length); | setInventoriesTotalCount(synthetics.length); | ||||
| setSelectedInventory(synthetics[0]); | setSelectedInventory(synthetics[0]); | ||||
| @@ -142,7 +150,7 @@ const InventorySearch: React.FC<Props> = ({ | |||||
| setInventoryLotLinesPagingController(() => defaultPagingController); | setInventoryLotLinesPagingController(() => defaultPagingController); | ||||
| return true; | return true; | ||||
| }, | }, | ||||
| [buildSyntheticInventory], | |||||
| [buildSyntheticInventory, withUniqueInventoryRowId], | |||||
| ); | ); | ||||
| // Scan-mode UI (hardware QR scanner via QrCodeScannerProvider) | // Scan-mode UI (hardware QR scanner via QrCodeScannerProvider) | ||||
| @@ -253,23 +261,25 @@ const InventorySearch: React.FC<Props> = ({ | |||||
| const response = await fetchInventoriesLatest(params); | const response = await fetchInventoriesLatest(params); | ||||
| if (response) { | if (response) { | ||||
| const records = (response.records ?? []).map(withUniqueInventoryRowId); | |||||
| setInventoriesTotalCount(response.total); | setInventoriesTotalCount(response.total); | ||||
| switch (actionType) { | switch (actionType) { | ||||
| case 'init': | case 'init': | ||||
| case 'reset': | case 'reset': | ||||
| case 'search': | case 'search': | ||||
| setFilteredInventories(() => response.records); | |||||
| setFilteredInventories(() => records); | |||||
| break; | break; | ||||
| case 'paging': | case 'paging': | ||||
| setFilteredInventories((fi) => | setFilteredInventories((fi) => | ||||
| uniqBy([...fi, ...response.records], (row) => `${row.itemId}-${row.uomId ?? row.uomUdfudesc ?? ''}`), | |||||
| uniqBy([...fi, ...records], (row) => `${row.itemId}-${row.uomId ?? row.uomUdfudesc ?? ''}`), | |||||
| ); | ); | ||||
| } | } | ||||
| return { ...response, records }; | |||||
| } | } | ||||
| return response; | return response; | ||||
| }, | }, | ||||
| [enableLocationFilter, location, withLocationParams], | |||||
| [enableLocationFilter, location, withLocationParams, withUniqueInventoryRowId], | |||||
| ); | ); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -39,6 +39,13 @@ const InventoryTable: React.FC<Props> = ({ inventories, pagingController, setPag | |||||
| headerAlign: "right", | headerAlign: "right", | ||||
| type: "integer", | type: "integer", | ||||
| }, | }, | ||||
| { | |||||
| name: "uomId", | |||||
| label: t("UoM ID"), | |||||
| align: "right", | |||||
| headerAlign: "right", | |||||
| renderCell: (params) => params.uomId ?? "-", | |||||
| }, | |||||
| { | { | ||||
| name: "uomUdfudesc", | name: "uomUdfudesc", | ||||
| label: t("Stock UoM"), | label: t("Stock UoM"), | ||||
| @@ -332,7 +332,7 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| ] | ] | ||||
| }, | }, | ||||
| */ | */ | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 82 | v1.0.0 | 2026-09-10 */ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 82 | v1.0.1 | 2026-09-11 */ | |||||
| { | { | ||||
| id: "rep-007", | id: "rep-007", | ||||
| title: "庫存結餘報告", | title: "庫存結餘報告", | ||||
| @@ -78,6 +78,7 @@ | |||||
| "Target Location": "Target Location", | "Target Location": "Target Location", | ||||
| "Type": "Type", | "Type": "Type", | ||||
| "UoM": "UoM", | "UoM": "UoM", | ||||
| "UoM ID": "UoM ID", | |||||
| "WIP": "Work in progress", | "WIP": "Work in progress", | ||||
| "Warehouse": "Warehouse", | "Warehouse": "Warehouse", | ||||
| "cmb": "Consumables", | "cmb": "Consumables", | ||||
| @@ -78,6 +78,7 @@ | |||||
| "Target Location": "目標倉位", | "Target Location": "目標倉位", | ||||
| "Type": "類型", | "Type": "類型", | ||||
| "UoM": "單位", | "UoM": "單位", | ||||
| "UoM ID": "單位ID", | |||||
| "WIP": "半成品", | "WIP": "半成品", | ||||
| "Warehouse": "倉庫", | "Warehouse": "倉庫", | ||||
| "cmb": "消耗品", | "cmb": "消耗品", | ||||
| @@ -58,6 +58,7 @@ export function logFeatureUsage( | |||||
| const res = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/feature-usage/log`, { | const res = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/feature-usage/log`, { | ||||
| method: "POST", | method: "POST", | ||||
| headers: { "Content-Type": "application/json" }, | headers: { "Content-Type": "application/json" }, | ||||
| skipAuthRedirect: true, | |||||
| body: JSON.stringify({ | body: JSON.stringify({ | ||||
| featureCode, | featureCode, | ||||
| actionType, | actionType, | ||||