93 lines
2.6 KiB
TypeScript
93 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>
|
||
|
|
);
|
||
|
|
}
|