feat(bookings): Integrate auto-pricing into DRAFT booking UI, and refactor customer-company relationships across services.

This commit is contained in:
ghost2023
2026-06-05 10:23:08 +03:00
parent 0184db9040
commit a991139cc2
7 changed files with 1683 additions and 521 deletions

View File

@@ -16,7 +16,6 @@ import {
ShieldCheck,
AlertTriangle,
Info,
Clock,
Layers,
CheckCircle2,
History,
@@ -36,7 +35,6 @@ import {
import Breadcrumbs from "@/components/Breadcrumbs";
import { api } from "@/services/api";
import type { Freight } from "@edr/types";
import type { GeneratePriceResponse } from "@/services/bookings.service";
import {
Card,
CardHeader,
@@ -46,6 +44,14 @@ import {
Badge,
Button,
Separator,
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
} from "@edr/ui-common";
import { cn } from "@/lib/utils";
import useAuth from "@/hooks/useAuth";
@@ -57,12 +63,40 @@ const PROGRESS_STAGES = [
{ label: "Complete", icon: PackageCheck, statuses: ["DELIVERED"] },
];
const STATUS_MAP: Record<string, { title: string; description: string; color: string; stage: number }> = {
DRAFT: { title: "Drafting Request", description: "Booking is being prepared and has not been submitted.", color: "text-slate-500", stage: 0 },
CONFIRMED: { title: "Booking Confirmed", description: "Booking has been confirmed and approved.", color: "text-emerald-600", stage: 1 },
IN_TRANSIT: { title: "Cargo Moving", description: "Shipment is currently moving through the rail network.", color: "text-sky-600", stage: 2 },
DELIVERED: { title: "Service Complete", description: "Cargo delivered and service successfully terminated.", color: "text-emerald-600", stage: 3 },
CANCELLED: { title: "Cancelled", description: "This booking process has been terminated.", color: "text-red-600", stage: -1 },
const STATUS_MAP: Record<
string,
{ title: string; description: string; color: string; stage: number }
> = {
DRAFT: {
title: "Drafting Request",
description: "Booking is being prepared and has not been submitted.",
color: "text-slate-500",
stage: 0,
},
CONFIRMED: {
title: "Booking Confirmed",
description: "Booking has been confirmed and approved.",
color: "text-emerald-600",
stage: 1,
},
IN_TRANSIT: {
title: "Cargo Moving",
description: "Shipment is currently moving through the rail network.",
color: "text-sky-600",
stage: 2,
},
DELIVERED: {
title: "Service Complete",
description: "Cargo delivered and service successfully terminated.",
color: "text-emerald-600",
stage: 3,
},
CANCELLED: {
title: "Cancelled",
description: "This booking process has been terminated.",
color: "text-red-600",
stage: -1,
},
};
const REQUIRED_DOC_FIELDS = [
@@ -74,10 +108,14 @@ const REQUIRED_DOC_FIELDS = [
export default function BookingDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { data: booking, isLoading, isError, error } = useQuery(
const {
data: booking,
isLoading,
isError,
error,
} = useQuery(
api.bookings.get.queryOptions({
input: { id: id! },
enabled: !!id,
@@ -85,7 +123,9 @@ export default function BookingDetailPage() {
);
const refetchBooking = () => {
queryClient.invalidateQueries({ queryKey: api.bookings.get.queryKey({ id: id! }) });
queryClient.invalidateQueries({
queryKey: api.bookings.get.queryKey({ id: id! }),
});
};
if (isLoading) {
@@ -93,7 +133,9 @@ export default function BookingDetailPage() {
<div className="container mx-auto flex items-center justify-center p-12">
<div className="flex flex-col items-center gap-4">
<LoaderCircle className="size-8 animate-spin text-primary" />
<p className="text-sm text-muted-foreground">Loading booking details</p>
<p className="text-sm text-muted-foreground">
Loading booking details
</p>
</div>
</div>
);
@@ -110,7 +152,9 @@ export default function BookingDetailPage() {
Failed to load booking
</h1>
<p className="mt-2 text-sm text-muted-foreground">
{error instanceof Error ? error.message : "An unexpected error occurred."}
{error instanceof Error
? error.message
: "An unexpected error occurred."}
</p>
</Card>
</div>
@@ -133,7 +177,9 @@ export default function BookingDetailPage() {
}
if (booking.status === "DRAFT") {
return <DraftBookingView booking={booking} onBookingUpdated={refetchBooking} />;
return (
<DraftBookingView booking={booking} onBookingUpdated={refetchBooking} />
);
}
return <ReadonlyBookingView booking={booking} />;
@@ -151,20 +197,20 @@ function DraftBookingView({
const { customer } = useAuth();
const fileInputRefs = useRef<Record<string, HTMLInputElement | null>>({});
const [pricingData, setPricingData] = useState<GeneratePriceResponse | null>(null);
const [selectedFiles, setSelectedFiles] = useState<Record<string, File | null>>({});
const [selectedFiles, setSelectedFiles] = useState<
Record<string, File | null>
>({});
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
const [cancelReason, setCancelReason] = useState("");
const anyFileSelected = Object.values(selectedFiles).some(Boolean);
const allDocsProvided = !anyFileSelected;
const priceMutation = useMutation({
mutationFn: () => api.bookings.generatePrice.call({ id: booking.id }),
onSuccess: (data) => {
setPricingData(data);
queryClient.invalidateQueries({ queryKey: api.bookings.get.queryKey({ id: booking.id }) });
},
});
const pricingQuery = useQuery(
api.bookings.generatePrice.queryOptions({
input: { id: booking.id },
enabled: !!booking.id,
}),
);
const uploadMutation = useMutation({
mutationFn: (files: Record<string, File | File[] | null>) =>
@@ -187,6 +233,7 @@ function DraftBookingView({
mutationFn: (reason: string) =>
api.bookings.cancel.call({ id: booking.id, reason }),
onSuccess: () => {
setCancelDialogOpen(false);
onBookingUpdated();
},
});
@@ -211,17 +258,19 @@ function DraftBookingView({
cancelMutation.mutate(reason);
}
const canConfirm = !!pricingData && !uploadMutation.isPending && !submitMutation.isPending;
const canConfirm =
pricingQuery.isSuccess && !uploadMutation.isPending && !submitMutation.isPending;
const companyName = (customer as any)?.company?.name ?? "—";
const companyTin = (customer as any)?.company?.tin ?? "—";
const contactName = (customer as any)?.profile
? `${(customer as any).profile.firstName ?? ""} ${(customer as any).profile.lastName ?? ""}`.trim() || "—"
? `${(customer as any).profile.firstName ?? ""} ${(customer as any).profile.lastName ?? ""}`.trim() ||
"—"
: "—";
const contactEmail = (customer as any)?.profile?.email ?? "—";
return (
<div className="container mx-auto max-w-5xl px-4 py-8">
<div className="container mx-auto px-4 py-8">
<div className="flex flex-col gap-6">
<Breadcrumbs
items={[
@@ -251,14 +300,14 @@ function DraftBookingView({
</CardHeader>
</Card>
{priceMutation.isError && (
{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>
<p className="font-semibold">Pricing failed</p>
<p className="mt-1 text-red-600">
{priceMutation.error instanceof Error
? priceMutation.error.message
{pricingQuery.error instanceof Error
? pricingQuery.error.message
: "An unexpected error occurred."}
</p>
</div>
@@ -307,50 +356,85 @@ function DraftBookingView({
</div>
)}
<Card className={cn(pricingData ? "border-emerald-200 bg-emerald-50/30" : "")}>
<Card
className={cn(
pricingQuery.isSuccess ? "border-emerald-200 bg-emerald-50/30" : "",
)}
>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<DollarSign className="size-4 text-primary" />
Pricing Estimation
</CardTitle>
<CardDescription>
Generate a price estimate based on your booking details.
Price estimate based on your booking details.
</CardDescription>
</CardHeader>
<CardContent>
{pricingData ? (
{pricingQuery.isLoading ? (
<div className="flex flex-col items-center gap-3 py-6 text-center">
<LoaderCircle className="size-6 animate-spin text-primary" />
<p className="text-xs text-muted-foreground">
Calculating price
</p>
</div>
) : pricingQuery.isError ? (
<div className="flex flex-col items-center gap-3 py-6 text-center">
<p className="text-xs text-muted-foreground">
Could not calculate price.
</p>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => pricingQuery.refetch()}
>
Retry
</Button>
</div>
) : pricingQuery.data ? (
<div className="flex flex-col gap-4">
<div className="overflow-hidden rounded-lg border">
<table className="w-full text-left text-xs">
<thead className="bg-muted text-muted-foreground">
<tr>
<th className="px-4 py-2 font-semibold">Description</th>
<th className="px-4 py-2 font-semibold text-right">Amount</th>
<th className="px-4 py-2 font-semibold text-right">
Amount
</th>
</tr>
</thead>
<tbody className="divide-y">
{pricingData.lineItems.map((item, i) => (
{pricingQuery.data.lineItems.map((item, i) => (
<tr key={i}>
<td className="px-4 py-2 text-foreground">{item.description}</td>
<td className="px-4 py-2 text-foreground">
{item.description}
</td>
<td className="px-4 py-2 text-right font-medium text-foreground">
{item.amount.toLocaleString()} {item.currency}
</td>
</tr>
))}
<tr className="bg-primary/5 font-bold">
<td className="px-4 py-2 text-foreground">Total Estimated Cost</td>
<td className="px-4 py-2 text-foreground">
Total Estimated Cost
</td>
<td className="px-4 py-2 text-right text-foreground">
{pricingData.totalAmount.toLocaleString()} {pricingData.currency}
{pricingQuery.data.totalAmount.toLocaleString()}{" "}
{pricingQuery.data.currency}
</td>
</tr>
</tbody>
</table>
</div>
{pricingData.warnings.length > 0 && (
{pricingQuery.data.warnings.length > 0 && (
<div className="flex flex-col gap-2 rounded-lg border border-amber-200 bg-amber-50 p-3">
{pricingData.warnings.map((w, i) => (
<p key={i} className="flex items-start gap-2 text-xs text-amber-800">
{pricingQuery.data.warnings.map((w, i) => (
<p
key={i}
className="flex items-start gap-2 text-xs text-amber-800"
>
<AlertTriangle className="mt-0.5 size-3 shrink-0" />
{w}
</p>
@@ -362,40 +446,13 @@ function DraftBookingView({
<button
type="button"
className="text-xs font-medium text-primary underline-offset-2 hover:underline"
onClick={() => {
setPricingData(null);
priceMutation.mutate();
}}
onClick={() => pricingQuery.refetch()}
>
Re-calculate pricing
</button>
</div>
</div>
) : (
<div className="flex flex-col items-center gap-4 py-6 text-center">
<div className="flex size-12 items-center justify-center rounded-full bg-muted">
<DollarSign className="size-6 text-muted-foreground" />
</div>
<div>
<p className="text-sm font-medium text-foreground">No pricing yet</p>
<p className="mt-1 text-xs text-muted-foreground">
Generate a price estimate to review before submitting.
</p>
</div>
<Button
type="button"
onClick={() => priceMutation.mutate()}
disabled={priceMutation.isPending}
>
{priceMutation.isPending ? (
<LoaderCircle className="mr-1 h-4 w-4 animate-spin" />
) : (
<DollarSign className="mr-1 h-4 w-4" />
)}
{priceMutation.isPending ? "Calculating..." : "Request Pricing Estimation"}
</Button>
</div>
)}
) : null}
</CardContent>
</Card>
@@ -406,8 +463,8 @@ function DraftBookingView({
Required Documents
</CardTitle>
<CardDescription>
Provide the necessary documents for this booking. Some information is
pre-filled from your company profile.
Provide the necessary documents for this booking. Some information
is pre-filled from your company profile.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-6">
@@ -460,7 +517,10 @@ function DraftBookingView({
accept=".pdf,.jpg,.jpeg,.png"
className="hidden"
onChange={(e) => {
handleFileSelect(doc.key, e.target.files?.[0] ?? null);
handleFileSelect(
doc.key,
e.target.files?.[0] ?? null,
);
}}
/>
<button
@@ -474,7 +534,9 @@ function DraftBookingView({
onClick={() => fileInputRefs.current[doc.key]?.click()}
>
<Upload className="size-3" />
{selectedFiles[doc.key] ? selectedFiles[doc.key]!.name : "Choose file"}
{selectedFiles[doc.key]
? selectedFiles[doc.key]!.name
: "Choose file"}
</button>
{selectedFiles[doc.key] && (
<button
@@ -528,27 +590,61 @@ function DraftBookingView({
If you no longer need this booking, you can cancel it.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-3 sm:flex-row sm:items-center">
<input
type="text"
placeholder="Reason for cancellation (optional)"
className="flex-1 rounded-lg border border-border bg-background px-3 py-2 text-xs text-foreground outline-none focus:border-destructive/50"
value={cancelReason}
onChange={(e) => setCancelReason(e.target.value)}
/>
<Button
type="button"
variant="destructive"
onClick={handleCancel}
disabled={cancelMutation.isPending}
>
{cancelMutation.isPending ? (
<LoaderCircle className="mr-1 h-4 w-4 animate-spin" />
) : (
<XCircle className="mr-1 h-4 w-4" />
)}
Cancel Booking
</Button>
<CardContent className="flex flex-col gap-3">
<p className="text-xs text-muted-foreground">
Cancelling will terminate this booking request and cannot be
undone.
</p>
<Dialog open={cancelDialogOpen} onOpenChange={setCancelDialogOpen}>
<DialogTrigger asChild>
<Button type="button" variant="destructive" className="self-start">
<XCircle className="mr-1 h-4 w-4" />
Cancel Booking
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Cancel Booking</DialogTitle>
<DialogDescription>
Are you sure you want to cancel this booking? This action
cannot be undone.
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3 py-2">
<label className="text-xs font-medium text-foreground">
Reason for cancellation
</label>
<input
type="text"
placeholder="Optional — provide a reason"
className="rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus:border-destructive/50"
value={cancelReason}
onChange={(e) => setCancelReason(e.target.value)}
autoFocus
/>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">
Keep Booking
</Button>
</DialogClose>
<Button
type="button"
variant="destructive"
onClick={handleCancel}
disabled={cancelMutation.isPending}
>
{cancelMutation.isPending ? (
<LoaderCircle className="mr-1 h-4 w-4 animate-spin" />
) : (
<XCircle className="mr-1 h-4 w-4" />
)}
Yes, Cancel Booking
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
@@ -556,9 +652,11 @@ function DraftBookingView({
<div className="mx-auto flex max-w-5xl items-center justify-end gap-3">
{!canConfirm && (
<p className="mr-auto text-xs text-muted-foreground">
{!pricingData
? "Request pricing estimation before confirming."
: "Upload documents before confirming."}
{pricingQuery.isLoading
? "Calculating price…"
: pricingQuery.isError
? "Price calculation failed."
: "Upload documents before confirming."}
</p>
)}
<Button
@@ -599,7 +697,6 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
return (
<div className="container mx-auto max-w-7xl px-4 py-8">
<div className="flex flex-col gap-8">
<Breadcrumbs
items={[
{ label: "Bookings", href: "/bookings" },
@@ -631,26 +728,27 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
</CardHeader>
</Card>
{(booking.status === "CONFIRMED" || booking.status === "IN_TRANSIT") && (
<Card className="border-primary/30 bg-primary/5">
<CardContent className="flex flex-col gap-4 p-6 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="font-semibold text-foreground">Contract ready</p>
<p className="text-sm text-muted-foreground">
Review the agreement and apply your digital signature.
</p>
</div>
<button
type="button"
className="inline-flex items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
onClick={() => navigate(`/bookings/${booking.id}/contract`)}
>
<FileSignature className="size-4" />
View &amp; sign contract
</button>
</CardContent>
</Card>
)}
{(booking.status === "CONFIRMED" ||
booking.status === "IN_TRANSIT") && (
<Card className="border-primary/30 bg-primary/5">
<CardContent className="flex flex-col gap-4 p-6 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="font-semibold text-foreground">Contract ready</p>
<p className="text-sm text-muted-foreground">
Review the agreement and apply your digital signature.
</p>
</div>
<button
type="button"
className="inline-flex items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
onClick={() => navigate(`/bookings/${booking.id}/contract`)}
>
<FileSignature className="size-4" />
View &amp; sign contract
</button>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
@@ -658,14 +756,21 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
<History className="size-4 text-primary" />
Booking Status Lifecycle
</CardTitle>
<CardDescription>Track the journey from request to completion</CardDescription>
<CardDescription>
Track the journey from request to completion
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-8">
<div className="relative flex w-full justify-between px-2">
<div className="absolute top-4 left-0 h-0.5 w-full bg-muted">
<div
className="h-full bg-primary transition-all duration-500"
style={{ width: currentStageIndex >= 0 ? `${(currentStageIndex / (PROGRESS_STAGES.length - 1)) * 100}%` : '0%' }}
style={{
width:
currentStageIndex >= 0
? `${(currentStageIndex / (PROGRESS_STAGES.length - 1)) * 100}%`
: "0%",
}}
/>
</div>
@@ -674,19 +779,32 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
const isActive = idx === currentStageIndex;
return (
<div key={stage.label} className="relative z-10 flex flex-col items-center gap-2">
<div className={cn(
"flex size-8 items-center justify-center rounded-full border-2 transition-all duration-300 bg-background",
isCompleted ? "border-primary text-primary" :
isActive ? "border-primary text-primary shadow-[0_0_10px_rgba(16,185,129,0.3)] scale-110" :
"border-muted text-muted-foreground"
)}>
{isCompleted ? <CheckCircle2 className="size-4" /> : <stage.icon className="size-4" />}
<div
key={stage.label}
className="relative z-10 flex flex-col items-center gap-2"
>
<div
className={cn(
"flex size-8 items-center justify-center rounded-full border-2 transition-all duration-300 bg-background",
isCompleted
? "border-primary text-primary"
: isActive
? "border-primary text-primary shadow-[0_0_10px_rgba(16,185,129,0.3)] scale-110"
: "border-muted text-muted-foreground",
)}
>
{isCompleted ? (
<CheckCircle2 className="size-4" />
) : (
<stage.icon className="size-4" />
)}
</div>
<span className={cn(
"text-[9px] font-bold uppercase tracking-widest",
isActive ? "text-primary" : "text-muted-foreground"
)}>
<span
className={cn(
"text-[9px] font-bold uppercase tracking-widest",
isActive ? "text-primary" : "text-muted-foreground",
)}
>
{stage.label}
</span>
</div>
@@ -695,26 +813,40 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
</div>
<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" /> : <Info className="size-5 text-primary" />}
</div>
<div className="flex flex-col gap-0.5">
<h4 className={cn("text-sm font-black uppercase tracking-tight", statusConfig.color)}>
{statusConfig.title}
</h4>
<p className="text-xs font-medium text-muted-foreground">
{statusConfig.description}
</p>
</div>
{normalizedStatus !== "CANCELLED" && normalizedStatus !== "DELIVERED" && (
<div className="ml-auto flex items-center gap-4 border-l border-border pl-6">
<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" />
) : (
<Info className="size-5 text-primary" />
)}
</div>
<div className="flex flex-col gap-0.5">
<h4
className={cn(
"text-sm font-black uppercase tracking-tight",
statusConfig.color,
)}
>
{statusConfig.title}
</h4>
<p className="text-xs font-medium text-muted-foreground">
{statusConfig.description}
</p>
</div>
{normalizedStatus !== "CANCELLED" &&
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">Est. Waiting</p>
<p className="text-xs font-black text-foreground">1-2 Working Days</p>
<p className="text-[9px] font-bold uppercase text-muted-foreground">
Est. Waiting
</p>
<p className="text-xs font-black text-foreground">
1-2 Working Days
</p>
</div>
<CreditCard className="size-5 text-muted-foreground" />
</div>
)}
</div>
)}
</div>
</CardContent>
</Card>
@@ -740,7 +872,10 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
<Train className="size-5" />
<ArrowRight className="size-4" />
</div>
<Badge variant="outline" className="border-primary/20 bg-primary/5 text-[9px] font-bold uppercase">
<Badge
variant="outline"
className="border-primary/20 bg-primary/5 text-[9px] font-bold uppercase"
>
Rail
</Badge>
</div>
@@ -752,9 +887,31 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<InfoItem icon={<Layers />} label="Service" value={booking.serviceType === "RAIL_AND_FORWARDING" ? "Rail & Forwarding" : "Rail Only"} />
<InfoItem icon={<ShieldCheck />} label="Return" value={booking.equipmentReturn === "WITH_RETURN" ? "With Return" : "Without Return"} />
<InfoItem icon={<FileText />} label="Trade" value={booking.tradeDirection === "IMPORT" ? "Import" : "Export"} />
<InfoItem
icon={<Layers />}
label="Service"
value={
booking.serviceType === "RAIL_AND_FORWARDING"
? "Rail & Forwarding"
: "Rail Only"
}
/>
<InfoItem
icon={<ShieldCheck />}
label="Return"
value={
booking.equipmentReturn === "WITH_RETURN"
? "With Return"
: "Without Return"
}
/>
<InfoItem
icon={<FileText />}
label="Trade"
value={
booking.tradeDirection === "IMPORT" ? "Import" : "Export"
}
/>
</div>
</CardContent>
</Card>
@@ -771,14 +928,23 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
<h3 className="border-l-4 border-primary pl-3 text-xs font-bold uppercase tracking-wide text-foreground">
First Mile
</h3>
<InfoItem label="Address" value={booking.firstMileEnabled && booking.firstMilePickupAddress ? booking.firstMilePickupAddress : "Not requested"} />
<InfoItem
label="Address"
value={
booking.firstMileEnabled && booking.firstMilePickupAddress
? booking.firstMilePickupAddress
: "Not requested"
}
/>
</div>
<div className="flex flex-col gap-3">
<h3 className="border-l-4 border-primary pl-3 text-xs font-bold uppercase tracking-wide text-foreground">
Last Mile
</h3>
<p className="pl-4 text-xs text-muted-foreground italic">
{booking.lastMileEnabled && booking.lastMileDeliveryAddress ? booking.lastMileDeliveryAddress : "Not requested"}
{booking.lastMileEnabled && booking.lastMileDeliveryAddress
? booking.lastMileDeliveryAddress
: "Not requested"}
</p>
</div>
</CardContent>
@@ -793,31 +959,57 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
</CardHeader>
<CardContent className="flex flex-col gap-6">
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<InfoItem icon={<Package />} label="Freight Type" value={booking.freightType === "BULK" ? "Bulk" : "Break Bulk"} />
<InfoItem icon={<Weight />} label="Weight (VGM)" value={`${booking.cargoTotalWeightVgm} Tons`} />
<InfoItem icon={<Ship />} label="Currency" value={booking.paymentCurrency} />
<InfoItem
icon={<Package />}
label="Freight Type"
value={
booking.freightType === "BULK" ? "Bulk" : "Break Bulk"
}
/>
<InfoItem
icon={<Weight />}
label="Weight (VGM)"
value={`${booking.cargoTotalWeightVgm} Tons`}
/>
<InfoItem
icon={<Ship />}
label="Currency"
value={booking.paymentCurrency}
/>
</div>
{booking.containers && booking.containers.length > 0 && (
<>
<Separator />
<div className="flex flex-col gap-3">
<h3 className="text-xs font-bold uppercase tracking-wide text-foreground">Load Details</h3>
<h3 className="text-xs font-bold uppercase tracking-wide text-foreground">
Load Details
</h3>
<div className="rounded-lg border overflow-hidden">
<table className="w-full text-left text-xs">
<thead className="bg-muted text-muted-foreground">
<tr>
<th className="px-3 py-2 font-semibold">Type</th>
<th className="px-3 py-2 font-semibold text-center">Quantity</th>
<th className="px-3 py-2 font-semibold text-right">VGM (Tons)</th>
<th className="px-3 py-2 font-semibold text-center">
Quantity
</th>
<th className="px-3 py-2 font-semibold text-right">
VGM (Tons)
</th>
</tr>
</thead>
<tbody className="divide-y">
{booking.containers.map((c, i) => (
<tr key={i}>
<td className="px-3 py-2 font-medium">{c.type}</td>
<td className="px-3 py-2 text-center">{c.qty} Units</td>
<td className="px-3 py-2 text-right">{c.vgm}t</td>
<td className="px-3 py-2 font-medium">
{c.type}
</td>
<td className="px-3 py-2 text-center">
{c.qty} Units
</td>
<td className="px-3 py-2 text-right">
{c.vgm}t
</td>
</tr>
))}
</tbody>
@@ -839,7 +1031,10 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<InfoItem label="Type" value={booking.contractType === "RENEWAL" ? "Renewal" : "New"} />
<InfoItem
label="Type"
value={booking.contractType === "RENEWAL" ? "Renewal" : "New"}
/>
<InfoItem label="Customer ID" value={booking.customerId} />
<Separator />
<div className="flex flex-wrap gap-2">
@@ -860,15 +1055,21 @@ 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">Cargo Description</p>
<p className="text-xs text-foreground leading-relaxed italic">"{booking.freightSubtype}"</p>
<p className="text-[9px] font-bold uppercase tracking-tight text-muted-foreground">
Cargo Description
</p>
<p className="text-xs text-foreground leading-relaxed italic">
"{booking.freightSubtype}"
</p>
</div>
)}
{booking.financialTerms && (
<>
<Separator />
<div className="flex flex-col gap-1">
<p className="text-[9px] font-bold uppercase tracking-tight text-muted-foreground">Financial Terms</p>
<p className="text-[9px] 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">
<p className="text-xs text-amber-900 flex gap-2">
<StickyNote className="size-3 shrink-0 mt-0.5 text-amber-500" />
@@ -879,7 +1080,9 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
</>
)}
{!booking.freightSubtype && !booking.financialTerms && (
<p className="text-xs text-muted-foreground italic">No additional information provided.</p>
<p className="text-xs text-muted-foreground italic">
No additional information provided.
</p>
)}
</CardContent>
</Card>
@@ -917,17 +1120,23 @@ function RouteEndpoint({
function InfoItem({
icon,
label,
value
value,
}: {
icon?: React.ReactNode;
label: string;
value?: string | number | null
value?: string | number | null;
}) {
return (
<div className="flex items-start gap-2">
{icon && <div className="mt-0.5 text-muted-foreground [&_svg]:size-3.5">{icon}</div>}
{icon && (
<div className="mt-0.5 text-muted-foreground [&_svg]:size-3.5">
{icon}
</div>
)}
<div className="flex flex-col">
<p className="text-[9px] font-bold uppercase tracking-tight text-muted-foreground">{label}</p>
<p className="text-[9px] font-bold uppercase tracking-tight text-muted-foreground">
{label}
</p>
<p className="text-xs font-bold text-foreground">{value ?? "—"}</p>
</div>
</div>
@@ -946,9 +1155,12 @@ function StatusBadge({ status }: { status: string }) {
return (
<Badge
variant="outline"
className={cn("px-2 py-0.5 font-bold uppercase tracking-wider text-[9px]", statusColors[status] || "bg-muted")}
className={cn(
"px-2 py-0.5 font-bold uppercase tracking-wider text-[9px]",
statusColors[status] || "bg-muted",
)}
>
{status.replace(/_/g, ' ')}
{status.replace(/_/g, " ")}
</Badge>
);
}

View File

@@ -18,7 +18,6 @@ import { trackingService } from "./tracking.service";
import { fileUploadSettingsService } from "./fileUploadSettings.service";
import { dropdownSettingsService } from "./dropdownSettings.service";
import { authService } from "./auth.service";
import { customersService } from "./customers.service";
import { companiesService } from "./companies.service";
import {
CreateDropdownOptionDto,
@@ -28,11 +27,6 @@ import {
UpdateDropdownOptionDto,
UpdateDropdownSettingDto,
} from "@/types/dropdownSettings";
import {
CreateCustomerDto,
Customer,
UpdateCustomerDto,
} from "@/types/customers";
import type {
CompanyInfoResponse,
CreateCompanyPayload,