diff --git a/src/components/inventory/inventory-detail-table.tsx b/src/components/inventory/inventory-detail-table.tsx index b79ee0a..64029c4 100644 --- a/src/components/inventory/inventory-detail-table.tsx +++ b/src/components/inventory/inventory-detail-table.tsx @@ -15,11 +15,13 @@ import { Table, TableBody, TableCell, - TableHead, - TableHeader, TableRow, } from '@/components/ui/table'; import { Download, Search } from 'lucide-react'; +import { + SortableTableHead, + useSortableTable, +} from '@/components/ui/sortable-table-head'; type Props = { data: InventoryDetailRow[]; @@ -43,6 +45,46 @@ function formatDate(val: string | null | undefined): string { return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`; } +type FlatDetailRow = { + CustPartNum: string; + SkidNum: string; + LotNum: string; + MfgLot: string; + Bin: string; + OnHandQty: number; + LinearFt: number; + TheoreticalWeight: number; + WIP_FG: string; + NumCoilsPerSkid: number; + CustomerPoNum: string; + SalesOrderJob: string; + AllocRelease: string; + DateProcessedToInventory: string; +}; + +function flattenRow(row: InventoryDetailRow): FlatDetailRow { + return { + CustPartNum: row.CustPartNum ?? '', + SkidNum: row.SkidNum ?? '', + LotNum: row.LotNum ?? '', + MfgLot: row.MfgLot ?? '', + Bin: row.Bin ?? '', + OnHandQty: Number(row.OnHandQty ?? 0), + LinearFt: Number(row.LinearFt ?? 0), + TheoreticalWeight: Number(row.TheoreticalWeight ?? 0), + WIP_FG: row.WIP_FG ?? '', + NumCoilsPerSkid: Number(row.NumCoilsPerSkid ?? 0), + CustomerPoNum: row.CustomerPoNum ?? '', + SalesOrderJob: [row.VorteqSalesOrderNum, row.VorteqJobNum] + .filter(Boolean) + .join(' / '), + AllocRelease: [row.AllocationNumber, row.ShipmentReleaseId] + .filter(Boolean) + .join(' / '), + DateProcessedToInventory: row.DateProcessedToInventory ?? '', + }; +} + function DetailTableContent({ data, partNum, @@ -52,21 +94,25 @@ function DetailTableContent({ }) { const [searchTerm, setSearchTerm] = useState(''); - const filteredData = data.filter((row) => { + const flatData = data.map(flattenRow); + + const filteredData = flatData.filter((row) => { if (!searchTerm) return true; const s = searchTerm.toLowerCase(); return ( - 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) + 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.SalesOrderJob.toLowerCase().includes(s) ); }); + const { sortKey, sortDirection, handleSort, sortedData } = + useSortableTable(filteredData); + const handleExportCSV = () => { const headers = [ 'Cust Part#', @@ -80,30 +126,26 @@ function DetailTableContent({ 'WIP/FG', '# Coils Per Skid', 'PO #', - 'Sales Order', - 'Job Order', + 'Sales Order / Job', 'Alloc # / Release Id', 'Date Processed', ]; - const rows = filteredData.map((row) => [ - 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 rows = sortedData.map((row) => [ + row.CustPartNum, + row.SkidNum, + row.LotNum, + row.MfgLot, + row.Bin, + row.OnHandQty, + row.LinearFt, + row.TheoreticalWeight, + row.WIP_FG, + row.NumCoilsPerSkid, + row.CustomerPoNum, + row.SalesOrderJob, + row.AllocRelease, + row.DateProcessedToInventory, ]); const csvContent = [headers, ...rows] @@ -138,28 +180,130 @@ function DetailTableContent({ -
+
- - - Cust Part# - Skid# - Lot# - Mfg Lot# - Bin# - Qty LB - Lin. Ft - Theo. Wt - WIP/FG - # Coils - PO # - Sales Order / Job - Alloc # / Release - Date Processed - - + + + + Cust Part# + + + Skid# + + + Lot# + + + Mfg Lot# + + + Bin# + + + Qty LB + + + Lin. Ft + + + Theo. Wt + + + WIP/FG + + + # Coils + + + PO # + + + Sales Order / Job + + + Alloc # / Release + + + Date Processed + + + - {filteredData.length === 0 ? ( + {sortedData.length === 0 ? ( ) : ( - filteredData.map((row, i) => ( - - {row.CustPartNum ?? ''} - {row.SkidNum ?? ''} - {row.LotNum ?? ''} - {row.MfgLot ?? ''} - {row.Bin ?? ''} + sortedData.map((row, i) => ( + + {row.CustPartNum} + {row.SkidNum} + {row.LotNum} + {row.MfgLot} + {row.Bin} {formatNum(row.OnHandQty)} @@ -185,21 +329,13 @@ function DetailTableContent({ {formatNum(row.TheoreticalWeight)} - {row.WIP_FG ?? ''} + {row.WIP_FG} {formatNum(row.NumCoilsPerSkid)} - {row.CustomerPoNum ?? ''} - - {[row.VorteqSalesOrderNum, row.VorteqJobNum] - .filter(Boolean) - .join(' / ')} - - - {[row.AllocationNumber, row.ShipmentReleaseId] - .filter(Boolean) - .join(' / ')} - + {row.CustomerPoNum} + {row.SalesOrderJob} + {row.AllocRelease} {formatDate(row.DateProcessedToInventory)} @@ -211,7 +347,7 @@ function DetailTableContent({
- Showing {filteredData.length} of {data.length} items + Showing {sortedData.length} of {data.length} items
); diff --git a/src/components/inventory/inventory-summary-table.tsx b/src/components/inventory/inventory-summary-table.tsx index bde4761..61fd90e 100644 --- a/src/components/inventory/inventory-summary-table.tsx +++ b/src/components/inventory/inventory-summary-table.tsx @@ -5,8 +5,6 @@ import { Table, TableBody, TableCell, - TableHead, - TableHeader, TableRow, } from '@/components/ui/table'; import { Button } from '@/components/ui/button'; @@ -22,6 +20,19 @@ 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'; +import { + SortableTableHead, + useSortableTable, +} from '@/components/ui/sortable-table-head'; + +const CATEGORY_COLORS: Record = { + wip: 'bg-emerald-600 text-white', + 'finished-goods': 'bg-blue-600 text-white', + 'processed-other': 'bg-amber-600 text-white', + unprocessed: 'bg-orange-500 text-white', + 'unprocessed-rr': 'bg-red-600 text-white', + 'processed-rr': 'bg-rose-600 text-white', +}; type InventorySummaryTableProps = { data: InventorySummaryRow[]; @@ -49,6 +60,11 @@ export function InventorySummaryTable({ row.cust_part_num?.toLowerCase().includes(searchTerm.toLowerCase()) ); + const { sortKey, sortDirection, handleSort, sortedData } = + useSortableTable(filteredData); + + const headerColor = CATEGORY_COLORS[category] || 'bg-slate-700 text-white'; + const handleExportCSV = () => { const headers = [ 'Plant', @@ -59,7 +75,7 @@ export function InventorySummaryTable({ 'Qty LB', 'Rows', ]; - const rows = filteredData.map((row) => [ + const rows = sortedData.map((row) => [ row.plant, row.cust_part_num || '', row.part_num, @@ -143,23 +159,74 @@ export function InventorySummaryTable({ -
+
- - - Plant - Cust Part# - Vorteq Part# - Vorteq Desc - Warehouse - Qty LB - Rows - - - + + + + Plant + + + Cust Part# + + + Vorteq Part# + + + Vorteq Desc + + + Warehouse + + + Qty LB + + + Rows + + + + - {filteredData.map((row, idx) => ( - + {sortedData.map((row, idx) => ( + {row.plant} {row.cust_part_num || '-'} {row.part_num} @@ -188,12 +255,12 @@ export function InventorySummaryTable({
- Showing {filteredData.length} of {data.length} items + Showing {sortedData.length} of {data.length} items
{/* Detail Modal */} - + Inventory Detail {selectedRow && ( diff --git a/src/components/orders/orders-table.tsx b/src/components/orders/orders-table.tsx index ba63fb7..a49112e 100644 --- a/src/components/orders/orders-table.tsx +++ b/src/components/orders/orders-table.tsx @@ -15,11 +15,13 @@ import { Table, TableBody, TableCell, - TableHead, - TableHeader, TableRow, } from '@/components/ui/table'; import { Download, Search } from 'lucide-react'; +import { + SortableTableHead, + useSortableTable, +} from '@/components/ui/sortable-table-head'; type Props = { data: OrderRow[]; @@ -38,6 +40,9 @@ export function OrdersTable({ data }: Props) { ); }); + const { sortKey, sortDirection, handleSort, sortedData } = + useSortableTable(filteredData); + const handleExportCSV = () => { const headers = [ 'Order #', @@ -51,7 +56,7 @@ export function OrdersTable({ data }: Props) { 'Job #', ]; - const rows = filteredData.map((row) => [ + const rows = sortedData.map((row) => [ row.order_num?.toString() || '', row.customer_po || '', row.vorteq_part || '', @@ -101,23 +106,87 @@ export function OrdersTable({ data }: Props) { -
+
- - - Order # - Customer PO - Vorteq Part - Description - Plant - Warehouse - Qty Completed - Completion Date - Job # - - + + + + Order # + + + Customer PO + + + Vorteq Part + + + Description + + + Plant + + + Warehouse + + + Qty Completed + + + Completion Date + + + Job # + + + - {filteredData.length === 0 ? ( + {sortedData.length === 0 ? ( ) : ( - filteredData.map((row, i) => ( - + sortedData.map((row, i) => ( + {row.order_num} @@ -158,7 +227,7 @@ export function OrdersTable({ data }: Props) {
- Showing {filteredData.length} of {data.length} orders + Showing {sortedData.length} of {data.length} orders
diff --git a/src/components/ui/sortable-table-head.tsx b/src/components/ui/sortable-table-head.tsx new file mode 100644 index 0000000..baf8960 --- /dev/null +++ b/src/components/ui/sortable-table-head.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { useState } from 'react'; +import { ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +export type SortDirection = 'asc' | 'desc' | null; + +type SortableTableHeadProps = { + children: React.ReactNode; + sortKey: string; + currentSortKey: string | null; + currentDirection: SortDirection; + onSort: (key: string) => void; + className?: string; +}; + +export function SortableTableHead({ + children, + sortKey, + currentSortKey, + currentDirection, + onSort, + className, +}: SortableTableHeadProps) { + const isActive = currentSortKey === sortKey; + + return ( +
+ ); +} + +export function useSortableTable( + data: T[], + defaultSortKey: string | null = null, + defaultDirection: SortDirection = null +) { + const [sortKey, setSortKey] = useState(defaultSortKey); + const [sortDirection, setSortDirection] = + useState(defaultDirection); + + const handleSort = (key: string) => { + if (sortKey === key) { + if (sortDirection === 'asc') setSortDirection('desc'); + else if (sortDirection === 'desc') { + setSortKey(null); + setSortDirection(null); + } else setSortDirection('asc'); + } else { + setSortKey(key); + setSortDirection('asc'); + } + }; + + const sortedData = [...data].sort((a, b) => { + if (!sortKey || !sortDirection) return 0; + const aVal = (a as Record)[sortKey]; + const bVal = (b as Record)[sortKey]; + + if (aVal == null && bVal == null) return 0; + if (aVal == null) return 1; + if (bVal == null) return -1; + + let comparison = 0; + if (typeof aVal === 'number' && typeof bVal === 'number') { + comparison = aVal - bVal; + } else if (aVal instanceof Date && bVal instanceof Date) { + comparison = aVal.getTime() - bVal.getTime(); + } else { + comparison = String(aVal).localeCompare(String(bVal), undefined, { + numeric: true, + sensitivity: 'base', + }); + } + + return sortDirection === 'desc' ? -comparison : comparison; + }); + + return { sortKey, sortDirection, handleSort, sortedData }; +}
onSort(sortKey)} + > +
+ {children} + {isActive && currentDirection === 'asc' ? ( + + ) : isActive && currentDirection === 'desc' ? ( + + ) : ( + + )} +
+