251 lines
8.3 KiB
TypeScript
251 lines
8.3 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useEffect, useState } from 'react';
|
||
|
|
import { useParams, useRouter } from 'next/navigation';
|
||
|
|
import type { ShipRequestHeader } from '@/types/requests';
|
||
|
|
import {
|
||
|
|
Card,
|
||
|
|
CardContent,
|
||
|
|
CardDescription,
|
||
|
|
CardHeader,
|
||
|
|
CardTitle,
|
||
|
|
} from '@/components/ui/card';
|
||
|
|
import { Button } from '@/components/ui/button';
|
||
|
|
import {
|
||
|
|
Table,
|
||
|
|
TableBody,
|
||
|
|
TableCell,
|
||
|
|
TableRow,
|
||
|
|
} from '@/components/ui/table';
|
||
|
|
import { RequestStatusBadge } from '@/components/requests/request-status-badge';
|
||
|
|
import { RequestCancelDialog } from '@/components/requests/request-cancel-dialog';
|
||
|
|
import { ArrowLeft, Loader2 } from 'lucide-react';
|
||
|
|
import { formatDate } from '@/lib/utils';
|
||
|
|
import Link from 'next/link';
|
||
|
|
import { useToast } from '@/hooks/use-toast';
|
||
|
|
|
||
|
|
export default function ShipmentRequestDetailPage() {
|
||
|
|
const params = useParams();
|
||
|
|
const router = useRouter();
|
||
|
|
const { toast } = useToast();
|
||
|
|
const [request, setRequest] = useState<ShipRequestHeader | null>(null);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
fetch(`/api/shipment-requests/${params.id}`)
|
||
|
|
.then((res) => {
|
||
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
|
|
return res.json();
|
||
|
|
})
|
||
|
|
.then((data) => setRequest(data))
|
||
|
|
.catch((err) => setError(err.message))
|
||
|
|
.finally(() => setLoading(false));
|
||
|
|
}, [params.id]);
|
||
|
|
|
||
|
|
const handleCancel = async () => {
|
||
|
|
if (!request) return;
|
||
|
|
const res = await fetch(`/api/shipment-requests/${request.id}/cancel`, {
|
||
|
|
method: 'POST',
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
const err = await res.json();
|
||
|
|
toast({
|
||
|
|
title: 'Error',
|
||
|
|
description: err.error || 'Failed to cancel',
|
||
|
|
variant: 'destructive',
|
||
|
|
});
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
toast({ title: 'Request cancelled' });
|
||
|
|
router.push('/shipment-requests');
|
||
|
|
};
|
||
|
|
|
||
|
|
if (loading) {
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<h1 className="mb-6 text-3xl font-bold">Shipment Request</h1>
|
||
|
|
<Card>
|
||
|
|
<CardContent className="flex items-center justify-center py-12">
|
||
|
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (error || !request) {
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<h1 className="mb-6 text-3xl font-bold">Shipment Request</h1>
|
||
|
|
<Card>
|
||
|
|
<CardContent className="p-6 text-center text-destructive">
|
||
|
|
{error || 'Request not found'}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<div className="mb-6 flex items-center justify-between">
|
||
|
|
<h1 className="text-3xl font-bold">Shipment Request</h1>
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<RequestStatusBadge
|
||
|
|
isSubmitted={request.is_submitted}
|
||
|
|
isCancelled={request.is_cancelled}
|
||
|
|
/>
|
||
|
|
{request.is_submitted && !request.is_cancelled && (
|
||
|
|
<RequestCancelDialog
|
||
|
|
requestType="shipment"
|
||
|
|
onConfirm={handleCancel}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>Request Details</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
Created {formatDate(new Date(request.created_at))}
|
||
|
|
{request.submitted_at &&
|
||
|
|
` | Submitted ${formatDate(new Date(request.submitted_at))}`}
|
||
|
|
{request.cancelled_at &&
|
||
|
|
` | Cancelled ${formatDate(new Date(request.cancelled_at))}`}
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
||
|
|
<div>
|
||
|
|
<span className="text-xs font-semibold text-muted-foreground uppercase">
|
||
|
|
Ship-To Address
|
||
|
|
</span>
|
||
|
|
<p className="text-sm">{request.ship_to_address || '-'}</p>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<span className="text-xs font-semibold text-muted-foreground uppercase">
|
||
|
|
Order #
|
||
|
|
</span>
|
||
|
|
<p className="text-sm">{request.order_number || '-'}</p>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<span className="text-xs font-semibold text-muted-foreground uppercase">
|
||
|
|
PO #
|
||
|
|
</span>
|
||
|
|
<p className="text-sm">{request.po_number || '-'}</p>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<span className="text-xs font-semibold text-muted-foreground uppercase">
|
||
|
|
Release #
|
||
|
|
</span>
|
||
|
|
<p className="text-sm">{request.release_number || '-'}</p>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<span className="text-xs font-semibold text-muted-foreground uppercase">
|
||
|
|
Pickup Date
|
||
|
|
</span>
|
||
|
|
<p className="text-sm">
|
||
|
|
{request.pickup_date
|
||
|
|
? formatDate(new Date(request.pickup_date))
|
||
|
|
: '-'}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{request.instructions && (
|
||
|
|
<div>
|
||
|
|
<span className="text-xs font-semibold text-muted-foreground uppercase">
|
||
|
|
Instructions
|
||
|
|
</span>
|
||
|
|
<p className="whitespace-pre-wrap text-sm">
|
||
|
|
{request.instructions}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{request.email_recipients.length > 0 && (
|
||
|
|
<div>
|
||
|
|
<span className="text-xs font-semibold text-muted-foreground uppercase">
|
||
|
|
Email Recipients
|
||
|
|
</span>
|
||
|
|
<div className="mt-1 flex flex-wrap gap-1">
|
||
|
|
{request.email_recipients.map((email) => (
|
||
|
|
<span
|
||
|
|
key={email}
|
||
|
|
className="rounded-full bg-teal-100 px-2.5 py-0.5 text-xs font-medium text-teal-800"
|
||
|
|
>
|
||
|
|
{email}
|
||
|
|
</span>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Items */}
|
||
|
|
<div className="pt-2">
|
||
|
|
<h3 className="mb-2 text-sm font-semibold text-muted-foreground uppercase">
|
||
|
|
Items ({request.details.length})
|
||
|
|
</h3>
|
||
|
|
<div className="overflow-hidden rounded-md border">
|
||
|
|
<Table>
|
||
|
|
<thead>
|
||
|
|
<tr className="bg-teal-700 text-white">
|
||
|
|
<th className="px-3 py-2 text-left text-xs font-semibold uppercase">
|
||
|
|
Part #
|
||
|
|
</th>
|
||
|
|
<th className="px-3 py-2 text-left text-xs font-semibold uppercase">
|
||
|
|
Lot #
|
||
|
|
</th>
|
||
|
|
<th className="px-3 py-2 text-left text-xs font-semibold uppercase">
|
||
|
|
Plant
|
||
|
|
</th>
|
||
|
|
<th className="px-3 py-2 text-left text-xs font-semibold uppercase">
|
||
|
|
Warehouse
|
||
|
|
</th>
|
||
|
|
<th className="px-3 py-2 text-right text-xs font-semibold uppercase">
|
||
|
|
Qty
|
||
|
|
</th>
|
||
|
|
<th className="px-3 py-2 text-left text-xs font-semibold uppercase">
|
||
|
|
Notes
|
||
|
|
</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<TableBody>
|
||
|
|
{request.details.map((item, i) => (
|
||
|
|
<TableRow
|
||
|
|
key={item.id}
|
||
|
|
className={i % 2 === 0 ? 'bg-muted/30' : ''}
|
||
|
|
>
|
||
|
|
<TableCell className="font-medium">
|
||
|
|
{item.part_num}
|
||
|
|
</TableCell>
|
||
|
|
<TableCell>{item.lot_num || '-'}</TableCell>
|
||
|
|
<TableCell>{item.plant}</TableCell>
|
||
|
|
<TableCell>{item.warehouse || '-'}</TableCell>
|
||
|
|
<TableCell className="text-right">
|
||
|
|
{item.quantity}
|
||
|
|
</TableCell>
|
||
|
|
<TableCell>{item.notes || '-'}</TableCell>
|
||
|
|
</TableRow>
|
||
|
|
))}
|
||
|
|
</TableBody>
|
||
|
|
</Table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="pt-4">
|
||
|
|
<Link href="/shipment-requests">
|
||
|
|
<Button variant="outline">
|
||
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||
|
|
Back to List
|
||
|
|
</Button>
|
||
|
|
</Link>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|