feat: add sortable column headers with color-coded table headers
Add reusable SortableTableHead component and useSortableTable hook with tri-state sorting (asc/desc/none). Apply colored headers and zebra striping to orders, inventory summary, and inventory detail tables. Widen detail modal to 95vw. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9fad8af225
commit
a9ba3791f7
4 changed files with 481 additions and 112 deletions
|
|
@ -15,11 +15,13 @@ import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { Download, Search } from 'lucide-react';
|
import { Download, Search } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
SortableTableHead,
|
||||||
|
useSortableTable,
|
||||||
|
} from '@/components/ui/sortable-table-head';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: InventoryDetailRow[];
|
data: InventoryDetailRow[];
|
||||||
|
|
@ -43,6 +45,46 @@ function formatDate(val: string | null | undefined): string {
|
||||||
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
|
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({
|
function DetailTableContent({
|
||||||
data,
|
data,
|
||||||
partNum,
|
partNum,
|
||||||
|
|
@ -52,21 +94,25 @@ function DetailTableContent({
|
||||||
}) {
|
}) {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
const filteredData = data.filter((row) => {
|
const flatData = data.map(flattenRow);
|
||||||
|
|
||||||
|
const filteredData = flatData.filter((row) => {
|
||||||
if (!searchTerm) return true;
|
if (!searchTerm) return true;
|
||||||
const s = searchTerm.toLowerCase();
|
const s = searchTerm.toLowerCase();
|
||||||
return (
|
return (
|
||||||
row.CustPartNum?.toLowerCase().includes(s) ||
|
row.CustPartNum.toLowerCase().includes(s) ||
|
||||||
row.SkidNum?.toLowerCase().includes(s) ||
|
row.SkidNum.toLowerCase().includes(s) ||
|
||||||
row.LotNum?.toLowerCase().includes(s) ||
|
row.LotNum.toLowerCase().includes(s) ||
|
||||||
row.MfgLot?.toLowerCase().includes(s) ||
|
row.MfgLot.toLowerCase().includes(s) ||
|
||||||
row.Bin?.toLowerCase().includes(s) ||
|
row.Bin.toLowerCase().includes(s) ||
|
||||||
row.CustomerPoNum?.toLowerCase().includes(s) ||
|
row.CustomerPoNum.toLowerCase().includes(s) ||
|
||||||
row.VorteqSalesOrderNum?.toLowerCase().includes(s) ||
|
row.SalesOrderJob.toLowerCase().includes(s)
|
||||||
row.VorteqJobNum?.toLowerCase().includes(s)
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { sortKey, sortDirection, handleSort, sortedData } =
|
||||||
|
useSortableTable(filteredData);
|
||||||
|
|
||||||
const handleExportCSV = () => {
|
const handleExportCSV = () => {
|
||||||
const headers = [
|
const headers = [
|
||||||
'Cust Part#',
|
'Cust Part#',
|
||||||
|
|
@ -80,30 +126,26 @@ function DetailTableContent({
|
||||||
'WIP/FG',
|
'WIP/FG',
|
||||||
'# Coils Per Skid',
|
'# Coils Per Skid',
|
||||||
'PO #',
|
'PO #',
|
||||||
'Sales Order',
|
'Sales Order / Job',
|
||||||
'Job Order',
|
|
||||||
'Alloc # / Release Id',
|
'Alloc # / Release Id',
|
||||||
'Date Processed',
|
'Date Processed',
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = filteredData.map((row) => [
|
const rows = sortedData.map((row) => [
|
||||||
row.CustPartNum ?? '',
|
row.CustPartNum,
|
||||||
row.SkidNum ?? '',
|
row.SkidNum,
|
||||||
row.LotNum ?? '',
|
row.LotNum,
|
||||||
row.MfgLot ?? '',
|
row.MfgLot,
|
||||||
row.Bin ?? '',
|
row.Bin,
|
||||||
row.OnHandQty ?? '',
|
row.OnHandQty,
|
||||||
row.LinearFt ?? '',
|
row.LinearFt,
|
||||||
row.TheoreticalWeight ?? '',
|
row.TheoreticalWeight,
|
||||||
row.WIP_FG ?? '',
|
row.WIP_FG,
|
||||||
row.NumCoilsPerSkid ?? '',
|
row.NumCoilsPerSkid,
|
||||||
row.CustomerPoNum ?? '',
|
row.CustomerPoNum,
|
||||||
row.VorteqSalesOrderNum ?? '',
|
row.SalesOrderJob,
|
||||||
row.VorteqJobNum ?? '',
|
row.AllocRelease,
|
||||||
[row.AllocationNumber, row.ShipmentReleaseId]
|
row.DateProcessedToInventory,
|
||||||
.filter(Boolean)
|
|
||||||
.join(' / '),
|
|
||||||
row.DateProcessedToInventory ?? '',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const csvContent = [headers, ...rows]
|
const csvContent = [headers, ...rows]
|
||||||
|
|
@ -138,28 +180,130 @@ function DetailTableContent({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto rounded-md border">
|
<div className="overflow-x-auto overflow-hidden rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<thead>
|
||||||
<TableRow>
|
<tr className="bg-indigo-600 text-white">
|
||||||
<TableHead>Cust Part#</TableHead>
|
<SortableTableHead
|
||||||
<TableHead>Skid#</TableHead>
|
sortKey="CustPartNum"
|
||||||
<TableHead>Lot#</TableHead>
|
currentSortKey={sortKey}
|
||||||
<TableHead>Mfg Lot#</TableHead>
|
currentDirection={sortDirection}
|
||||||
<TableHead>Bin#</TableHead>
|
onSort={handleSort}
|
||||||
<TableHead className="text-right">Qty LB</TableHead>
|
>
|
||||||
<TableHead className="text-right">Lin. Ft</TableHead>
|
Cust Part#
|
||||||
<TableHead className="text-right">Theo. Wt</TableHead>
|
</SortableTableHead>
|
||||||
<TableHead>WIP/FG</TableHead>
|
<SortableTableHead
|
||||||
<TableHead className="text-right"># Coils</TableHead>
|
sortKey="SkidNum"
|
||||||
<TableHead>PO #</TableHead>
|
currentSortKey={sortKey}
|
||||||
<TableHead>Sales Order / Job</TableHead>
|
currentDirection={sortDirection}
|
||||||
<TableHead>Alloc # / Release</TableHead>
|
onSort={handleSort}
|
||||||
<TableHead>Date Processed</TableHead>
|
>
|
||||||
</TableRow>
|
Skid#
|
||||||
</TableHeader>
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="LotNum"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Lot#
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="MfgLot"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Mfg Lot#
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="Bin"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Bin#
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="OnHandQty"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
Qty LB
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="LinearFt"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
Lin. Ft
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="TheoreticalWeight"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
Theo. Wt
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="WIP_FG"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
WIP/FG
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="NumCoilsPerSkid"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
# Coils
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="CustomerPoNum"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
PO #
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="SalesOrderJob"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Sales Order / Job
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="AllocRelease"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Alloc # / Release
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="DateProcessedToInventory"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Date Processed
|
||||||
|
</SortableTableHead>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredData.length === 0 ? (
|
{sortedData.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={14}
|
colSpan={14}
|
||||||
|
|
@ -169,13 +313,13 @@ function DetailTableContent({
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
filteredData.map((row, i) => (
|
sortedData.map((row, i) => (
|
||||||
<TableRow key={i}>
|
<TableRow key={i} className={i % 2 === 0 ? 'bg-muted/30' : ''}>
|
||||||
<TableCell>{row.CustPartNum ?? ''}</TableCell>
|
<TableCell>{row.CustPartNum}</TableCell>
|
||||||
<TableCell>{row.SkidNum ?? ''}</TableCell>
|
<TableCell>{row.SkidNum}</TableCell>
|
||||||
<TableCell>{row.LotNum ?? ''}</TableCell>
|
<TableCell>{row.LotNum}</TableCell>
|
||||||
<TableCell>{row.MfgLot ?? ''}</TableCell>
|
<TableCell>{row.MfgLot}</TableCell>
|
||||||
<TableCell>{row.Bin ?? ''}</TableCell>
|
<TableCell>{row.Bin}</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{formatNum(row.OnHandQty)}
|
{formatNum(row.OnHandQty)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -185,21 +329,13 @@ function DetailTableContent({
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{formatNum(row.TheoreticalWeight)}
|
{formatNum(row.TheoreticalWeight)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.WIP_FG ?? ''}</TableCell>
|
<TableCell>{row.WIP_FG}</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{formatNum(row.NumCoilsPerSkid)}
|
{formatNum(row.NumCoilsPerSkid)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.CustomerPoNum ?? ''}</TableCell>
|
<TableCell>{row.CustomerPoNum}</TableCell>
|
||||||
<TableCell>
|
<TableCell>{row.SalesOrderJob}</TableCell>
|
||||||
{[row.VorteqSalesOrderNum, row.VorteqJobNum]
|
<TableCell>{row.AllocRelease}</TableCell>
|
||||||
.filter(Boolean)
|
|
||||||
.join(' / ')}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{[row.AllocationNumber, row.ShipmentReleaseId]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' / ')}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{formatDate(row.DateProcessedToInventory)}
|
{formatDate(row.DateProcessedToInventory)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -211,7 +347,7 @@ function DetailTableContent({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 text-sm text-muted-foreground">
|
<div className="mt-4 text-sm text-muted-foreground">
|
||||||
Showing {filteredData.length} of {data.length} items
|
Showing {sortedData.length} of {data.length} items
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
@ -22,6 +20,19 @@ import { Search, Download, Eye, Loader2 } from 'lucide-react';
|
||||||
import { InventorySummaryRow } from '@/services/inventory';
|
import { InventorySummaryRow } from '@/services/inventory';
|
||||||
import type { InventoryDetailRow } from '@/services/inventory';
|
import type { InventoryDetailRow } from '@/services/inventory';
|
||||||
import { InventoryDetailTable } from './inventory-detail-table';
|
import { InventoryDetailTable } from './inventory-detail-table';
|
||||||
|
import {
|
||||||
|
SortableTableHead,
|
||||||
|
useSortableTable,
|
||||||
|
} from '@/components/ui/sortable-table-head';
|
||||||
|
|
||||||
|
const CATEGORY_COLORS: Record<string, string> = {
|
||||||
|
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 = {
|
type InventorySummaryTableProps = {
|
||||||
data: InventorySummaryRow[];
|
data: InventorySummaryRow[];
|
||||||
|
|
@ -49,6 +60,11 @@ export function InventorySummaryTable({
|
||||||
row.cust_part_num?.toLowerCase().includes(searchTerm.toLowerCase())
|
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 handleExportCSV = () => {
|
||||||
const headers = [
|
const headers = [
|
||||||
'Plant',
|
'Plant',
|
||||||
|
|
@ -59,7 +75,7 @@ export function InventorySummaryTable({
|
||||||
'Qty LB',
|
'Qty LB',
|
||||||
'Rows',
|
'Rows',
|
||||||
];
|
];
|
||||||
const rows = filteredData.map((row) => [
|
const rows = sortedData.map((row) => [
|
||||||
row.plant,
|
row.plant,
|
||||||
row.cust_part_num || '',
|
row.cust_part_num || '',
|
||||||
row.part_num,
|
row.part_num,
|
||||||
|
|
@ -143,23 +159,74 @@ export function InventorySummaryTable({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<thead>
|
||||||
<TableRow>
|
<tr className={headerColor}>
|
||||||
<TableHead>Plant</TableHead>
|
<SortableTableHead
|
||||||
<TableHead>Cust Part#</TableHead>
|
sortKey="plant"
|
||||||
<TableHead>Vorteq Part#</TableHead>
|
currentSortKey={sortKey}
|
||||||
<TableHead>Vorteq Desc</TableHead>
|
currentDirection={sortDirection}
|
||||||
<TableHead>Warehouse</TableHead>
|
onSort={handleSort}
|
||||||
<TableHead className="text-right">Qty LB</TableHead>
|
>
|
||||||
<TableHead className="text-right">Rows</TableHead>
|
Plant
|
||||||
<TableHead className="w-[70px]"></TableHead>
|
</SortableTableHead>
|
||||||
</TableRow>
|
<SortableTableHead
|
||||||
</TableHeader>
|
sortKey="cust_part_num"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Cust Part#
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="part_num"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Vorteq Part#
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="description"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Vorteq Desc
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="warehouse"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Warehouse
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="on_hand_qty"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
Qty LB
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="rows"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
Rows
|
||||||
|
</SortableTableHead>
|
||||||
|
<th className="w-[70px]"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredData.map((row, idx) => (
|
{sortedData.map((row, idx) => (
|
||||||
<TableRow key={idx}>
|
<TableRow key={idx} className={idx % 2 === 0 ? 'bg-muted/30' : ''}>
|
||||||
<TableCell>{row.plant}</TableCell>
|
<TableCell>{row.plant}</TableCell>
|
||||||
<TableCell>{row.cust_part_num || '-'}</TableCell>
|
<TableCell>{row.cust_part_num || '-'}</TableCell>
|
||||||
<TableCell className="font-medium">{row.part_num}</TableCell>
|
<TableCell className="font-medium">{row.part_num}</TableCell>
|
||||||
|
|
@ -188,12 +255,12 @@ export function InventorySummaryTable({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 text-sm text-muted-foreground">
|
<div className="mt-4 text-sm text-muted-foreground">
|
||||||
Showing {filteredData.length} of {data.length} items
|
Showing {sortedData.length} of {data.length} items
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Detail Modal */}
|
{/* Detail Modal */}
|
||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<DialogContent className="max-h-[90vh] max-w-6xl overflow-hidden">
|
<DialogContent className="max-h-[90vh] w-[95vw] max-w-[95vw] overflow-hidden">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Inventory Detail</DialogTitle>
|
<DialogTitle>Inventory Detail</DialogTitle>
|
||||||
{selectedRow && (
|
{selectedRow && (
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,13 @@ import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { Download, Search } from 'lucide-react';
|
import { Download, Search } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
SortableTableHead,
|
||||||
|
useSortableTable,
|
||||||
|
} from '@/components/ui/sortable-table-head';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: OrderRow[];
|
data: OrderRow[];
|
||||||
|
|
@ -38,6 +40,9 @@ export function OrdersTable({ data }: Props) {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { sortKey, sortDirection, handleSort, sortedData } =
|
||||||
|
useSortableTable(filteredData);
|
||||||
|
|
||||||
const handleExportCSV = () => {
|
const handleExportCSV = () => {
|
||||||
const headers = [
|
const headers = [
|
||||||
'Order #',
|
'Order #',
|
||||||
|
|
@ -51,7 +56,7 @@ export function OrdersTable({ data }: Props) {
|
||||||
'Job #',
|
'Job #',
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = filteredData.map((row) => [
|
const rows = sortedData.map((row) => [
|
||||||
row.order_num?.toString() || '',
|
row.order_num?.toString() || '',
|
||||||
row.customer_po || '',
|
row.customer_po || '',
|
||||||
row.vorteq_part || '',
|
row.vorteq_part || '',
|
||||||
|
|
@ -101,23 +106,87 @@ export function OrdersTable({ data }: Props) {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-md border">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<thead>
|
||||||
<TableRow>
|
<tr className="bg-slate-700 text-white">
|
||||||
<TableHead>Order #</TableHead>
|
<SortableTableHead
|
||||||
<TableHead>Customer PO</TableHead>
|
sortKey="order_num"
|
||||||
<TableHead>Vorteq Part</TableHead>
|
currentSortKey={sortKey}
|
||||||
<TableHead>Description</TableHead>
|
currentDirection={sortDirection}
|
||||||
<TableHead>Plant</TableHead>
|
onSort={handleSort}
|
||||||
<TableHead>Warehouse</TableHead>
|
>
|
||||||
<TableHead className="text-right">Qty Completed</TableHead>
|
Order #
|
||||||
<TableHead>Completion Date</TableHead>
|
</SortableTableHead>
|
||||||
<TableHead>Job #</TableHead>
|
<SortableTableHead
|
||||||
</TableRow>
|
sortKey="customer_po"
|
||||||
</TableHeader>
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Customer PO
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="vorteq_part"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Vorteq Part
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="part_description"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Description
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="plant"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Plant
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="warehouse"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Warehouse
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="qty_completed"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
Qty Completed
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="completion_date"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Completion Date
|
||||||
|
</SortableTableHead>
|
||||||
|
<SortableTableHead
|
||||||
|
sortKey="job_num"
|
||||||
|
currentSortKey={sortKey}
|
||||||
|
currentDirection={sortDirection}
|
||||||
|
onSort={handleSort}
|
||||||
|
>
|
||||||
|
Job #
|
||||||
|
</SortableTableHead>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredData.length === 0 ? (
|
{sortedData.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={9}
|
colSpan={9}
|
||||||
|
|
@ -127,8 +196,8 @@ export function OrdersTable({ data }: Props) {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
filteredData.map((row, i) => (
|
sortedData.map((row, i) => (
|
||||||
<TableRow key={i}>
|
<TableRow key={i} className={i % 2 === 0 ? 'bg-muted/30' : ''}>
|
||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
{row.order_num}
|
{row.order_num}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -158,7 +227,7 @@ export function OrdersTable({ data }: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 text-sm text-muted-foreground">
|
<div className="mt-4 text-sm text-muted-foreground">
|
||||||
Showing {filteredData.length} of {data.length} orders
|
Showing {sortedData.length} of {data.length} orders
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
97
src/components/ui/sortable-table-head.tsx
Normal file
97
src/components/ui/sortable-table-head.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<th
|
||||||
|
className={cn(
|
||||||
|
'h-10 cursor-pointer select-none px-4 text-left align-middle text-xs font-semibold uppercase tracking-wider transition-colors hover:bg-white/10',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={() => onSort(sortKey)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{children}
|
||||||
|
{isActive && currentDirection === 'asc' ? (
|
||||||
|
<ArrowUp className="h-3.5 w-3.5 shrink-0" />
|
||||||
|
) : isActive && currentDirection === 'desc' ? (
|
||||||
|
<ArrowDown className="h-3.5 w-3.5 shrink-0" />
|
||||||
|
) : (
|
||||||
|
<ArrowUpDown className="h-3.5 w-3.5 shrink-0 opacity-40" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSortableTable<T>(
|
||||||
|
data: T[],
|
||||||
|
defaultSortKey: string | null = null,
|
||||||
|
defaultDirection: SortDirection = null
|
||||||
|
) {
|
||||||
|
const [sortKey, setSortKey] = useState<string | null>(defaultSortKey);
|
||||||
|
const [sortDirection, setSortDirection] =
|
||||||
|
useState<SortDirection>(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<string, unknown>)[sortKey];
|
||||||
|
const bVal = (b as Record<string, unknown>)[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 };
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue