quest-vorteq/src/components/coil-activity/usage-table.tsx
Lorentz Hinrichsen 53c35cc273
Some checks failed
Build and Deploy / build (push) Successful in 10m53s
Build and Deploy / deploy (push) Failing after 2s
feat: add order ack detail/PDF, BOL detail/PDF, coil activity usage & receipts
C-005: Order Acknowledgement Detail + PDF
- Order detail page with full line items, releases, addresses, paint codes
- PDF export via Puppeteer matching legacy format
- Clickable order # links and PDF icons in orders table

C-007: BOL Detail + PDF
- BOL detail page with ship-from/to, line items, weights
- PDF export matching legacy BOL format
- PDF icon column in shipments table

C-008: Coil Activity - Usage Report
- Date range picker (max 31 days, default last 10 days)
- Reusable UI: Popover, Calendar (react-day-picker v9), DateRangePicker
- SQL from legacy portal_CoilActivityUsage.sql with OnHandQty dedup
- 11-column sortable table with search and CSV export

C-009: Coil Activity - Receipts Report
- VGL customer exception (special SQL vs portal view)
- 10-column sortable table with search and CSV export
- Shared date range picker component

Also: dashboard API route, shipments API route, HDC→HDM mapping,
Puppeteer PDF infrastructure, improved error handling.

Progress: 9/16 Phase 2 tasks complete.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:14:52 -05:00

262 lines
8.6 KiB
TypeScript

'use client';
import { useState } from 'react';
import type { CoilUsageRow } from '@/types/coil-activity';
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,
TableRow,
} from '@/components/ui/table';
import { Download, Search } from 'lucide-react';
import {
SortableTableHead,
useSortableTable,
} from '@/components/ui/sortable-table-head';
import { formatDate } from '@/lib/utils';
type Props = {
data: CoilUsageRow[];
};
export function UsageTable({ data }: Props) {
const [searchTerm, setSearchTerm] = useState('');
const filteredData = data.filter((row) => {
const s = searchTerm.toLowerCase();
return (
(row.vorteq_part_num || '').toLowerCase().includes(s) ||
(row.customer_part_num || '').toLowerCase().includes(s) ||
(row.part_desc || '').toLowerCase().includes(s) ||
(row.lot_num || '').toLowerCase().includes(s) ||
(row.mfg_lot || '').toLowerCase().includes(s) ||
(row.plant_name || '').toLowerCase().includes(s) ||
(row.job_num || '').toLowerCase().includes(s) ||
(row.customer_po || '').toLowerCase().includes(s)
);
});
const { sortKey, sortDirection, handleSort, sortedData } =
useSortableTable(filteredData);
const handleExportCSV = () => {
const headers = [
'Date Used',
'Vorteq Part#',
'Cust Part#',
'Part Description',
'Lot#',
'Mfg Lot#',
'Weight',
'Plant Name',
'Job#',
'Cust PO#',
'Qty LB',
];
const rows = sortedData.map((row) => [
row.date_used ? new Date(row.date_used).toLocaleDateString() : '',
row.vorteq_part_num || '',
row.customer_part_num || '',
row.part_desc || '',
row.lot_num || '',
row.mfg_lot || '',
row.weight ?? '',
row.plant_name || '',
row.job_num || '',
row.customer_po || '',
row.on_hand_qty ?? '',
]);
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 = `usage-data-${new Date().toISOString().split('T')[0]}.csv`;
a.click();
window.URL.revokeObjectURL(url);
};
return (
<Card>
<CardHeader>
<CardTitle>Usage Data</CardTitle>
<CardDescription>Showing {sortedData.length} results</CardDescription>
</CardHeader>
<CardContent>
<div className="mb-4 flex items-center gap-4">
<div className="relative flex-1">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search part numbers, descriptions, lots, plants, jobs..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8"
/>
</div>
<Button onClick={handleExportCSV} variant="outline">
<Download className="mr-2 h-4 w-4" />
Export CSV
</Button>
</div>
<div className="overflow-hidden rounded-md border">
<Table>
<thead>
<tr className="bg-teal-700 text-white">
<SortableTableHead
sortKey="date_used"
currentSortKey={sortKey}
currentDirection={sortDirection}
onSort={handleSort}
>
Date Used
</SortableTableHead>
<SortableTableHead
sortKey="vorteq_part_num"
currentSortKey={sortKey}
currentDirection={sortDirection}
onSort={handleSort}
>
Vorteq Part#
</SortableTableHead>
<SortableTableHead
sortKey="customer_part_num"
currentSortKey={sortKey}
currentDirection={sortDirection}
onSort={handleSort}
>
Cust Part#
</SortableTableHead>
<SortableTableHead
sortKey="part_desc"
currentSortKey={sortKey}
currentDirection={sortDirection}
onSort={handleSort}
>
Part Description
</SortableTableHead>
<SortableTableHead
sortKey="lot_num"
currentSortKey={sortKey}
currentDirection={sortDirection}
onSort={handleSort}
>
Lot#
</SortableTableHead>
<SortableTableHead
sortKey="mfg_lot"
currentSortKey={sortKey}
currentDirection={sortDirection}
onSort={handleSort}
>
Mfg Lot#
</SortableTableHead>
<SortableTableHead
sortKey="weight"
currentSortKey={sortKey}
currentDirection={sortDirection}
onSort={handleSort}
className="text-right"
>
Weight
</SortableTableHead>
<SortableTableHead
sortKey="plant_name"
currentSortKey={sortKey}
currentDirection={sortDirection}
onSort={handleSort}
>
Plant Name
</SortableTableHead>
<SortableTableHead
sortKey="job_num"
currentSortKey={sortKey}
currentDirection={sortDirection}
onSort={handleSort}
>
Job#
</SortableTableHead>
<SortableTableHead
sortKey="customer_po"
currentSortKey={sortKey}
currentDirection={sortDirection}
onSort={handleSort}
>
Cust PO#
</SortableTableHead>
<SortableTableHead
sortKey="on_hand_qty"
currentSortKey={sortKey}
currentDirection={sortDirection}
onSort={handleSort}
className="text-right"
>
Qty LB
</SortableTableHead>
</tr>
</thead>
<TableBody>
{sortedData.length === 0 ? (
<TableRow>
<TableCell
colSpan={11}
className="text-center text-muted-foreground"
>
No usage data found
</TableCell>
</TableRow>
) : (
sortedData.map((row, i) => (
<TableRow
key={i}
className={i % 2 === 0 ? 'bg-muted/30' : ''}
>
<TableCell>
{row.date_used ? formatDate(new Date(row.date_used)) : '-'}
</TableCell>
<TableCell>{row.vorteq_part_num || '-'}</TableCell>
<TableCell>{row.customer_part_num || '-'}</TableCell>
<TableCell>{row.part_desc || '-'}</TableCell>
<TableCell>{row.lot_num || '-'}</TableCell>
<TableCell>{row.mfg_lot || '-'}</TableCell>
<TableCell className="text-right">
{row.weight != null
? row.weight.toLocaleString()
: '-'}
</TableCell>
<TableCell>{row.plant_name || '-'}</TableCell>
<TableCell>{row.job_num || '-'}</TableCell>
<TableCell>{row.customer_po || '-'}</TableCell>
<TableCell className="text-right">
{row.on_hand_qty != null
? row.on_hand_qty.toLocaleString()
: '-'}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<div className="mt-4 text-sm text-muted-foreground">
Showing {sortedData.length} of {data.length} records
</div>
</CardContent>
</Card>
);
}