{PROGRESS_STAGES.map((stage, idx) => {
@@ -131,14 +141,15 @@ function ProgressTracker({
: state === "active"
? activeFill
: "#0EA371";
- const circleBorder = state === "idle" ? "1px solid #E1E7EE" : undefined;
+ const circleBorder =
+ state === "idle" ? "1px solid #E1E7EE" : undefined;
const circleShadow =
state === "active" ? `0 0 0 4px ${activeRing}` : undefined;
return (
{/* left connector */}
@@ -147,15 +158,19 @@ function ProgressTracker({
style={{
height: 3,
background:
- idx === 0 ? "transparent" : reachedLeft ? "#0EA371" : "#E1E7EE",
+ idx === 0
+ ? "transparent"
+ : reachedLeft
+ ? "#0EA371"
+ : "#E1E7EE",
}}
/>
{/* stage circle */}
{stage.label}
-
- {state === "done"
- ? "Completed"
- : state === "active"
- ? negative
- ? "Stopped"
- : "In progress"
- : "Pending"}
-
);
})}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/pricing.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/pricing.tsx
index ec6c10f02..1fec98a35 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/pricing.tsx
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/pricing.tsx
@@ -1,5 +1,5 @@
-import { Box, Button, Group, Stack, Text } from "@mantine/core";
-import { CheckCircle2, Clock, FileText } from "lucide-react";
+import { Box, Group, Stack, Text } from "@mantine/core";
+import { CheckCircle2, Clock } from "lucide-react";
import type { Freight } from "@edr/types";
@@ -173,16 +173,16 @@ export function PaymentCard({
>
)}
-
}
- styles={{ root: { height: 46 }, label: { fontSize: 13.5, fontWeight: 700, color: "#10202F" } }}
- >
- Download invoice
-
+ {/*
} */}
+ {/* styles={{ root: { height: 46 }, label: { fontSize: 13.5, fontWeight: 700, color: "#10202F" } }} */}
+ {/* > */}
+ {/* Download invoice */}
+ {/* */}
);
}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/constants.ts b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/constants.ts
index c6e19fb9b..6ff19a633 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/constants.ts
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/constants.ts
@@ -3,6 +3,7 @@ import {
FileText,
PackageCheck,
ShieldCheck,
+ Ship,
Train,
} from "lucide-react";
@@ -15,32 +16,41 @@ export const PROGRESS_STAGES = [
{
label: "Submitted",
icon: ClipboardCheck,
- statuses: ["SUBMITTED", "PENDING_APPROVAL"],
+ statuses: ["SUBMITTED"],
},
{
- label: "Approved",
+ label: "Approval",
+ icon: ShieldCheck,
+ statuses: ["PENDING_APPROVAL", "APPROVED_PENDING_SIGNATURE", "APPROVED"],
+ },
+ {
+ label: "Contract",
+ icon: ShieldCheck,
+ statuses: ["CONTRACT_READY", "SIGNED_CUSTOMER"],
+ },
+ {
+ label: "Payment",
icon: ShieldCheck,
statuses: [
- "APPROVED_PENDING_SIGNATURE",
- "APPROVED",
- "CONTRACT_READY",
- "SIGNED_CUSTOMER",
"FULLY_EXECUTED",
+ "SELECTED_FOR_BATCH",
+ "PAYMENT_VERIFICATION_IN_PROGRESS",
+ ],
+ },
+ {
+ label: "Loading",
+ icon: Ship,
+ statuses: [
+ "PAID",
+ "PNR_GENERATED",
+ "PENDING_CONSOLIDATION",
+ "CONSOLIDATED",
],
},
{
label: "In Transit",
icon: Train,
- statuses: [
- "SELECTED_FOR_BATCH",
- "EXPIRED",
- "PNR_GENERATED",
- "PAYMENT_VERIFICATION_IN_PROGRESS",
- "PAID",
- "IN_TRANSIT",
- "PENDING_CONSOLIDATION",
- "CONSOLIDATED",
- ],
+ statuses: ["EXPIRED", "IN_TRANSIT"],
},
{
label: "Complete",
@@ -72,7 +82,7 @@ export const STATUS_MAP: Record<
PENDING_APPROVAL: {
title: "Pending approval",
description: "Your booking is moving through the approval process.",
- stage: 1,
+ stage: 2,
},
APPROVED_PENDING_SIGNATURE: {
title: "Approved — awaiting signature",
@@ -88,71 +98,71 @@ export const STATUS_MAP: Record<
title: "Contract ready to sign",
description:
"Your contract is ready. Review and apply your signature to proceed.",
- stage: 2,
+ stage: 3,
},
SIGNED_CUSTOMER: {
title: "Signed — awaiting staff",
description:
"Your signature has been submitted. Awaiting the final staff signature.",
- stage: 2,
+ stage: 3,
},
FULLY_EXECUTED: {
title: "Contract fully executed",
description: "Signed by all parties. You can now proceed to payment.",
- stage: 2,
+ stage: 4,
},
SELECTED_FOR_BATCH: {
title: "Selected for a train — payment due",
description:
"Your booking was selected for a scheduled train. Complete payment within the pay window to secure your slot.",
- stage: 3,
+ stage: 4,
},
EXPIRED: {
title: "Pay window expired",
description:
"The payment window was missed. You can move this booking to another schedule or cancel it.",
- stage: 3,
+ stage: 6,
},
PNR_GENERATED: {
title: "Payment reference generated",
description:
"A payment reference number has been generated for this booking.",
- stage: 3,
+ stage: 5,
},
PAYMENT_VERIFICATION_IN_PROGRESS: {
title: "Verifying payment",
description: "Your payment is being verified.",
- stage: 3,
+ stage: 4,
},
PAID: {
title: "Payment confirmed",
description: "Payment has been confirmed for this booking.",
- stage: 3,
+ stage: 5,
},
IN_TRANSIT: {
title: "Cargo moving",
description: "Your shipment is currently moving through the rail network.",
- stage: 3,
+ stage: 6,
},
PENDING_CONSOLIDATION: {
title: "Pending consolidation",
description: "Awaiting a consolidation partner shipment.",
- stage: 3,
+ stage: 5,
},
CONSOLIDATED: {
title: "Consolidated",
description: "Cargo has been consolidated with a partner shipment.",
- stage: 3,
+ stage: 5,
},
COMPLETED: {
title: "Service complete",
description: "Cargo delivered and service successfully terminated.",
- stage: 4,
+ stage: 7,
},
DELIVERED: {
title: "Service complete",
description: "Cargo delivered and service successfully terminated.",
- stage: 4,
+ stage: 7,
},
REJECTED: {
title: "Booking rejected",
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx
index e992bdd20..696381917 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx
@@ -3,7 +3,7 @@ import type { CreateBookingPayload } from "@/services/bookings.service";
import { zodResolver } from "@hookform/resolvers/zod";
import { Alert, Box, Button, Group, Text, Title } from "@mantine/core";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { AlertCircle, Check, ChevronLeft, ChevronRight } from "lucide-react";
+import { AlertCircle, Check, ChevronLeft, ChevronRight, Send } from "lucide-react";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
@@ -97,6 +97,31 @@ export default function NewBookingPage() {
},
});
+ const submitMutation = useMutation({
+ mutationFn: async (payload: CreateBookingPayload) => {
+ const booking = await api.bookings.create.call(payload);
+
+ const documents = form.getValues("documents") ?? {};
+ const hasDocuments = Object.values(documents).some((value) =>
+ Array.isArray(value) ? value.length > 0 : Boolean(value),
+ );
+ if (hasDocuments) {
+ await api.bookings.uploadDocuments.call({
+ id: booking.id,
+ files: documents,
+ });
+ }
+
+ await api.bookings.submit.call({ id: booking.id });
+
+ return booking;
+ },
+ onSuccess: (booking) => {
+ queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() });
+ navigate(`/bookings/${booking.id}`);
+ },
+ });
+
const form = useForm
({
defaultValues: initialBookingFormValues,
resolver: zodResolver(bookingFormSchema),
@@ -116,6 +141,15 @@ export default function NewBookingPage() {
return route;
}, [originYard, destinationYard]);
+ const docValues = form.watch("documents") ?? {};
+ const hasDocuments = useMemo(
+ () =>
+ Object.values(docValues).some((value) =>
+ Array.isArray(value) ? value.length > 0 : Boolean(value),
+ ),
+ [docValues],
+ );
+
async function handleContinue() {
const valid = await form.trigger(stepFields[step], { shouldFocus: true });
if (!valid) return;
@@ -123,14 +157,14 @@ export default function NewBookingPage() {
setStep((currentStep) => Math.min(STEPS.length, currentStep + 1));
}
- const handleSubmit = form.handleSubmit((data) => {
+ function buildApiPayload(data: BookingFormValues): CreateBookingPayload {
if (data.contractType === "renewal" && !data.previousContractRef) {
form.setError("previousContractRef", {
type: "manual",
message: "Select a previous contract reference.",
});
setStep(1);
- return;
+ throw new Error("Validation failed");
}
const totalWeight =
@@ -141,7 +175,6 @@ export default function NewBookingPage() {
)
: Number(data.cargoWeight || 0);
- // ── Reference data lookups ──────────────────────────────────────────
const shippingLines = referenceData?.shipping_line ?? [];
const cargoTree = referenceData?.cargo_type ?? [];
const containerGroups = referenceData?.containers ?? [];
@@ -174,8 +207,7 @@ export default function NewBookingPage() {
(s) => s.id === data.serviceTypeId,
)!;
- // ── Build API payload ───────────────────────────────────────────────
- const apiPayload: CreateBookingPayload = {
+ return {
scheduledDate: new Date().toISOString(),
contractType:
data.contractType.toUpperCase() as CreateBookingPayload["contractType"],
@@ -222,8 +254,24 @@ export default function NewBookingPage() {
: {}),
...(cargoFreeText ? { cargoFreeText } : {}),
};
+ }
- createMutation.mutate(apiPayload);
+ const handleDraftSubmit = form.handleSubmit((data) => {
+ try {
+ const apiPayload = buildApiPayload(data);
+ createMutation.mutate(apiPayload);
+ } catch {
+ // validation error already handled
+ }
+ });
+
+ const handleFullSubmit = form.handleSubmit((data) => {
+ try {
+ const apiPayload = buildApiPayload(data);
+ submitMutation.mutate(apiPayload);
+ } catch {
+ // validation error already handled
+ }
});
return (
@@ -270,7 +318,7 @@ export default function NewBookingPage() {
id="new-booking-form"
className="flex flex-col"
style={{ flex: 1 }}
- onSubmit={handleSubmit}
+ onSubmit={handleDraftSubmit}
>
@@ -295,6 +343,24 @@ export default function NewBookingPage() {
)}
+ {submitMutation.isError && (
+ }
+ radius="md"
+ mb="lg"
+ >
+
+ Failed to submit
+
+
+ {submitMutation.error instanceof Error
+ ? submitMutation.error.message
+ : "An unexpected error occurred. Please try again."}
+
+
+ )}
+
{step === 1 && (
)}
@@ -367,18 +433,35 @@ export default function NewBookingPage() {
Continue
) : (
-
- }
- >
- {createMutation.isPending ? "Saving Draft..." : "Save as Draft"}
-
+
+
+ }
+ >
+ {createMutation.isPending ? "Saving Draft..." : "Save as Draft"}
+
+ {hasDocuments && (
+
+ }
+ onClick={() => handleFullSubmit()}
+ >
+ {submitMutation.isPending ? "Submitting..." : "Submit"}
+
+ )}
+
)}