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 <noreply@anthropic.com>
This commit is contained in:
parent
90ad1925e7
commit
cf9af94f7d
4 changed files with 127 additions and 159 deletions
|
|
@ -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({
|
|||
<TableHead>Description</TableHead>
|
||||
<TableHead>Plant</TableHead>
|
||||
<TableHead>Warehouse</TableHead>
|
||||
<TableHead className="text-right">On Hand</TableHead>
|
||||
<TableHead className="text-right">Allocated</TableHead>
|
||||
<TableHead className="text-right">Available</TableHead>
|
||||
<TableHead>UM</TableHead>
|
||||
<TableHead className="text-right">On Hand Qty</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
|
@ -116,7 +107,7 @@ export function InventorySummaryTable({
|
|||
<TableRow key={idx}>
|
||||
<TableCell>
|
||||
<Link
|
||||
href={`/inventory/${category}/detail?part=${row.part_num}&plant=${row.plant}&warehouse=${row.warehouse}`}
|
||||
href={`/inventory/${category}/detail?part=${encodeURIComponent(row.part_num)}&plant=${encodeURIComponent(row.plant_key || row.plant)}&warehouse=${encodeURIComponent(row.warehouse)}`}
|
||||
className="font-medium hover:underline"
|
||||
>
|
||||
{row.part_num}
|
||||
|
|
@ -128,15 +119,8 @@ export function InventorySummaryTable({
|
|||
<TableCell>{row.plant}</TableCell>
|
||||
<TableCell>{row.warehouse}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{row.on_hand_qty.toLocaleString()}
|
||||
{Number(row.on_hand_qty).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{(row.allocated_qty || 0).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{(row.available_qty || 0).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>{row.um}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search order #, PO, customer part, or Vorteq part..."
|
||||
placeholder="Search order #, PO, part, or job..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-8"
|
||||
|
|
@ -109,22 +106,21 @@ export function OrdersTable({ data }: Props) {
|
|||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Order #</TableHead>
|
||||
<TableHead>PO #</TableHead>
|
||||
<TableHead>Order Date</TableHead>
|
||||
<TableHead>Need By</TableHead>
|
||||
<TableHead>Customer Part</TableHead>
|
||||
<TableHead>Customer PO</TableHead>
|
||||
<TableHead>Vorteq Part</TableHead>
|
||||
<TableHead className="text-right">Qty</TableHead>
|
||||
<TableHead className="text-right">Shipped</TableHead>
|
||||
<TableHead className="text-right">Remaining</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead>Plant</TableHead>
|
||||
<TableHead>Warehouse</TableHead>
|
||||
<TableHead className="text-right">Qty Completed</TableHead>
|
||||
<TableHead>Completion Date</TableHead>
|
||||
<TableHead>Job #</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredData.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={10}
|
||||
colSpan={9}
|
||||
className="text-center text-muted-foreground"
|
||||
>
|
||||
No orders found
|
||||
|
|
@ -133,49 +129,27 @@ export function OrdersTable({ data }: Props) {
|
|||
) : (
|
||||
filteredData.map((row, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell>
|
||||
<Link
|
||||
href={`/orders/${row.order_num}`}
|
||||
className="font-medium hover:underline"
|
||||
>
|
||||
<TableCell className="font-medium">
|
||||
{row.order_num}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>{row.po_num || '-'}</TableCell>
|
||||
<TableCell>
|
||||
{row.order_date
|
||||
? new Date(row.order_date).toLocaleDateString()
|
||||
: '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{row.need_by_date
|
||||
? new Date(row.need_by_date).toLocaleDateString()
|
||||
: '-'}
|
||||
</TableCell>
|
||||
<TableCell>{row.customer_part || '-'}</TableCell>
|
||||
<TableCell>{row.customer_po || '-'}</TableCell>
|
||||
<TableCell className="font-mono">
|
||||
{row.vorteq_part || '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{row.order_qty?.toFixed(0) || '0'}
|
||||
<TableCell className="max-w-xs truncate">
|
||||
{row.part_description || '-'}
|
||||
</TableCell>
|
||||
<TableCell>{row.plant || '-'}</TableCell>
|
||||
<TableCell>{row.warehouse || '-'}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{row.shipped_qty?.toFixed(0) || '0'}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{row.remaining_qty?.toFixed(0) || '0'}
|
||||
{Number(row.qty_completed || 0).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
className={
|
||||
row.open_order
|
||||
? 'rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800'
|
||||
: 'rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-800'
|
||||
}
|
||||
>
|
||||
{row.status}
|
||||
</span>
|
||||
{row.completion_date
|
||||
? new Date(row.completion_date).toLocaleDateString()
|
||||
: '-'}
|
||||
</TableCell>
|
||||
<TableCell>{row.job_num || '-'}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>): 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<InventorySummaryRow[]> {
|
||||
const result = await execStoredProc<InventorySummaryRow[]>(
|
||||
const result = await execStoredProc<Record<string, unknown>[]>(
|
||||
'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<InventorySummaryRow[]> {
|
||||
const result = await execStoredProc<InventorySummaryRow[]>(
|
||||
const result = await execStoredProc<Record<string, unknown>[]>(
|
||||
'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<InventorySummaryRow[]> {
|
||||
const result = await execStoredProc<InventorySummaryRow[]>(
|
||||
const result = await execStoredProc<Record<string, unknown>[]>(
|
||||
'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<InventorySummaryRow[]> {
|
||||
if (isSubUser) {
|
||||
return []; // Sub-users cannot access unprocessed inventory
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await execStoredProc<InventorySummaryRow[]>(
|
||||
const result = await execStoredProc<Record<string, unknown>[]>(
|
||||
'PortalUnprocessedInventorySummary',
|
||||
{
|
||||
Customer: custId,
|
||||
CUSTID: custId,
|
||||
DBNAME: dbName,
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
return result.map(mapSummaryRow);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -139,18 +157,18 @@ export async function getUnprocessedRRSummary(
|
|||
isSubUser: boolean
|
||||
): Promise<InventorySummaryRow[]> {
|
||||
if (isSubUser) {
|
||||
return []; // Sub-users cannot access R&R inventory
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await execStoredProc<InventorySummaryRow[]>(
|
||||
const result = await execStoredProc<Record<string, unknown>[]>(
|
||||
'PortalUnprocessedInventoryRejectsAndReturnsSummary',
|
||||
{
|
||||
Customer: custId,
|
||||
CUSTID: custId,
|
||||
DBNAME: dbName,
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
return result.map(mapSummaryRow);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -162,18 +180,18 @@ export async function getProcessedRRSummary(
|
|||
isSubUser: boolean
|
||||
): Promise<InventorySummaryRow[]> {
|
||||
if (isSubUser) {
|
||||
return []; // Sub-users cannot access R&R inventory
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await execStoredProc<InventorySummaryRow[]>(
|
||||
const result = await execStoredProc<Record<string, unknown>[]>(
|
||||
'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)
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>): 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<OrderRow[]> {
|
||||
// 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<OrderRow[]>(sql, {
|
||||
Cust1: custId,
|
||||
Cust2: cust2,
|
||||
const result = await execQuery<Record<string, unknown>[]>(query, {
|
||||
CustID: queryCustId,
|
||||
});
|
||||
|
||||
return result;
|
||||
return result.map(mapOrderRow);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue