feat: add sortable column headers with color-coded table headers
Some checks failed
Build and Deploy / build (push) Failing after 11m47s
Build and Deploy / deploy (push) Has been cancelled

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:
Lorentz 2026-02-16 20:35:09 +00:00
parent 9fad8af225
commit a9ba3791f7
4 changed files with 481 additions and 112 deletions

View file

@ -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>
</> </>
); );

View file

@ -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 && (

View file

@ -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>

View 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 };
}