feat: add inventory detail modal with correct legacy columns
Replace full-page navigation with an inline modal dialog when clicking View on inventory summary rows. Fix V6 detail stored procedure params (@CUSTID/@sub instead of @Customer/@SUBUSER) and update detail table columns to match legacy portal: Cust Part#, Skid#, Lot#, Mfg Lot#, Bin#, Qty LB, Lin. Ft, Theo. Wt, WIP/FG, # Coils, PO#, Sales Order/Job, Alloc#/Release, Date Processed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
56ce4ba940
commit
e47c35e9a9
4 changed files with 407 additions and 124 deletions
82
src/app/api/inventory/details/route.ts
Normal file
82
src/app/api/inventory/details/route.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import {
|
||||
getQuestSession,
|
||||
getActiveCompany,
|
||||
isSubUser,
|
||||
} from '@/lib/permissions';
|
||||
import {
|
||||
getInventoryDetails,
|
||||
type InventoryCategory,
|
||||
} from '@/services/inventory';
|
||||
|
||||
const VALID_CATEGORIES: InventoryCategory[] = [
|
||||
'wip',
|
||||
'finished-goods',
|
||||
'processed-other',
|
||||
'unprocessed',
|
||||
'unprocessed-rr',
|
||||
'processed-rr',
|
||||
];
|
||||
|
||||
const BLOCKED_FOR_SUBUSER: InventoryCategory[] = [
|
||||
'unprocessed',
|
||||
'unprocessed-rr',
|
||||
'processed-rr',
|
||||
];
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getQuestSession();
|
||||
if (!session) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized', code: 'UNAUTHORIZED' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const activeCompany = await getActiveCompany();
|
||||
if (!activeCompany) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No active company selected', code: 'NO_COMPANY' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { searchParams } = request.nextUrl;
|
||||
const category = searchParams.get('category') as InventoryCategory;
|
||||
const part = searchParams.get('part') || undefined;
|
||||
const plant = searchParams.get('plant') || undefined;
|
||||
const warehouse = searchParams.get('warehouse') || undefined;
|
||||
|
||||
if (!category || !VALID_CATEGORIES.includes(category)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid category', code: 'INVALID_INPUT' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const userIsSubUser = await isSubUser();
|
||||
if (userIsSubUser && BLOCKED_FOR_SUBUSER.includes(category)) {
|
||||
return NextResponse.json({ data: [] });
|
||||
}
|
||||
|
||||
const dbName = `[${process.env.PORTAL_DB_NAME || 'VorteqPortal'}]`;
|
||||
const sub = userIsSubUser ? 1 : 0;
|
||||
|
||||
const details = await getInventoryDetails(
|
||||
category,
|
||||
activeCompany.epicor_cust_id,
|
||||
dbName,
|
||||
sub,
|
||||
{ partNum: part, plant, warehouse }
|
||||
);
|
||||
|
||||
return NextResponse.json({ data: details });
|
||||
} catch (error) {
|
||||
console.error('Error fetching inventory details:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error', code: 'INTERNAL_ERROR' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,50 +26,84 @@ type Props = {
|
|||
partNum?: string;
|
||||
plant?: string;
|
||||
warehouse?: string;
|
||||
embedded?: boolean;
|
||||
};
|
||||
|
||||
export function InventoryDetailTable({
|
||||
function formatNum(val: number | string | null | undefined): string {
|
||||
if (val == null || val === '') return '';
|
||||
const n = Number(val);
|
||||
if (isNaN(n)) return '';
|
||||
return n.toLocaleString('en-US');
|
||||
}
|
||||
|
||||
function formatDate(val: string | null | undefined): string {
|
||||
if (!val) return '';
|
||||
const d = new Date(val);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
function DetailTableContent({
|
||||
data,
|
||||
partNum,
|
||||
plant,
|
||||
warehouse,
|
||||
}: Props) {
|
||||
}: {
|
||||
data: InventoryDetailRow[];
|
||||
partNum?: string;
|
||||
}) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const filteredData = data.filter((row) => {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
if (!searchTerm) return true;
|
||||
const s = searchTerm.toLowerCase();
|
||||
return (
|
||||
row.part_num?.toLowerCase().includes(searchLower) ||
|
||||
row.plant_name?.toLowerCase().includes(searchLower) ||
|
||||
row.warehouse_desc?.toLowerCase().includes(searchLower) ||
|
||||
row.bin_num?.toLowerCase().includes(searchLower) ||
|
||||
row.lot_num?.toLowerCase().includes(searchLower)
|
||||
row.CustPartNum?.toLowerCase().includes(s) ||
|
||||
row.SkidNum?.toLowerCase().includes(s) ||
|
||||
row.LotNum?.toLowerCase().includes(s) ||
|
||||
row.MfgLot?.toLowerCase().includes(s) ||
|
||||
row.Bin?.toLowerCase().includes(s) ||
|
||||
row.CustomerPoNum?.toLowerCase().includes(s) ||
|
||||
row.VorteqSalesOrderNum?.toLowerCase().includes(s) ||
|
||||
row.VorteqJobNum?.toLowerCase().includes(s)
|
||||
);
|
||||
});
|
||||
|
||||
const handleExportCSV = () => {
|
||||
const headers = [
|
||||
'Part Number',
|
||||
'Plant',
|
||||
'Warehouse',
|
||||
'Bin',
|
||||
'Lot',
|
||||
'Serial',
|
||||
'On Hand Qty',
|
||||
'UOM',
|
||||
'Description',
|
||||
'Cust Part#',
|
||||
'Skid#',
|
||||
'Lot#',
|
||||
'Mfg Lot#',
|
||||
'Bin#',
|
||||
'Qty LB',
|
||||
'Lin. Ft',
|
||||
'Theo. Wt',
|
||||
'WIP/FG',
|
||||
'# Coils Per Skid',
|
||||
'PO #',
|
||||
'Sales Order',
|
||||
'Job Order',
|
||||
'Alloc # / Release Id',
|
||||
'Date Processed',
|
||||
];
|
||||
|
||||
const rows = filteredData.map((row) => [
|
||||
row.part_num || '',
|
||||
row.plant_name || '',
|
||||
row.warehouse_desc || '',
|
||||
row.bin_num || '',
|
||||
row.lot_num || '',
|
||||
row.serial_num || '',
|
||||
row.on_hand_qty?.toString() || '0',
|
||||
row.uom || '',
|
||||
row.part_description || '',
|
||||
row.CustPartNum ?? '',
|
||||
row.SkidNum ?? '',
|
||||
row.LotNum ?? '',
|
||||
row.MfgLot ?? '',
|
||||
row.Bin ?? '',
|
||||
row.OnHandQty ?? '',
|
||||
row.LinearFt ?? '',
|
||||
row.TheoreticalWeight ?? '',
|
||||
row.WIP_FG ?? '',
|
||||
row.NumCoilsPerSkid ?? '',
|
||||
row.CustomerPoNum ?? '',
|
||||
row.VorteqSalesOrderNum ?? '',
|
||||
row.VorteqJobNum ?? '',
|
||||
[row.AllocationNumber, row.ShipmentReleaseId]
|
||||
.filter(Boolean)
|
||||
.join(' / '),
|
||||
row.DateProcessedToInventory ?? '',
|
||||
]);
|
||||
|
||||
const csvContent = [headers, ...rows]
|
||||
|
|
@ -80,11 +114,120 @@ export function InventoryDetailTable({
|
|||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `inventory-detail-${new Date().toISOString().split('T')[0]}.csv`;
|
||||
const partSuffix = partNum ? `-${partNum}` : '';
|
||||
a.download = `inventory-detail${partSuffix}-${new Date().toISOString().split('T')[0]}.csv`;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex items-center gap-4">
|
||||
<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 by part, skid, lot, PO, or sales order..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleExportCSV} variant="outline" size="sm">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Export CSV
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Cust Part#</TableHead>
|
||||
<TableHead>Skid#</TableHead>
|
||||
<TableHead>Lot#</TableHead>
|
||||
<TableHead>Mfg Lot#</TableHead>
|
||||
<TableHead>Bin#</TableHead>
|
||||
<TableHead className="text-right">Qty LB</TableHead>
|
||||
<TableHead className="text-right">Lin. Ft</TableHead>
|
||||
<TableHead className="text-right">Theo. Wt</TableHead>
|
||||
<TableHead>WIP/FG</TableHead>
|
||||
<TableHead className="text-right"># Coils</TableHead>
|
||||
<TableHead>PO #</TableHead>
|
||||
<TableHead>Sales Order / Job</TableHead>
|
||||
<TableHead>Alloc # / Release</TableHead>
|
||||
<TableHead>Date Processed</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredData.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={14}
|
||||
className="text-center text-muted-foreground"
|
||||
>
|
||||
No inventory found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredData.map((row, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell>{row.CustPartNum ?? ''}</TableCell>
|
||||
<TableCell>{row.SkidNum ?? ''}</TableCell>
|
||||
<TableCell>{row.LotNum ?? ''}</TableCell>
|
||||
<TableCell>{row.MfgLot ?? ''}</TableCell>
|
||||
<TableCell>{row.Bin ?? ''}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatNum(row.OnHandQty)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatNum(row.LinearFt)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatNum(row.TheoreticalWeight)}
|
||||
</TableCell>
|
||||
<TableCell>{row.WIP_FG ?? ''}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatNum(row.NumCoilsPerSkid)}
|
||||
</TableCell>
|
||||
<TableCell>{row.CustomerPoNum ?? ''}</TableCell>
|
||||
<TableCell>
|
||||
{[row.VorteqSalesOrderNum, row.VorteqJobNum]
|
||||
.filter(Boolean)
|
||||
.join(' / ')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{[row.AllocationNumber, row.ShipmentReleaseId]
|
||||
.filter(Boolean)
|
||||
.join(' / ')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate(row.DateProcessedToInventory)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
Showing {filteredData.length} of {data.length} items
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function InventoryDetailTable({
|
||||
data,
|
||||
partNum,
|
||||
plant,
|
||||
warehouse,
|
||||
embedded = false,
|
||||
}: Props) {
|
||||
if (embedded) {
|
||||
return <DetailTableContent data={data} partNum={partNum} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
|
@ -96,70 +239,7 @@ export function InventoryDetailTable({
|
|||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-4 flex items-center gap-4">
|
||||
<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 part, plant, warehouse, bin, or lot..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleExportCSV} variant="outline">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Export CSV
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Part Number</TableHead>
|
||||
<TableHead>Plant</TableHead>
|
||||
<TableHead>Warehouse</TableHead>
|
||||
<TableHead>Bin</TableHead>
|
||||
<TableHead>Lot</TableHead>
|
||||
<TableHead>Serial</TableHead>
|
||||
<TableHead className="text-right">On Hand</TableHead>
|
||||
<TableHead>UOM</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredData.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-center text-muted-foreground">
|
||||
No inventory found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredData.map((row, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell className="font-medium">{row.part_num}</TableCell>
|
||||
<TableCell>{row.plant_name}</TableCell>
|
||||
<TableCell>{row.warehouse_desc}</TableCell>
|
||||
<TableCell>{row.bin_num || '-'}</TableCell>
|
||||
<TableCell>{row.lot_num || '-'}</TableCell>
|
||||
<TableCell>{row.serial_num || '-'}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{row.on_hand_qty?.toFixed(2) || '0.00'}
|
||||
</TableCell>
|
||||
<TableCell>{row.uom || '-'}</TableCell>
|
||||
<TableCell className="max-w-xs truncate">
|
||||
{row.part_description || '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
Showing {filteredData.length} of {data.length} items
|
||||
</div>
|
||||
<DetailTableContent data={data} partNum={partNum} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useState, useCallback } from 'react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -12,8 +11,17 @@ import {
|
|||
} from '@/components/ui/table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Search, Download, Eye } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Search, Download, Eye, Loader2 } from 'lucide-react';
|
||||
import { InventorySummaryRow } from '@/services/inventory';
|
||||
import type { InventoryDetailRow } from '@/services/inventory';
|
||||
import { InventoryDetailTable } from './inventory-detail-table';
|
||||
|
||||
type InventorySummaryTableProps = {
|
||||
data: InventorySummaryRow[];
|
||||
|
|
@ -25,6 +33,13 @@ export function InventorySummaryTable({
|
|||
category,
|
||||
}: InventorySummaryTableProps) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [selectedRow, setSelectedRow] = useState<InventorySummaryRow | null>(
|
||||
null
|
||||
);
|
||||
const [detailData, setDetailData] = useState<InventoryDetailRow[]>([]);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [detailError, setDetailError] = useState<string | null>(null);
|
||||
|
||||
const filteredData = data.filter(
|
||||
(row) =>
|
||||
|
|
@ -67,6 +82,41 @@ export function InventorySummaryTable({
|
|||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleViewDetail = useCallback(
|
||||
async (row: InventorySummaryRow) => {
|
||||
setSelectedRow(row);
|
||||
setDialogOpen(true);
|
||||
setDetailLoading(true);
|
||||
setDetailError(null);
|
||||
setDetailData([]);
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({ category });
|
||||
if (row.part_num) params.set('part', row.part_num);
|
||||
if (row.plant_key || row.plant)
|
||||
params.set('plant', row.plant_key || row.plant);
|
||||
if (row.warehouse) params.set('warehouse', row.warehouse);
|
||||
|
||||
const response = await fetch(
|
||||
`/api/inventory/details?${params.toString()}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch detail data');
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
setDetailData(json.data || []);
|
||||
} catch (err) {
|
||||
console.error('Error fetching inventory details:', err);
|
||||
setDetailError('Failed to load detail data. Please try again.');
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
},
|
||||
[category]
|
||||
);
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center text-muted-foreground">
|
||||
|
|
@ -123,13 +173,14 @@ export function InventorySummaryTable({
|
|||
</TableCell>
|
||||
<TableCell className="text-right">{row.rows}</TableCell>
|
||||
<TableCell>
|
||||
<Link
|
||||
href={`/inventory/${category}/detail?part=${encodeURIComponent(row.part_num)}&plant=${encodeURIComponent(row.plant_key || row.plant)}&warehouse=${encodeURIComponent(row.warehouse)}`}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-blue-600 hover:text-blue-800"
|
||||
onClick={() => handleViewDetail(row)}
|
||||
>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-blue-600 hover:text-blue-800">
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
|
@ -140,6 +191,75 @@ export function InventorySummaryTable({
|
|||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
Showing {filteredData.length} of {data.length} items
|
||||
</div>
|
||||
|
||||
{/* Detail Modal */}
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="max-h-[90vh] max-w-6xl overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Inventory Detail</DialogTitle>
|
||||
{selectedRow && (
|
||||
<DialogDescription asChild>
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-1 pt-2 text-sm sm:grid-cols-3">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Vorteq Part#:</span>{' '}
|
||||
<span className="font-medium text-foreground">
|
||||
{selectedRow.part_num}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Plant:</span>{' '}
|
||||
<span className="font-medium text-foreground">
|
||||
{selectedRow.plant}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Warehouse:</span>{' '}
|
||||
<span className="font-medium text-foreground">
|
||||
{selectedRow.warehouse}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Qty LB:</span>{' '}
|
||||
<span className="font-medium text-foreground">
|
||||
{Number(selectedRow.on_hand_qty).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="text-muted-foreground">Description:</span>{' '}
|
||||
<span className="font-medium text-foreground">
|
||||
{selectedRow.description || '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
<div className="overflow-y-auto">
|
||||
{detailLoading && (
|
||||
<div className="flex h-48 items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{detailError && (
|
||||
<div className="flex h-48 items-center justify-center text-destructive">
|
||||
{detailError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!detailLoading && !detailError && (
|
||||
<InventoryDetailTable
|
||||
data={detailData}
|
||||
partNum={selectedRow?.part_num}
|
||||
plant={selectedRow?.plant_key || selectedRow?.plant}
|
||||
warehouse={selectedRow?.warehouse}
|
||||
embedded
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,24 +19,27 @@ export type InventorySummaryRow = {
|
|||
cust_part_num?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Raw detail row returned by Epicor stored procedures.
|
||||
* Column names match the SP output (PascalCase).
|
||||
*/
|
||||
export type InventoryDetailRow = {
|
||||
part_num: string;
|
||||
description?: string;
|
||||
part_description?: string;
|
||||
lot_num?: string;
|
||||
serial_num?: string;
|
||||
plant?: string;
|
||||
plant_name?: string;
|
||||
warehouse?: string;
|
||||
warehouse_desc?: string;
|
||||
bin_num?: string;
|
||||
on_hand_qty: number;
|
||||
allocated_qty?: number;
|
||||
available_qty?: number;
|
||||
um?: string;
|
||||
uom?: string;
|
||||
receipt_date?: Date;
|
||||
paint_code?: string;
|
||||
CustPartNum?: string | null;
|
||||
SkidNum?: string | null;
|
||||
LotNum?: string | null;
|
||||
MfgLot?: string | null;
|
||||
Bin?: string | null;
|
||||
OnHandQty?: number | null;
|
||||
LinearFt?: number | null;
|
||||
TheoreticalWeight?: number | null;
|
||||
WIP_FG?: string | null;
|
||||
NumCoilsPerSkid?: number | null;
|
||||
CustomerPoNum?: string | null;
|
||||
VorteqSalesOrderNum?: string | null;
|
||||
VorteqJobNum?: string | null;
|
||||
AllocationNumber?: string | null;
|
||||
ShipmentReleaseId?: string | null;
|
||||
DateProcessedToInventory?: string | null;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
|
|
@ -256,12 +259,10 @@ export async function getInventoryDetails(
|
|||
DBNAME: dbName,
|
||||
};
|
||||
|
||||
// V6 procedures use @Customer, non-V6 use @CUSTID
|
||||
if (procConfig.isV6) {
|
||||
params.Customer = custId;
|
||||
params.SUBUSER = sub;
|
||||
} else {
|
||||
// V6 detail procedures use @CUSTID and @sub (unlike summary SPs which use @Customer/@SUBUSER)
|
||||
params.CUSTID = custId;
|
||||
if (procConfig.isV6) {
|
||||
params.sub = sub;
|
||||
}
|
||||
|
||||
// Add filters if provided (only for filtered variant)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue