From cf9af94f7d8d48365e3e159c39e74125ec94b4c7 Mon Sep 17 00:00:00 2001 From: Lorentz Date: Mon, 16 Feb 2026 15:39:14 +0000 Subject: [PATCH] fix: map Epicor SP column names and use portal_Orders view Inventory service now maps PascalCase SP columns (VorteqPartNum, OnHandQty, etc.) to snake_case types. Fixed non-V6 SP parameter from Customer to CUSTID. Orders service now uses the portal_Orders view instead of raw table queries with nonexistent columns. Co-Authored-By: Claude Opus 4.6 --- .../inventory/inventory-summary-table.tsx | 24 +--- src/components/orders/orders-table.tsx | 106 +++++++----------- src/services/inventory.ts | 72 +++++++----- src/services/orders.ts | 84 ++++++-------- 4 files changed, 127 insertions(+), 159 deletions(-) diff --git a/src/components/inventory/inventory-summary-table.tsx b/src/components/inventory/inventory-summary-table.tsx index 49b8353..73d970a 100644 --- a/src/components/inventory/inventory-summary-table.tsx +++ b/src/components/inventory/inventory-summary-table.tsx @@ -40,10 +40,7 @@ export function InventorySummaryTable({ 'Description', 'Plant', 'Warehouse', - 'On Hand', - 'Allocated', - 'Available', - 'UM', + 'On Hand Qty', ]; const rows = filteredData.map((row) => [ row.part_num, @@ -51,9 +48,6 @@ export function InventorySummaryTable({ row.plant, row.warehouse, row.on_hand_qty, - row.allocated_qty || 0, - row.available_qty || 0, - row.um, ]); const csvContent = [headers, ...rows] @@ -105,10 +99,7 @@ export function InventorySummaryTable({ Description Plant Warehouse - On Hand - Allocated - Available - UM + On Hand Qty @@ -116,7 +107,7 @@ export function InventorySummaryTable({ {row.part_num} @@ -128,15 +119,8 @@ export function InventorySummaryTable({ {row.plant} {row.warehouse} - {row.on_hand_qty.toLocaleString()} + {Number(row.on_hand_qty).toLocaleString()} - - {(row.allocated_qty || 0).toLocaleString()} - - - {(row.available_qty || 0).toLocaleString()} - - {row.um} ))} diff --git a/src/components/orders/orders-table.tsx b/src/components/orders/orders-table.tsx index cff9ec6..ba63fb7 100644 --- a/src/components/orders/orders-table.tsx +++ b/src/components/orders/orders-table.tsx @@ -1,7 +1,6 @@ 'use client'; import { useState } from 'react'; -import Link from 'next/link'; import type { OrderRow } from '@/services/orders'; import { Card, @@ -33,39 +32,37 @@ export function OrdersTable({ data }: Props) { const searchLower = searchTerm.toLowerCase(); return ( row.order_num?.toString().includes(searchLower) || - row.po_num?.toLowerCase().includes(searchLower) || - row.customer_part?.toLowerCase().includes(searchLower) || - row.vorteq_part?.toLowerCase().includes(searchLower) + row.customer_po?.toLowerCase().includes(searchLower) || + row.vorteq_part?.toLowerCase().includes(searchLower) || + row.job_num?.toLowerCase().includes(searchLower) ); }); const handleExportCSV = () => { const headers = [ 'Order #', - 'PO #', - 'Order Date', - 'Need By', - 'Customer Part', + 'Customer PO', 'Vorteq Part', - 'Order Qty', - 'Shipped', - 'Remaining', - 'UM', - 'Status', + 'Description', + 'Plant', + 'Warehouse', + 'Qty Completed', + 'Completion Date', + 'Job #', ]; const rows = filteredData.map((row) => [ row.order_num?.toString() || '', - row.po_num || '', - row.order_date ? new Date(row.order_date).toLocaleDateString() : '', - row.need_by_date ? new Date(row.need_by_date).toLocaleDateString() : '', - row.customer_part || '', + row.customer_po || '', row.vorteq_part || '', - row.order_qty?.toString() || '0', - row.shipped_qty?.toString() || '0', - row.remaining_qty?.toString() || '0', - row.um || '', - row.status || '', + row.part_description || '', + row.plant || '', + row.warehouse || '', + row.qty_completed?.toString() || '0', + row.completion_date + ? new Date(row.completion_date).toLocaleDateString() + : '', + row.job_num || '', ]); const csvContent = [headers, ...rows] @@ -92,7 +89,7 @@ export function OrdersTable({ data }: Props) {
setSearchTerm(e.target.value)} className="pl-8" @@ -109,22 +106,21 @@ export function OrdersTable({ data }: Props) { Order # - PO # - Order Date - Need By - Customer Part + Customer PO Vorteq Part - Qty - Shipped - Remaining - Status + Description + Plant + Warehouse + Qty Completed + Completion Date + Job # {filteredData.length === 0 ? ( No orders found @@ -133,49 +129,27 @@ export function OrdersTable({ data }: Props) { ) : ( filteredData.map((row, i) => ( - - - {row.order_num} - + + {row.order_num} - {row.po_num || '-'} - - {row.order_date - ? new Date(row.order_date).toLocaleDateString() - : '-'} - - - {row.need_by_date - ? new Date(row.need_by_date).toLocaleDateString() - : '-'} - - {row.customer_part || '-'} + {row.customer_po || '-'} {row.vorteq_part || '-'} - - {row.order_qty?.toFixed(0) || '0'} + + {row.part_description || '-'} + {row.plant || '-'} + {row.warehouse || '-'} - {row.shipped_qty?.toFixed(0) || '0'} - - - {row.remaining_qty?.toFixed(0) || '0'} + {Number(row.qty_completed || 0).toLocaleString()} - - {row.status} - + {row.completion_date + ? new Date(row.completion_date).toLocaleDateString() + : '-'} + {row.job_num || '-'} )) )} diff --git a/src/services/inventory.ts b/src/services/inventory.ts index 1504183..5dc18e2 100644 --- a/src/services/inventory.ts +++ b/src/services/inventory.ts @@ -1,7 +1,8 @@ /** * Inventory Service * - * Handles inventory data retrieval from Epicor stored procedures + * Handles inventory data retrieval from Epicor stored procedures. + * SP columns are PascalCase; we map them to snake_case for the UI. */ import { execStoredProc } from '@/lib/epicor'; @@ -10,11 +11,11 @@ export type InventorySummaryRow = { part_num: string; description: string; plant: string; + plant_key: string; warehouse: string; on_hand_qty: number; - allocated_qty: number; - available_qty: number; - um: string; // Unit of measure + paint_code?: string; + cust_part_num?: string; }; export type InventoryDetailRow = { @@ -35,9 +36,25 @@ export type InventoryDetailRow = { uom?: string; receipt_date?: Date; paint_code?: string; - [key: string]: unknown; // Allow additional fields from stored procedures + [key: string]: unknown; }; +/** + * Map raw Epicor summary SP row to our normalized type + */ +function mapSummaryRow(raw: Record): InventorySummaryRow { + return { + part_num: String(raw.VorteqPartNum ?? ''), + description: String(raw.VorteqPartDesc ?? ''), + plant: String(raw.Plant ?? ''), + plant_key: String(raw.PlantKey ?? ''), + warehouse: String(raw.Warehouse ?? ''), + on_hand_qty: Number(raw.OnHandQty ?? 0), + paint_code: raw.PaintCode ? String(raw.PaintCode) : undefined, + cust_part_num: raw.CustPartNum ? String(raw.CustPartNum) : undefined, + }; +} + export type InventoryCategory = | 'wip' | 'finished-goods' @@ -54,7 +71,7 @@ export async function getWorkInProgressSummary( dbName: string, sub: number ): Promise { - const result = await execStoredProc( + const result = await execStoredProc[]>( 'PortalWorkInProgressInventorySummaryV6', { Customer: custId, @@ -63,7 +80,7 @@ export async function getWorkInProgressSummary( } ); - return result; + return result.map(mapSummaryRow); } /** @@ -74,7 +91,7 @@ export async function getFinishedGoodsSummary( dbName: string, sub: number ): Promise { - const result = await execStoredProc( + const result = await execStoredProc[]>( 'PortalFinishedGoodsInventorySummaryV6', { Customer: custId, @@ -83,7 +100,7 @@ export async function getFinishedGoodsSummary( } ); - return result; + return result.map(mapSummaryRow); } /** @@ -94,7 +111,7 @@ export async function getProcessedOtherSummary( dbName: string, sub: number ): Promise { - const result = await execStoredProc( + const result = await execStoredProc[]>( 'PortalProcessedOtherInventorySummaryV6', { Customer: custId, @@ -103,12 +120,13 @@ export async function getProcessedOtherSummary( } ); - return result; + return result.map(mapSummaryRow); } /** * Get Unprocessed inventory summary - * Note: Sub-users are blocked from accessing this category + * Note: Sub-users are blocked from accessing this category. + * Non-V6 SPs use @CUSTID parameter. */ export async function getUnprocessedSummary( custId: string, @@ -116,18 +134,18 @@ export async function getUnprocessedSummary( isSubUser: boolean ): Promise { if (isSubUser) { - return []; // Sub-users cannot access unprocessed inventory + return []; } - const result = await execStoredProc( + const result = await execStoredProc[]>( 'PortalUnprocessedInventorySummary', { - Customer: custId, + CUSTID: custId, DBNAME: dbName, } ); - return result; + return result.map(mapSummaryRow); } /** @@ -139,18 +157,18 @@ export async function getUnprocessedRRSummary( isSubUser: boolean ): Promise { if (isSubUser) { - return []; // Sub-users cannot access R&R inventory + return []; } - const result = await execStoredProc( + const result = await execStoredProc[]>( 'PortalUnprocessedInventoryRejectsAndReturnsSummary', { - Customer: custId, + CUSTID: custId, DBNAME: dbName, } ); - return result; + return result.map(mapSummaryRow); } /** @@ -162,18 +180,18 @@ export async function getProcessedRRSummary( isSubUser: boolean ): Promise { if (isSubUser) { - return []; // Sub-users cannot access R&R inventory + return []; } - const result = await execStoredProc( + const result = await execStoredProc[]>( 'PortalProcessedInventoryRejectsAndReturnsSummary', { - Customer: custId, + CUSTID: custId, DBNAME: dbName, } ); - return result; + return result.map(mapSummaryRow); } /** @@ -236,10 +254,12 @@ export async function getInventoryDetails( DBNAME: dbName, }; - // V6 procedures use Customer, non-V6 also use Customer - params.Customer = custId; + // V6 procedures use @Customer, non-V6 use @CUSTID if (procConfig.isV6) { + params.Customer = custId; params.SUBUSER = sub; + } else { + params.CUSTID = custId; } // Add filters if provided (only for filtered variant) diff --git a/src/services/orders.ts b/src/services/orders.ts index 7d32878..2b34807 100644 --- a/src/services/orders.ts +++ b/src/services/orders.ts @@ -1,70 +1,60 @@ /** * Orders Service - * Handles order data retrieval from Epicor + * Handles order data retrieval from Epicor using the portal_Orders view */ import { execQuery } from '@/lib/epicor'; export type OrderRow = { order_num: number; - po_num: string; - order_date: Date; - need_by_date: Date; - customer_part: string; + customer_po: string; vorteq_part: string; - order_qty: number; - shipped_qty: number; - remaining_qty: number; - um: string; - open_order: boolean; - status: string; - ship_to_name?: string; + part_description: string; + plant: string; + warehouse: string; + qty_completed: number; + completion_date: Date; + job_num: string; [key: string]: unknown; }; /** - * Get top 100 orders for a customer - * Special handling for HDC customer + * Map raw portal_Orders view row to our normalized type + */ +function mapOrderRow(raw: Record): OrderRow { + return { + order_num: Number(raw.VorteqSalesOrderNum ?? 0), + customer_po: String(raw.CustomerPO ?? ''), + vorteq_part: String(raw.VorteqPartNum ?? ''), + part_description: String(raw.PartDescription ?? ''), + plant: String(raw.Plant ?? ''), + warehouse: String(raw.Warehouse ?? ''), + qty_completed: Number(raw.QuantityCompleted ?? 0), + completion_date: raw.CompletionDate as Date, + job_num: String(raw.VorteqJobNum ?? ''), + }; +} + +/** + * Get top 100 orders for a customer using the portal_Orders view. + * HDC exception: uses portal_OrdersHDC view and maps to CustID 'HDM'. */ export async function getTop100Orders(custId: string): Promise { - // HDC exception: use different customer ID for second parameter - const cust2 = custId === 'HDC' ? 'HDM' : custId; + const viewName = custId === 'HDC' ? 'portal_OrdersHDC' : 'portal_Orders'; + const queryCustId = custId === 'HDC' ? 'HDM' : custId; - // Query Epicor OrderHed and OrderDtl tables - // This is a simplified version - the actual portal_Orders.sql may have more complex logic - const sql = ` - SELECT TOP 100 - oh.OrderNum AS order_num, - oh.PONum AS po_num, - oh.OrderDate AS order_date, - oh.NeedByDate AS need_by_date, - od.XPartNum AS customer_part, - od.PartNum AS vorteq_part, - od.OrderQty AS order_qty, - od.ShippedQty AS shipped_qty, - (od.OrderQty - od.ShippedQty) AS remaining_qty, - od.IUM AS um, - oh.OpenOrder AS open_order, - CASE - WHEN oh.OpenOrder = 1 THEN 'Open' - ELSE 'Closed' - END AS status, - st.Name AS ship_to_name - FROM Erp.OrderHed oh - INNER JOIN Erp.OrderDtl od ON oh.Company = od.Company AND oh.OrderNum = od.OrderNum - INNER JOIN Erp.Customer c ON oh.Company = c.Company AND oh.CustNum = c.CustNum - LEFT JOIN Erp.ShipTo st ON oh.Company = st.Company AND oh.ShipToNum = st.ShipToNum - WHERE c.CustID = @Cust1 - OR c.CustID = @Cust2 - ORDER BY oh.OrderDate DESC, oh.OrderNum DESC + const query = ` + SELECT TOP 100 * + FROM dbo.${viewName} + WHERE CustomerID = @CustID + ORDER BY CompletionDate DESC `; - const result = await execQuery(sql, { - Cust1: custId, - Cust2: cust2, + const result = await execQuery[]>(query, { + CustID: queryCustId, }); - return result; + return result.map(mapOrderRow); } /**