quest-vorteq/src/app/(portal)/shipment-requests/[id]/page.tsx

251 lines
8.3 KiB
TypeScript
Raw Normal View History

'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>
);
}