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>
92 lines
2.6 KiB
TypeScript
92 lines
2.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, useCallback } from 'react';
|
|
import { subDays } from 'date-fns';
|
|
import type { CoilReceiptRow } from '@/types/coil-activity';
|
|
import { ReceiptsTable } from '@/components/coil-activity/receipts-table';
|
|
import { DateRangePicker } from '@/components/ui/date-range-picker';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
|
|
function LoadingSkeleton() {
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="space-y-4">
|
|
{[...Array(10)].map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="h-12 w-full animate-pulse rounded bg-muted"
|
|
/>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default function CoilReceiptsPage() {
|
|
const [data, setData] = useState<CoilReceiptRow[] | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [dateRange, setDateRange] = useState(() => ({
|
|
from: subDays(new Date(), 9),
|
|
to: new Date(),
|
|
}));
|
|
|
|
const fetchData = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const startDate = dateRange.from.toISOString().split('T')[0];
|
|
const endDate = dateRange.to.toISOString().split('T')[0];
|
|
const res = await fetch(
|
|
`/api/coil-activity/receipts?startDate=${startDate}&endDate=${endDate}`
|
|
);
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({}));
|
|
throw new Error(body.error || `HTTP ${res.status}`);
|
|
}
|
|
const json = await res.json();
|
|
setData(json);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : String(err));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [dateRange]);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
|
<div>
|
|
<h1 className="mb-2 text-3xl font-bold">Coil Activity — Receipts</h1>
|
|
<p className="text-muted-foreground">
|
|
View coil material receipts for the selected date range
|
|
</p>
|
|
</div>
|
|
<DateRangePicker
|
|
from={dateRange.from}
|
|
to={dateRange.to}
|
|
onUpdate={setDateRange}
|
|
maxDays={31}
|
|
/>
|
|
</div>
|
|
|
|
{error ? (
|
|
<Card>
|
|
<CardContent className="p-6 text-center text-destructive">
|
|
Failed to load receipts data: {error}
|
|
</CardContent>
|
|
</Card>
|
|
) : loading || data === null ? (
|
|
<LoadingSkeleton />
|
|
) : (
|
|
<ReceiptsTable data={data} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|