mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat: setup the recent booking endpoint and ui fixes
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { format } from "date-fns";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowRight,
|
||||
Building2,
|
||||
@@ -7,6 +9,7 @@ import {
|
||||
Clock,
|
||||
DollarSign,
|
||||
Eye,
|
||||
LoaderCircle,
|
||||
Mail,
|
||||
MapPin,
|
||||
Package,
|
||||
@@ -20,14 +23,12 @@ import {
|
||||
|
||||
import {
|
||||
getCurrentCustomer,
|
||||
getMyBookings,
|
||||
getMyInvoices,
|
||||
getMyShipments,
|
||||
} from "@/lib/currentCustomer";
|
||||
import { formatCurrency } from "@/pages/billing/invoices.mock";
|
||||
import type { ShipmentStatus } from "@/pages/tracking/shipments.mock";
|
||||
import type { InvoiceStatus } from "@/pages/billing/invoices.mock";
|
||||
import type { BookingStatus } from "@/pages/bookings/bookings.mock";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -36,16 +37,36 @@ import {
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@edr/ui-common";
|
||||
import { api } from "@/services/api";
|
||||
|
||||
const ACTIVE_STATUSES = [
|
||||
"DRAFT",
|
||||
"SUBMITTED",
|
||||
"PENDING_APPROVAL",
|
||||
"IN_TRANSIT",
|
||||
];
|
||||
|
||||
export default function MyPortalPage() {
|
||||
const me = useMemo(() => getCurrentCustomer(), []);
|
||||
const myBookings = useMemo(() => getMyBookings(), []);
|
||||
const myShipments = useMemo(() => getMyShipments(), []);
|
||||
const myInvoices = useMemo(() => getMyInvoices(), []);
|
||||
|
||||
const activeBookings = myBookings.filter(
|
||||
(b) => b.status === "Confirmed" || b.status === "In Transit",
|
||||
const navigate = useNavigate();
|
||||
|
||||
const bookingsQuery = useQuery(
|
||||
api.bookings.list.queryOptions({
|
||||
input: { sortBy: "createdAt", sortOrder: "DESC" },
|
||||
}),
|
||||
);
|
||||
|
||||
const myBookings = useMemo(
|
||||
() =>
|
||||
(bookingsQuery.data?.items ?? []).filter((b) =>
|
||||
ACTIVE_STATUSES.includes(b.status),
|
||||
),
|
||||
[bookingsQuery.data],
|
||||
);
|
||||
|
||||
const activeShipments = myShipments.filter((s) => s.status === "In Transit");
|
||||
const outstandingInvoices = myInvoices.filter(
|
||||
(inv) => inv.status === "Sent" || inv.status === "Overdue",
|
||||
@@ -58,7 +79,7 @@ export default function MyPortalPage() {
|
||||
.filter((inv) => inv.status === "Paid" && inv.currency === "USD")
|
||||
.reduce((sum, inv) => sum + inv.amount, 0);
|
||||
|
||||
const recentBookings = [...myBookings].slice(0, 5);
|
||||
const recentBookings = myBookings.slice(0, 5);
|
||||
const recentInvoices = [...myInvoices].slice(0, 4);
|
||||
|
||||
return (
|
||||
@@ -117,7 +138,7 @@ export default function MyPortalPage() {
|
||||
<Link to="/bookings/new">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="bg-white text-[#10B981] hover:bg-slate-100"
|
||||
className="bg-white text-[#10B981] hover:bg-muted"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
New Booking
|
||||
@@ -156,7 +177,7 @@ export default function MyPortalPage() {
|
||||
|
||||
<CardContent>
|
||||
{activeShipments.length === 0 ? (
|
||||
<p className="rounded-2xl border border-dashed border-slate-200 p-8 text-center text-sm text-slate-500">
|
||||
<p className="rounded-2xl border border-dashed border-border p-8 text-center text-sm text-muted-foreground">
|
||||
No shipments currently in transit.
|
||||
</p>
|
||||
) : (
|
||||
@@ -164,27 +185,27 @@ export default function MyPortalPage() {
|
||||
{activeShipments.slice(0, 4).map((shipment) => (
|
||||
<div
|
||||
key={shipment.id}
|
||||
className="rounded-2xl border border-slate-100 p-4 transition hover:border-primary/20 hover:bg-primary/5"
|
||||
className="rounded-2xl border border-border p-4 transition hover:border-primary/20 hover:bg-primary/5"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-semibold text-slate-900">
|
||||
<span className="font-semibold text-foreground">
|
||||
{shipment.reference}
|
||||
</span>
|
||||
<ShipmentBadge status={shipment.status} />
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-slate-700">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{shipment.originStation}
|
||||
<ArrowRight className="mx-2 inline h-3 w-3 text-slate-400" />
|
||||
{shipment.destinationStation}
|
||||
</p>
|
||||
<div className="mt-3 flex items-center justify-between text-xs text-slate-500">
|
||||
<div className="mt-3 flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<MapPin className="h-3 w-3 text-primary" />
|
||||
{shipment.currentLocation}
|
||||
</span>
|
||||
<span>ETA {shipment.eta}</span>
|
||||
</div>
|
||||
<div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-slate-100">
|
||||
<div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary transition-all"
|
||||
style={{ width: `${shipment.progress}%` }}
|
||||
@@ -215,50 +236,43 @@ export default function MyPortalPage() {
|
||||
|
||||
<CardContent className="px-0">
|
||||
{recentBookings.length === 0 ? (
|
||||
<p className="rounded-2xl border border-dashed border-slate-200 p-8 text-center text-sm text-slate-500">
|
||||
<p className="rounded-2xl border border-dashed border-border p-8 text-center text-sm text-muted-foreground">
|
||||
You haven't booked any freight yet.
|
||||
</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[600px] whitespace-nowrap text-left text-sm">
|
||||
<thead className="text-xs text-slate-500">
|
||||
<thead className="text-xs text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-6 py-2 font-medium">Reference</th>
|
||||
<th className="py-2 font-medium">Route</th>
|
||||
<th className="py-2 font-medium">Cargo</th>
|
||||
<th className="py-2 font-medium">Date</th>
|
||||
<th className="py-2 font-medium">Status</th>
|
||||
<th className="px-6 py-2 text-right font-medium">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recentBookings.map((booking) => (
|
||||
<tr
|
||||
key={booking.id}
|
||||
className="border-t border-slate-100 transition hover:bg-primary/5"
|
||||
className="border-t border-border transition hover:bg-primary/5 cursor-pointer"
|
||||
onClick={() => navigate(`/bookings/${booking.id}`)}
|
||||
>
|
||||
<td className="px-6 py-3 font-medium text-slate-900">
|
||||
<td className="px-6 py-3 font-medium text-foreground">
|
||||
{booking.reference}
|
||||
</td>
|
||||
<td className="py-3 text-slate-700">
|
||||
{booking.originStation} → {booking.destinationStation}
|
||||
<td className="py-3 text-muted-foreground">
|
||||
{booking.originYard?.label ?? booking.originYard?.code ?? "—"} → {booking.destinationYard?.label ?? booking.destinationYard?.code ?? "—"}
|
||||
</td>
|
||||
<td className="py-3 text-slate-700">
|
||||
{booking.cargoType}
|
||||
<td className="py-3 text-muted-foreground">
|
||||
{booking.freightType === "CONTAINER" ? "Container" : booking.freightType}
|
||||
</td>
|
||||
<td className="py-3 text-muted-foreground">
|
||||
{format(new Date(booking.createdAt), "MMM d, yyyy HH:mm")}
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<BookingBadge status={booking.status} />
|
||||
</td>
|
||||
<td className="px-6 py-3 text-right">
|
||||
<Link
|
||||
to={`/bookings/${booking.id}`}
|
||||
aria-label="View booking"
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-lg text-slate-500 transition hover:bg-primary/10 hover:text-primary"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -289,7 +303,7 @@ export default function MyPortalPage() {
|
||||
|
||||
<CardContent>
|
||||
{recentInvoices.length === 0 ? (
|
||||
<p className="rounded-2xl border border-dashed border-slate-200 p-8 text-center text-sm text-slate-500">
|
||||
<p className="rounded-2xl border border-dashed border-border p-8 text-center text-sm text-muted-foreground">
|
||||
No invoices yet.
|
||||
</p>
|
||||
) : (
|
||||
@@ -297,16 +311,16 @@ export default function MyPortalPage() {
|
||||
{recentInvoices.map((invoice) => (
|
||||
<div
|
||||
key={invoice.id}
|
||||
className="rounded-2xl border border-slate-100 p-4 transition hover:border-primary/20 hover:bg-primary/5"
|
||||
className="rounded-2xl border border-border p-4 transition hover:border-primary/20 hover:bg-primary/5"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Receipt className="h-4 w-4 text-primary" />
|
||||
<InvoiceBadge status={invoice.status} />
|
||||
</div>
|
||||
<p className="mt-0.5 pt-2 text-lg font-bold text-slate-900">
|
||||
<p className="mt-0.5 pt-2 text-lg font-bold text-foreground">
|
||||
{formatCurrency(invoice.amount, invoice.currency)}
|
||||
</p>
|
||||
<p className="mt-1 flex items-center gap-1 text-xs text-slate-500">
|
||||
<p className="mt-1 flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
Due {invoice.dueDate}
|
||||
</p>
|
||||
@@ -334,8 +348,8 @@ function ProfileRow({
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 text-primary">{icon}</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-xs font-medium text-slate-500">{label}</p>
|
||||
<p className="mt-0.5 text-sm text-slate-900">{value}</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">{label}</p>
|
||||
<p className="mt-0.5 text-sm text-foreground">{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -343,9 +357,9 @@ function ProfileRow({
|
||||
|
||||
function ShipmentBadge({ status }: { status: ShipmentStatus }) {
|
||||
const styles: Record<ShipmentStatus, string> = {
|
||||
"In Transit": "bg-indigo-100 text-indigo-700",
|
||||
Delivered: "bg-emerald-100 text-emerald-700",
|
||||
Delayed: "bg-red-100 text-red-700",
|
||||
"In Transit": "bg-muted text-foreground",
|
||||
Delivered: "bg-primary/10 text-primary",
|
||||
Delayed: "bg-destructive/10 text-destructive",
|
||||
};
|
||||
return (
|
||||
<span
|
||||
@@ -356,29 +370,31 @@ function ShipmentBadge({ status }: { status: ShipmentStatus }) {
|
||||
);
|
||||
}
|
||||
|
||||
function BookingBadge({ status }: { status: BookingStatus }) {
|
||||
const styles: Record<BookingStatus, string> = {
|
||||
Pending: "bg-amber-100 text-amber-700",
|
||||
Confirmed: "bg-sky-100 text-sky-700",
|
||||
"In Transit": "bg-indigo-100 text-indigo-700",
|
||||
Delivered: "bg-emerald-100 text-emerald-700",
|
||||
Cancelled: "bg-red-100 text-red-700",
|
||||
function BookingBadge({ status }: { status: string }) {
|
||||
const styles: Record<string, string> = {
|
||||
DRAFT: "bg-amber-100 text-amber-700",
|
||||
SUBMITTED: "bg-primary/10 text-primary",
|
||||
PENDING_APPROVAL: "bg-muted text-foreground",
|
||||
IN_TRANSIT: "bg-muted text-foreground",
|
||||
COMPLETED: "bg-primary/10 text-primary",
|
||||
CANCELLED: "bg-destructive/10 text-destructive",
|
||||
REJECTED: "bg-destructive/10 text-destructive",
|
||||
};
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium ${styles[status]}`}
|
||||
className={`inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium ${styles[status] || "bg-muted text-muted-foreground"}`}
|
||||
>
|
||||
{status}
|
||||
{status.replace(/_/g, " ")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function InvoiceBadge({ status }: { status: InvoiceStatus }) {
|
||||
const styles: Record<InvoiceStatus, string> = {
|
||||
Draft: "bg-slate-100 text-slate-600",
|
||||
Sent: "bg-sky-100 text-sky-700",
|
||||
Paid: "bg-emerald-100 text-emerald-700",
|
||||
Overdue: "bg-red-100 text-red-700",
|
||||
Draft: "bg-muted text-muted-foreground",
|
||||
Sent: "bg-primary/10 text-primary",
|
||||
Paid: "bg-primary/10 text-primary",
|
||||
Overdue: "bg-destructive/10 text-destructive",
|
||||
Cancelled: "bg-amber-100 text-amber-700",
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
FileUp,
|
||||
} from "lucide-react";
|
||||
|
||||
import { format } from "date-fns";
|
||||
|
||||
import Breadcrumbs from "@/components/Breadcrumbs";
|
||||
import { api } from "@/services/api";
|
||||
import type { Freight } from "@edr/types";
|
||||
@@ -70,31 +72,31 @@ const STATUS_MAP: Record<
|
||||
DRAFT: {
|
||||
title: "Drafting Request",
|
||||
description: "Booking is being prepared and has not been submitted.",
|
||||
color: "text-slate-500",
|
||||
color: "text-muted-foreground",
|
||||
stage: 0,
|
||||
},
|
||||
CONFIRMED: {
|
||||
title: "Booking Confirmed",
|
||||
description: "Booking has been confirmed and approved.",
|
||||
color: "text-emerald-600",
|
||||
color: "text-primary",
|
||||
stage: 1,
|
||||
},
|
||||
IN_TRANSIT: {
|
||||
title: "Cargo Moving",
|
||||
description: "Shipment is currently moving through the rail network.",
|
||||
color: "text-sky-600",
|
||||
color: "text-primary",
|
||||
stage: 2,
|
||||
},
|
||||
DELIVERED: {
|
||||
title: "Service Complete",
|
||||
description: "Cargo delivered and service successfully terminated.",
|
||||
color: "text-emerald-600",
|
||||
color: "text-primary",
|
||||
stage: 3,
|
||||
},
|
||||
CANCELLED: {
|
||||
title: "Cancelled",
|
||||
description: "This booking process has been terminated.",
|
||||
color: "text-red-600",
|
||||
color: "text-destructive",
|
||||
stage: -1,
|
||||
},
|
||||
};
|
||||
@@ -145,10 +147,10 @@ export default function BookingDetailPage() {
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<Card className="flex flex-col items-center p-12 text-center">
|
||||
<div className="flex size-16 items-center justify-center rounded-full bg-red-50 text-red-400">
|
||||
<div className="flex size-16 items-center justify-center rounded-full bg-destructive/10 text-destructive">
|
||||
<AlertTriangle className="size-8" />
|
||||
</div>
|
||||
<h1 className="mt-4 text-2xl font-bold text-slate-900">
|
||||
<h1 className="mt-4 text-2xl font-bold text-foreground">
|
||||
Failed to load booking
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
@@ -165,10 +167,10 @@ export default function BookingDetailPage() {
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<Card className="flex flex-col items-center p-12 text-center">
|
||||
<div className="flex size-16 items-center justify-center rounded-full bg-slate-100 text-slate-400">
|
||||
<div className="flex size-16 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
<Package className="size-8" />
|
||||
</div>
|
||||
<h1 className="mt-4 text-2xl font-bold text-slate-900">
|
||||
<h1 className="mt-4 text-2xl font-bold text-foreground">
|
||||
Booking not found
|
||||
</h1>
|
||||
</Card>
|
||||
@@ -196,15 +198,22 @@ function DraftBookingView({
|
||||
const queryClient = useQueryClient();
|
||||
const { customer } = useAuth();
|
||||
const fileInputRefs = useRef<Record<string, HTMLInputElement | null>>({});
|
||||
const documentsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [selectedFiles, setSelectedFiles] = useState<
|
||||
Record<string, File | null>
|
||||
>({});
|
||||
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
|
||||
const [cancelReason, setCancelReason] = useState("");
|
||||
const [docError, setDocError] = useState("");
|
||||
|
||||
const anyFileSelected = Object.values(selectedFiles).some(Boolean);
|
||||
|
||||
const uploadedCodes = useMemo(
|
||||
() => new Set(booking.files?.map((f) => f.code) ?? []),
|
||||
[booking.files],
|
||||
);
|
||||
|
||||
const pricingQuery = useQuery(
|
||||
api.bookings.generatePrice.queryOptions({
|
||||
input: { id: booking.id },
|
||||
@@ -217,6 +226,7 @@ function DraftBookingView({
|
||||
api.bookings.uploadDocuments.call({ id: booking.id, files }),
|
||||
onSuccess: () => {
|
||||
setSelectedFiles({});
|
||||
setDocError("");
|
||||
onBookingUpdated();
|
||||
},
|
||||
});
|
||||
@@ -258,8 +268,17 @@ function DraftBookingView({
|
||||
cancelMutation.mutate(reason);
|
||||
}
|
||||
|
||||
const canConfirm =
|
||||
pricingQuery.isSuccess && !uploadMutation.isPending && !submitMutation.isPending;
|
||||
function handleSubmitRequest() {
|
||||
const missing = REQUIRED_DOC_FIELDS.filter(
|
||||
(doc) => !uploadedCodes.has(doc.key),
|
||||
);
|
||||
if (missing.length > 0) {
|
||||
setDocError("Please upload all required documents before submitting.");
|
||||
documentsRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
return;
|
||||
}
|
||||
submitMutation.mutate();
|
||||
}
|
||||
|
||||
const companyName = (customer as any)?.company?.name ?? "—";
|
||||
const companyTin = (customer as any)?.company?.tin ?? "—";
|
||||
@@ -282,7 +301,7 @@ function DraftBookingView({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex size-14 items-center justify-center rounded-2xl bg-slate-200 text-slate-600">
|
||||
<div className="flex size-14 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
|
||||
<Package className="size-6" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-1">
|
||||
@@ -296,16 +315,31 @@ function DraftBookingView({
|
||||
Complete the steps below to submit your booking request.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSubmitRequest}
|
||||
disabled={submitMutation.isPending}
|
||||
>
|
||||
{submitMutation.isPending ? (
|
||||
<LoaderCircle className="animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 />
|
||||
)}
|
||||
{submitMutation.isPending
|
||||
? "Submitting..."
|
||||
: "Confirm Booking Request"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{pricingQuery.isError && (
|
||||
<div className="flex items-start gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-red-500" />
|
||||
<div className="flex items-start gap-3 rounded-xl border border-destructive/20 bg-destructive/10 p-4 text-sm text-destructive">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
|
||||
<div>
|
||||
<p className="font-semibold">Pricing failed</p>
|
||||
<p className="mt-1 text-red-600">
|
||||
<p className="mt-1 text-destructive/80">
|
||||
{pricingQuery.error instanceof Error
|
||||
? pricingQuery.error.message
|
||||
: "An unexpected error occurred."}
|
||||
@@ -315,11 +349,11 @@ function DraftBookingView({
|
||||
)}
|
||||
|
||||
{uploadMutation.isError && (
|
||||
<div className="flex items-start gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-red-500" />
|
||||
<div className="flex items-start gap-3 rounded-xl border border-destructive/20 bg-destructive/10 p-4 text-sm text-destructive">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
|
||||
<div>
|
||||
<p className="font-semibold">Document upload failed</p>
|
||||
<p className="mt-1 text-red-600">
|
||||
<p className="mt-1 text-destructive/80">
|
||||
{uploadMutation.error instanceof Error
|
||||
? uploadMutation.error.message
|
||||
: "An unexpected error occurred."}
|
||||
@@ -329,11 +363,11 @@ function DraftBookingView({
|
||||
)}
|
||||
|
||||
{submitMutation.isError && (
|
||||
<div className="flex items-start gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-red-500" />
|
||||
<div className="flex items-start gap-3 rounded-xl border border-destructive/20 bg-destructive/10 p-4 text-sm text-destructive">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
|
||||
<div>
|
||||
<p className="font-semibold">Submission failed</p>
|
||||
<p className="mt-1 text-red-600">
|
||||
<p className="mt-1 text-destructive/80">
|
||||
{submitMutation.error instanceof Error
|
||||
? submitMutation.error.message
|
||||
: "An unexpected error occurred."}
|
||||
@@ -343,11 +377,11 @@ function DraftBookingView({
|
||||
)}
|
||||
|
||||
{cancelMutation.isError && (
|
||||
<div className="flex items-start gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-red-500" />
|
||||
<div className="flex items-start gap-3 rounded-xl border border-destructive/20 bg-destructive/10 p-4 text-sm text-destructive">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
|
||||
<div>
|
||||
<p className="font-semibold">Cancel failed</p>
|
||||
<p className="mt-1 text-red-600">
|
||||
<p className="mt-1 text-destructive/80">
|
||||
{cancelMutation.error instanceof Error
|
||||
? cancelMutation.error.message
|
||||
: "An unexpected error occurred."}
|
||||
@@ -358,7 +392,7 @@ function DraftBookingView({
|
||||
|
||||
<Card
|
||||
className={cn(
|
||||
pricingQuery.isSuccess ? "border-emerald-200 bg-emerald-50/30" : "",
|
||||
pricingQuery.isSuccess ? "border-primary/20 bg-primary/5" : "",
|
||||
)}
|
||||
>
|
||||
<CardHeader>
|
||||
@@ -441,22 +475,12 @@ function DraftBookingView({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-1">
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium text-primary underline-offset-2 hover:underline"
|
||||
onClick={() => pricingQuery.refetch()}
|
||||
>
|
||||
Re-calculate pricing
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card ref={documentsRef}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<FileText className="size-4 text-primary" />
|
||||
@@ -468,6 +492,12 @@ function DraftBookingView({
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
{docError && (
|
||||
<div className="flex items-start gap-3 rounded-xl border border-destructive/20 bg-destructive/10 p-4 text-sm text-destructive">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
|
||||
<p className="font-semibold">{docError}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-lg border border-primary/10 bg-primary/[0.02] p-4">
|
||||
<h3 className="mb-3 flex items-center gap-2 text-xs font-bold uppercase tracking-wide text-foreground">
|
||||
<Building2 className="size-3.5 text-primary" />
|
||||
@@ -479,7 +509,7 @@ function DraftBookingView({
|
||||
<InfoItem label="Contact Person" value={contactName} />
|
||||
<InfoItem label="Contact Email" value={contactEmail} />
|
||||
</div>
|
||||
<p className="mt-3 text-[10px] text-muted-foreground">
|
||||
<p className="mt-3 text-xs text-muted-foreground">
|
||||
To update your company information, go to{" "}
|
||||
<button
|
||||
type="button"
|
||||
@@ -500,56 +530,73 @@ function DraftBookingView({
|
||||
Upload Booking Documents
|
||||
</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
{REQUIRED_DOC_FIELDS.map((doc) => (
|
||||
<div
|
||||
key={doc.key}
|
||||
className="flex flex-col gap-1.5 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<label className="text-xs font-medium text-foreground">
|
||||
{doc.label}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
ref={(el) => {
|
||||
fileInputRefs.current[doc.key] = el;
|
||||
}}
|
||||
type="file"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
handleFileSelect(
|
||||
doc.key,
|
||||
e.target.files?.[0] ?? null,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors",
|
||||
selectedFiles[doc.key]
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-700"
|
||||
: "border-border bg-background text-muted-foreground hover:border-foreground/20 hover:text-foreground",
|
||||
{REQUIRED_DOC_FIELDS.map((doc) => {
|
||||
const isUploaded = uploadedCodes.has(doc.key);
|
||||
return (
|
||||
<div
|
||||
key={doc.key}
|
||||
className="flex flex-col gap-1.5 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<label className="flex items-center gap-2 text-xs font-medium text-foreground">
|
||||
{isUploaded && (
|
||||
<CheckCircle2 className="size-3.5 text-primary" />
|
||||
)}
|
||||
onClick={() => fileInputRefs.current[doc.key]?.click()}
|
||||
>
|
||||
<Upload className="size-3" />
|
||||
{selectedFiles[doc.key]
|
||||
? selectedFiles[doc.key]!.name
|
||||
: "Choose file"}
|
||||
</button>
|
||||
{selectedFiles[doc.key] && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-muted-foreground hover:text-red-500"
|
||||
onClick={() => handleFileSelect(doc.key, null)}
|
||||
>
|
||||
<XCircle className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
{doc.label}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
{isUploaded ? (
|
||||
<span className="inline-flex items-center gap-1 rounded-lg border border-primary/20 bg-primary/10 px-3 py-1.5 text-xs font-medium text-primary">
|
||||
<CheckCircle2 className="size-3" />
|
||||
Uploaded
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
ref={(el) => {
|
||||
fileInputRefs.current[doc.key] = el;
|
||||
}}
|
||||
type="file"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
handleFileSelect(
|
||||
doc.key,
|
||||
e.target.files?.[0] ?? null,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors",
|
||||
selectedFiles[doc.key]
|
||||
? "border-primary/20 bg-primary/10 text-primary"
|
||||
: "border-border bg-background text-muted-foreground hover:border-foreground/20 hover:text-foreground",
|
||||
)}
|
||||
onClick={() =>
|
||||
fileInputRefs.current[doc.key]?.click()
|
||||
}
|
||||
>
|
||||
<Upload className="size-3" />
|
||||
{selectedFiles[doc.key]
|
||||
? selectedFiles[doc.key]!.name
|
||||
: "Choose file"}
|
||||
</button>
|
||||
{selectedFiles[doc.key] && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
onClick={() => handleFileSelect(doc.key, null)}
|
||||
>
|
||||
<XCircle className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
@@ -570,7 +617,7 @@ function DraftBookingView({
|
||||
: "Select files to upload"}
|
||||
</Button>
|
||||
{uploadMutation.isSuccess && (
|
||||
<p className="mt-2 flex items-center gap-1.5 text-xs text-emerald-600">
|
||||
<p className="mt-2 flex items-center gap-1.5 text-xs text-primary">
|
||||
<CheckCircle2 className="size-3" />
|
||||
Documents uploaded successfully
|
||||
</p>
|
||||
@@ -597,7 +644,11 @@ function DraftBookingView({
|
||||
</p>
|
||||
<Dialog open={cancelDialogOpen} onOpenChange={setCancelDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="button" variant="destructive" className="self-start">
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
className="self-start"
|
||||
>
|
||||
<XCircle className="mr-1 h-4 w-4" />
|
||||
Cancel Booking
|
||||
</Button>
|
||||
@@ -647,41 +698,6 @@ function DraftBookingView({
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="sticky bottom-0 z-20 -mx-4 border-t border-border bg-background px-4 py-4">
|
||||
<div className="mx-auto flex max-w-5xl items-center justify-end gap-3">
|
||||
{!canConfirm && (
|
||||
<p className="mr-auto text-xs text-muted-foreground">
|
||||
{pricingQuery.isLoading
|
||||
? "Calculating price…"
|
||||
: pricingQuery.isError
|
||||
? "Price calculation failed."
|
||||
: "Upload documents before confirming."}
|
||||
</p>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => navigate("/bookings")}
|
||||
>
|
||||
Back to Bookings
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => submitMutation.mutate()}
|
||||
disabled={!canConfirm}
|
||||
>
|
||||
{submitMutation.isPending ? (
|
||||
<LoaderCircle className="mr-1 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="mr-1 h-4 w-4" />
|
||||
)}
|
||||
{submitMutation.isPending
|
||||
? "Submitting..."
|
||||
: "Confirm Booking Request"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -720,7 +736,10 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="size-3" />
|
||||
{booking.scheduledDate ?? booking.createdAt}
|
||||
{format(
|
||||
new Date(booking.scheduledDate ?? booking.createdAt),
|
||||
"MMM d, yyyy HH:mm",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -801,7 +820,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"text-[9px] font-bold uppercase tracking-widest",
|
||||
"text-xs font-bold uppercase tracking-widest",
|
||||
isActive ? "text-primary" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -815,7 +834,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
|
||||
<div className="flex flex-col gap-4 rounded-xl border border-border/50 bg-muted/20 p-5 md:flex-row md:items-center">
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-full bg-background shadow-sm">
|
||||
{normalizedStatus === "CANCELLED" ? (
|
||||
<AlertTriangle className="size-5 text-red-500" />
|
||||
<AlertTriangle className="size-5 text-destructive" />
|
||||
) : (
|
||||
<Info className="size-5 text-primary" />
|
||||
)}
|
||||
@@ -837,7 +856,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
|
||||
normalizedStatus !== "DELIVERED" && (
|
||||
<div className="ml-auto flex items-center gap-4 border-l border-border pl-6">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-[9px] font-bold uppercase text-muted-foreground">
|
||||
<p className="text-xs font-bold uppercase text-muted-foreground">
|
||||
Est. Waiting
|
||||
</p>
|
||||
<p className="text-xs font-black text-foreground">
|
||||
@@ -864,7 +883,11 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
|
||||
<div className="flex flex-col items-center justify-between gap-4 rounded-xl border bg-muted/30 p-4 md:flex-row">
|
||||
<RouteEndpoint
|
||||
label="Origin Yard"
|
||||
station={booking.originStation}
|
||||
station={
|
||||
booking.originYard?.label ??
|
||||
booking.originYard?.code ??
|
||||
"—"
|
||||
}
|
||||
icon={<MapPin />}
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-1 text-primary">
|
||||
@@ -874,14 +897,18 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-primary/20 bg-primary/5 text-[9px] font-bold uppercase"
|
||||
className="border-primary/20 bg-primary/5 text-xs font-bold uppercase"
|
||||
>
|
||||
Rail
|
||||
</Badge>
|
||||
</div>
|
||||
<RouteEndpoint
|
||||
label="Destination Yard"
|
||||
station={booking.destinationStation}
|
||||
station={
|
||||
booking.destinationYard?.label ??
|
||||
booking.destinationYard?.code ??
|
||||
"—"
|
||||
}
|
||||
icon={<MapPin />}
|
||||
/>
|
||||
</div>
|
||||
@@ -1038,10 +1065,10 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
|
||||
<InfoItem label="Customer ID" value={booking.customerId} />
|
||||
<Separator />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline" className="bg-background text-[9px]">
|
||||
<Badge variant="outline" className="bg-background text-xs">
|
||||
Hazardous: {booking.isHazardous ? "Yes" : "No"}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-background text-[9px]">
|
||||
<Badge variant="outline" className="bg-background text-xs">
|
||||
Refrigerated: {booking.isRefrigerated ? "Yes" : "No"}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -1055,7 +1082,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{booking.freightSubtype && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-[9px] font-bold uppercase tracking-tight text-muted-foreground">
|
||||
<p className="text-xs font-bold uppercase tracking-tight text-muted-foreground">
|
||||
Cargo Description
|
||||
</p>
|
||||
<p className="text-xs text-foreground leading-relaxed italic">
|
||||
@@ -1067,7 +1094,7 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
|
||||
<>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-[9px] font-bold uppercase tracking-tight text-muted-foreground">
|
||||
<p className="text-xs font-bold uppercase tracking-tight text-muted-foreground">
|
||||
Financial Terms
|
||||
</p>
|
||||
<div className="rounded-lg bg-amber-50/50 border border-amber-100 p-2">
|
||||
@@ -1108,7 +1135,7 @@ function RouteEndpoint({
|
||||
{icon && <div className="[&_svg]:size-5">{icon}</div>}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-[9px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||
{label}
|
||||
</p>
|
||||
<p className="text-sm font-black text-foreground">{station}</p>
|
||||
@@ -1134,7 +1161,7 @@ function InfoItem({
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<p className="text-[9px] font-bold uppercase tracking-tight text-muted-foreground">
|
||||
<p className="text-xs font-bold uppercase tracking-tight text-muted-foreground">
|
||||
{label}
|
||||
</p>
|
||||
<p className="text-xs font-bold text-foreground">{value ?? "—"}</p>
|
||||
@@ -1145,18 +1172,18 @@ function InfoItem({
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const statusColors: Record<string, string> = {
|
||||
DRAFT: "bg-slate-50 text-slate-700 border-slate-200",
|
||||
CONFIRMED: "bg-emerald-50 text-emerald-700 border-emerald-200",
|
||||
IN_TRANSIT: "bg-sky-50 text-sky-700 border-sky-200",
|
||||
DELIVERED: "bg-indigo-50 text-indigo-700 border-indigo-200",
|
||||
CANCELLED: "bg-red-50 text-red-700 border-red-200",
|
||||
DRAFT: "bg-muted text-muted-foreground border-border",
|
||||
CONFIRMED: "bg-primary/10 text-primary border-primary/20",
|
||||
IN_TRANSIT: "bg-primary/10 text-primary border-primary/20",
|
||||
DELIVERED: "bg-muted text-foreground border-border",
|
||||
CANCELLED: "bg-destructive/10 text-destructive border-destructive/20",
|
||||
};
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"px-2 py-0.5 font-bold uppercase tracking-wider text-[9px]",
|
||||
"px-2 py-0.5 font-bold uppercase tracking-wider text-xs",
|
||||
statusColors[status] || "bg-muted",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -49,8 +49,8 @@ export default function MyBookings() {
|
||||
const term = searchTerm.toLowerCase();
|
||||
return (
|
||||
b.reference.toLowerCase().includes(term) ||
|
||||
b.originStation.toLowerCase().includes(term) ||
|
||||
b.destinationStation.toLowerCase().includes(term) ||
|
||||
(b.originYard?.label ?? b.originYard?.code ?? "").toLowerCase().includes(term) ||
|
||||
(b.destinationYard?.label ?? b.destinationYard?.code ?? "").toLowerCase().includes(term) ||
|
||||
b.status.toLowerCase().includes(term)
|
||||
);
|
||||
});
|
||||
@@ -85,8 +85,8 @@ export default function MyBookings() {
|
||||
<Package className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-slate-900">{booking.reference}</p>
|
||||
<p className="text-sm text-slate-500">{booking.scheduledDate ?? booking.createdAt}</p>
|
||||
<p className="font-medium text-foreground">{booking.reference}</p>
|
||||
<p className="text-sm text-muted-foreground">{booking.scheduledDate ?? booking.createdAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -96,10 +96,10 @@ export default function MyBookings() {
|
||||
id: "route",
|
||||
header: "Route",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2 text-sm text-slate-700">
|
||||
<span>{row.original.originStation}</span>
|
||||
<ArrowRight className="text-slate-400" />
|
||||
<span>{row.original.destinationStation}</span>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>{row.original.originYard?.label ?? row.original.originYard?.code ?? "—"}</span>
|
||||
<ArrowRight className="text-muted-foreground" />
|
||||
<span>{row.original.destinationYard?.label ?? row.original.destinationYard?.code ?? "—"}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -111,9 +111,9 @@ export default function MyBookings() {
|
||||
const containerCount = b.containers?.reduce((sum, c) => sum + c.qty, 0) ?? 0;
|
||||
const containerType = b.containers?.[0]?.type ?? null;
|
||||
return (
|
||||
<div className="text-sm text-slate-700">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p>{b.freightType === "BULK" ? "Bulk" : "Break Bulk"}</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{containerType && containerCount > 0 ? `${containerCount} × ${containerType} · ` : ""}{b.cargoTotalWeightVgm}t
|
||||
</p>
|
||||
</div>
|
||||
@@ -124,7 +124,7 @@ export default function MyBookings() {
|
||||
id: "transportMode",
|
||||
header: "Transport",
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-slate-700">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{row.original.serviceType === "RAIL_AND_FORWARDING" ? "Rail & Forwarding" : "Rail"}
|
||||
</span>
|
||||
),
|
||||
@@ -172,10 +172,10 @@ export default function MyBookings() {
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6 flex-row justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-slate-900">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-foreground">
|
||||
My Bookings
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-secondary-foreground">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
View and manage your freight booking requests.
|
||||
</p>
|
||||
</div>
|
||||
@@ -205,8 +205,8 @@ export default function MyBookings() {
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">Total Bookings</p>
|
||||
<h3 className="mt-2 text-3xl font-bold text-slate-900">
|
||||
<p className="text-sm text-muted-foreground">Total Bookings</p>
|
||||
<h3 className="mt-2 text-3xl font-bold text-foreground">
|
||||
{bookings.length}
|
||||
</h3>
|
||||
</div>
|
||||
@@ -219,8 +219,8 @@ export default function MyBookings() {
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">Active Bookings</p>
|
||||
<h3 className="mt-2 text-3xl font-bold text-slate-900">
|
||||
<p className="text-sm text-muted-foreground">Active Bookings</p>
|
||||
<h3 className="mt-2 text-3xl font-bold text-foreground">
|
||||
{activeCount}
|
||||
</h3>
|
||||
</div>
|
||||
@@ -233,8 +233,8 @@ export default function MyBookings() {
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">Pending Approval</p>
|
||||
<h3 className="mt-2 text-3xl font-bold text-slate-900">
|
||||
<p className="text-sm text-muted-foreground">Pending Approval</p>
|
||||
<h3 className="mt-2 text-3xl font-bold text-foreground">
|
||||
{pendingCount}
|
||||
</h3>
|
||||
</div>
|
||||
@@ -263,9 +263,9 @@ export default function MyBookings() {
|
||||
<CardContent className="px-0">
|
||||
{total === 0 && dataTableStatus === "success" ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 px-6 text-center">
|
||||
<Package className="h-12 w-12 text-slate-300 mb-4" />
|
||||
<h3 className="text-sm font-semibold text-slate-900">No bookings found</h3>
|
||||
<p className="text-xs text-slate-500 mt-1 max-w-sm">
|
||||
<Package className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-sm font-semibold text-foreground">No bookings found</h3>
|
||||
<p className="text-xs text-muted-foreground mt-1 max-w-sm">
|
||||
{searchTerm ? "No bookings match your current search filter." : "You haven't requested any bookings yet."}
|
||||
</p>
|
||||
</div>
|
||||
@@ -299,15 +299,15 @@ export default function MyBookings() {
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const styles: Record<string, string> = {
|
||||
DRAFT: "bg-amber-100 text-amber-700",
|
||||
CONFIRMED: "bg-sky-100 text-sky-700",
|
||||
IN_TRANSIT: "bg-indigo-100 text-indigo-700",
|
||||
DELIVERED: "bg-emerald-100 text-emerald-700",
|
||||
CANCELLED: "bg-red-100 text-red-700",
|
||||
CONFIRMED: "bg-primary/10 text-primary",
|
||||
IN_TRANSIT: "bg-muted text-foreground",
|
||||
DELIVERED: "bg-primary/10 text-primary",
|
||||
CANCELLED: "bg-destructive/10 text-destructive",
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex rounded-full px-3 py-1 text-xs font-medium ${styles[status] ?? "bg-slate-100 text-slate-700"}`}
|
||||
className={`inline-flex rounded-full px-3 py-1 text-xs font-medium ${styles[status] ?? "bg-muted text-muted-foreground"}`}
|
||||
>
|
||||
{status.replace(/_/g, ' ')}
|
||||
</span>
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
} from "@/types/fileUploadSettings";
|
||||
import {
|
||||
bookingsService,
|
||||
BookingListFilter,
|
||||
CreateBookingPayload,
|
||||
GeneratePriceResponse,
|
||||
} from "./bookings.service";
|
||||
@@ -115,7 +116,7 @@ export const api = {
|
||||
},
|
||||
|
||||
bookings: {
|
||||
list: endpoint<void, PaginatedResponse<Freight.IBooking>>(
|
||||
list: endpoint<BookingListFilter | void, PaginatedResponse<Freight.IBooking>>(
|
||||
"bookings",
|
||||
"list",
|
||||
bookingsService.list,
|
||||
|
||||
@@ -47,9 +47,19 @@ export interface SignContractPayload {
|
||||
consentText?: string;
|
||||
}
|
||||
|
||||
export interface BookingListFilter {
|
||||
status?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: "ASC" | "DESC";
|
||||
}
|
||||
|
||||
export const bookingsService = {
|
||||
list: async (): Promise<PaginatedResponse<Freight.IBooking>> => {
|
||||
const { data } = await client.get("/api/bookings");
|
||||
list: async (
|
||||
filter: BookingListFilter | void = {},
|
||||
): Promise<PaginatedResponse<Freight.IBooking>> => {
|
||||
const { data } = await client.get("/api/bookings", { params: filter });
|
||||
return data.data;
|
||||
},
|
||||
get: async (id: string): Promise<Freight.IBooking> => {
|
||||
|
||||
@@ -158,6 +158,14 @@ export interface IConsignment extends BaseEntity {
|
||||
destinationStation: string;
|
||||
}
|
||||
|
||||
export interface IYard extends BaseEntity {
|
||||
code: string;
|
||||
label: string;
|
||||
country: string;
|
||||
isActive: boolean;
|
||||
displayOrder: number;
|
||||
}
|
||||
|
||||
export interface IBooking extends BaseEntity {
|
||||
reference: string;
|
||||
customerId: string;
|
||||
@@ -177,8 +185,8 @@ export interface IBooking extends BaseEntity {
|
||||
lastMileDeliveryAddress?: string | null;
|
||||
|
||||
equipmentReturn: "WITH_RETURN" | "WITHOUT_RETURN";
|
||||
originStation: string;
|
||||
destinationStation: string;
|
||||
originYard?: IYard | null;
|
||||
destinationYard?: IYard | null;
|
||||
cargoTotalWeightVgm: number;
|
||||
|
||||
freightType: FreightType;
|
||||
@@ -208,7 +216,17 @@ export interface IBooking extends BaseEntity {
|
||||
signedByCeoId?: string | null;
|
||||
signedByCeoAt?: string | null;
|
||||
|
||||
files?: Array<{ id: string; name: string; url: string; mimeType: string }>;
|
||||
files?: Array<{
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
url: string;
|
||||
mimeType: string;
|
||||
size: number;
|
||||
resourceId: string;
|
||||
resource: string;
|
||||
signedUrl?: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface IInvoice extends BaseEntity {
|
||||
|
||||
Reference in New Issue
Block a user