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,
|
||||
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({
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<div className="overflow-x-auto overflow-hidden 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>
|
||||
<thead>
|
||||
<tr className="bg-indigo-600 text-white">
|
||||
<SortableTableHead
|
||||
sortKey="CustPartNum"
|
||||
currentSortKey={sortKey}
|
||||
currentDirection={sortDirection}
|
||||
onSort={handleSort}
|
||||
>
|
||||
Cust Part#
|
||||
</SortableTableHead>
|
||||
<SortableTableHead
|
||||
sortKey="SkidNum"
|
||||
currentSortKey={sortKey}
|
||||
currentDirection={sortDirection}
|
||||
onSort={handleSort}
|
||||
>
|
||||
Skid#
|
||||
</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>
|
||||
{filteredData.length === 0 ? (
|
||||
{sortedData.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={14}
|
||||
|
|
@ -169,13 +313,13 @@ function DetailTableContent({
|
|||
</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>
|
||||
sortedData.map((row, i) => (
|
||||
<TableRow key={i} className={i % 2 === 0 ? 'bg-muted/30' : ''}>
|
||||
<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>
|
||||
|
|
@ -185,21 +329,13 @@ function DetailTableContent({
|
|||
<TableCell className="text-right">
|
||||
{formatNum(row.TheoreticalWeight)}
|
||||
</TableCell>
|
||||
<TableCell>{row.WIP_FG ?? ''}</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>{row.CustomerPoNum}</TableCell>
|
||||
<TableCell>{row.SalesOrderJob}</TableCell>
|
||||
<TableCell>{row.AllocRelease}</TableCell>
|
||||
<TableCell>
|
||||
{formatDate(row.DateProcessedToInventory)}
|
||||
</TableCell>
|
||||
|
|
@ -211,7 +347,7 @@ function DetailTableContent({
|
|||
</div>
|
||||
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
Showing {filteredData.length} of {data.length} items
|
||||
Showing {sortedData.length} of {data.length} items
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<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 = {
|
||||
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({
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Plant</TableHead>
|
||||
<TableHead>Cust Part#</TableHead>
|
||||
<TableHead>Vorteq Part#</TableHead>
|
||||
<TableHead>Vorteq Desc</TableHead>
|
||||
<TableHead>Warehouse</TableHead>
|
||||
<TableHead className="text-right">Qty LB</TableHead>
|
||||
<TableHead className="text-right">Rows</TableHead>
|
||||
<TableHead className="w-[70px]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<thead>
|
||||
<tr className={headerColor}>
|
||||
<SortableTableHead
|
||||
sortKey="plant"
|
||||
currentSortKey={sortKey}
|
||||
currentDirection={sortDirection}
|
||||
onSort={handleSort}
|
||||
>
|
||||
Plant
|
||||
</SortableTableHead>
|
||||
<SortableTableHead
|
||||
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>
|
||||
{filteredData.map((row, idx) => (
|
||||
<TableRow key={idx}>
|
||||
{sortedData.map((row, idx) => (
|
||||
<TableRow key={idx} className={idx % 2 === 0 ? 'bg-muted/30' : ''}>
|
||||
<TableCell>{row.plant}</TableCell>
|
||||
<TableCell>{row.cust_part_num || '-'}</TableCell>
|
||||
<TableCell className="font-medium">{row.part_num}</TableCell>
|
||||
|
|
@ -188,12 +255,12 @@ export function InventorySummaryTable({
|
|||
</div>
|
||||
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
Showing {filteredData.length} of {data.length} items
|
||||
Showing {sortedData.length} of {data.length} items
|
||||
</div>
|
||||
|
||||
{/* Detail Modal */}
|
||||
<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>
|
||||
<DialogTitle>Inventory Detail</DialogTitle>
|
||||
{selectedRow && (
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Order #</TableHead>
|
||||
<TableHead>Customer PO</TableHead>
|
||||
<TableHead>Vorteq Part</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>
|
||||
<thead>
|
||||
<tr className="bg-slate-700 text-white">
|
||||
<SortableTableHead
|
||||
sortKey="order_num"
|
||||
currentSortKey={sortKey}
|
||||
currentDirection={sortDirection}
|
||||
onSort={handleSort}
|
||||
>
|
||||
Order #
|
||||
</SortableTableHead>
|
||||
<SortableTableHead
|
||||
sortKey="customer_po"
|
||||
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>
|
||||
{filteredData.length === 0 ? (
|
||||
{sortedData.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={9}
|
||||
|
|
@ -127,8 +196,8 @@ export function OrdersTable({ data }: Props) {
|
|||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredData.map((row, i) => (
|
||||
<TableRow key={i}>
|
||||
sortedData.map((row, i) => (
|
||||
<TableRow key={i} className={i % 2 === 0 ? 'bg-muted/30' : ''}>
|
||||
<TableCell className="font-medium">
|
||||
{row.order_num}
|
||||
</TableCell>
|
||||
|
|
@ -158,7 +227,7 @@ export function OrdersTable({ data }: Props) {
|
|||
</div>
|
||||
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
Showing {filteredData.length} of {data.length} orders
|
||||
Showing {sortedData.length} of {data.length} orders
|
||||
</div>
|
||||
</CardContent>
|
||||
</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