diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/app/(portal)/orders/page.tsx b/src/app/(portal)/orders/page.tsx new file mode 100644 index 0000000..e191cc8 --- /dev/null +++ b/src/app/(portal)/orders/page.tsx @@ -0,0 +1,58 @@ +import { Suspense } from 'react'; +import { redirect } from 'next/navigation'; +import { getTop100Orders } from '@/services/orders'; +import { getQuestSession, getActiveCompany } from '@/lib/permissions'; +import { OrdersTable } from '@/components/orders/orders-table'; +import { Card, CardContent } from '@/components/ui/card'; + +export const dynamic = 'force-dynamic'; + +async function OrdersData() { + const session = await getQuestSession(); + const activeCompany = await getActiveCompany(); + + if (!session || !activeCompany) { + redirect('/select-company'); + } + + const orders = await getTop100Orders(activeCompany.epicor_cust_id).catch( + (err) => { + console.error('Failed to fetch orders:', err); + return []; + } + ); + + return ; +} + +function LoadingSkeleton() { + return ( + + +
+ {[...Array(10)].map((_, i) => ( +
+ ))} +
+ + + ); +} + +export default function OrdersPage() { + return ( +
+

Orders

+

+ View your most recent orders and order acknowledgements +

+ + }> + + +
+ ); +} diff --git a/src/components/orders/orders-table.tsx b/src/components/orders/orders-table.tsx new file mode 100644 index 0000000..cff9ec6 --- /dev/null +++ b/src/components/orders/orders-table.tsx @@ -0,0 +1,192 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import type { OrderRow } from '@/services/orders'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Download, Search } from 'lucide-react'; + +type Props = { + data: OrderRow[]; +}; + +export function OrdersTable({ data }: Props) { + const [searchTerm, setSearchTerm] = useState(''); + + const filteredData = data.filter((row) => { + 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) + ); + }); + + const handleExportCSV = () => { + const headers = [ + 'Order #', + 'PO #', + 'Order Date', + 'Need By', + 'Customer Part', + 'Vorteq Part', + 'Order Qty', + 'Shipped', + 'Remaining', + 'UM', + 'Status', + ]; + + 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.vorteq_part || '', + row.order_qty?.toString() || '0', + row.shipped_qty?.toString() || '0', + row.remaining_qty?.toString() || '0', + row.um || '', + row.status || '', + ]); + + const csvContent = [headers, ...rows] + .map((row) => row.map((cell) => `"${cell}"`).join(',')) + .join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `orders-${new Date().toISOString().split('T')[0]}.csv`; + a.click(); + window.URL.revokeObjectURL(url); + }; + + return ( + + + Orders + Top 100 most recent orders + + +
+
+ + setSearchTerm(e.target.value)} + className="pl-8" + /> +
+ +
+ +
+ + + + Order # + PO # + Order Date + Need By + Customer Part + Vorteq Part + Qty + Shipped + Remaining + Status + + + + {filteredData.length === 0 ? ( + + + No orders found + + + ) : ( + filteredData.map((row, i) => ( + + + + {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.vorteq_part || '-'} + + + {row.order_qty?.toFixed(0) || '0'} + + + {row.shipped_qty?.toFixed(0) || '0'} + + + {row.remaining_qty?.toFixed(0) || '0'} + + + + {row.status} + + + + )) + )} + +
+
+ +
+ Showing {filteredData.length} of {data.length} orders +
+
+
+ ); +} diff --git a/src/services/orders.ts b/src/services/orders.ts new file mode 100644 index 0000000..7d32878 --- /dev/null +++ b/src/services/orders.ts @@ -0,0 +1,152 @@ +/** + * Orders Service + * Handles order data retrieval from Epicor + */ + +import { execQuery } from '@/lib/epicor'; + +export type OrderRow = { + order_num: number; + po_num: string; + order_date: Date; + need_by_date: Date; + customer_part: string; + vorteq_part: string; + order_qty: number; + shipped_qty: number; + remaining_qty: number; + um: string; + open_order: boolean; + status: string; + ship_to_name?: string; + [key: string]: unknown; +}; + +/** + * Get top 100 orders for a customer + * Special handling for HDC customer + */ +export async function getTop100Orders(custId: string): Promise { + // HDC exception: use different customer ID for second parameter + const cust2 = 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 result = await execQuery(sql, { + Cust1: custId, + Cust2: cust2, + }); + + return result; +} + +/** + * Get orders for a specific customer on or after a date + * Used for allocation requests + */ +export async function getOrdersForCustomerOnOrAfterDate( + custId: string, + date: string, + excludedOrderNumbers: number[] = [] +): Promise { + let sql = ` + SELECT + oh.OrderNum AS order_num, + oh.PONum AS po_num, + oh.OrderDate AS order_date, + od.PartNum AS vorteq_part, + od.OrderQty AS order_qty, + od.ShippedQty AS shipped_qty, + (od.OrderQty - od.ShippedQty) AS remaining_qty + 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 + WHERE c.CustID = @CustomerID + AND oh.OrderDate >= @Date + AND oh.OpenOrder = 1 + `; + + if (excludedOrderNumbers.length > 0) { + const excludedList = excludedOrderNumbers.join(','); + sql += ` AND oh.OrderNum NOT IN (${excludedList})`; + } + + sql += ' ORDER BY oh.OrderDate DESC'; + + const result = await execQuery(sql, { + CustomerID: custId, + Date: date, + }); + + return result; +} + +/** + * Get order details for acknowledgement + */ +export async function getOrderDetails(orderNum: number): Promise { + const sql = ` + SELECT + oh.OrderNum AS order_num, + oh.PONum AS po_num, + oh.OrderDate AS order_date, + oh.NeedByDate AS need_by_date, + od.OrderLine AS order_line, + od.XPartNum AS customer_part, + od.PartNum AS vorteq_part, + od.LineDesc AS line_desc, + od.OrderQty AS order_qty, + od.ShippedQty AS shipped_qty, + (od.OrderQty - od.ShippedQty) AS remaining_qty, + od.IUM AS um, + od.UnitPrice AS unit_price, + (od.OrderQty * od.UnitPrice) AS extended_price, + c.Name AS customer_name, + c.CustID AS cust_id, + st.Name AS ship_to_name, + st.Address1 AS ship_to_address1, + st.Address2 AS ship_to_address2, + st.City AS ship_to_city, + st.State AS ship_to_state, + st.ZIP AS ship_to_zip + 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.CustNum = st.CustNum AND oh.ShipToNum = st.ShipToNum + WHERE oh.OrderNum = @OrderNum + ORDER BY od.OrderLine + `; + + const result = await execQuery(sql, { + OrderNum: orderNum, + }); + + return result; +}