diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ClearanceCard.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ClearanceCard.tsx
index e5ea8d3a9..3002356f7 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ClearanceCard.tsx
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ClearanceCard.tsx
@@ -51,7 +51,12 @@ export function ClearanceCard({ booking }: { booking: Freight.IBooking }) {
}
const summary =
- status === "CLEARANCE_READY" ? (
+ status === "OPERATION_CHANGES_REQUESTED" ? (
+ }>
+ Operations returned this order for changes. Update the booking details,
+ pick a new shipment day and resubmit.
+
+ ) : status === "CLEARANCE_READY" ? (
}>
{`${
booking.customsClearingEnabled
@@ -103,9 +108,11 @@ export function ClearanceCard({ booking }: { booking: Freight.IBooking }) {
{summary}
- {isBookAction
- ? "Use “Book” to enter the cargo details and schedule your shipment."
- : `Use “${action?.label ?? "the action button"}” to manage your ${docNoun}.`}
+ {status === "OPERATION_CHANGES_REQUESTED"
+ ? "Use “Change booking” to update the details and pick a new shipment day."
+ : isBookAction
+ ? "Use “Book” to enter the cargo details and schedule your shipment."
+ : `Use “${action?.label ?? "the action button"}” to manage your ${docNoun}.`}
{!isBookAction && (
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/clearance/bookingNextAction.ts b/apps/edr-freight-web/portal/src/pages/bookings/clearance/bookingNextAction.ts
index ea70e2fa9..297b0c152 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/clearance/bookingNextAction.ts
+++ b/apps/edr-freight-web/portal/src/pages/bookings/clearance/bookingNextAction.ts
@@ -90,11 +90,21 @@ function actionByStatus(booking: ActionBooking): BookingNextAction | null {
title: "Schedule your shipment",
};
case "OPERATION_CHANGES_REQUESTED":
- return {
- kind: "SCHEDULE_OPERATION",
- label: "Choose day & resubmit",
- title: "Resubmit your shipment",
- };
+ // Contract bookings reopen the full completion form (cargo + shipment
+ // day, prefilled from the booking) — same page as the initial booking.
+ // Contract-less bookings keep the in-place day-picker modal.
+ return booking.contractId
+ ? {
+ kind: "BOOK",
+ label: "Change booking",
+ title: "Change your booking",
+ to: `/contracts/${booking.contractId}/bookings/${booking.id}/complete`,
+ }
+ : {
+ kind: "SCHEDULE_OPERATION",
+ label: "Choose day & resubmit",
+ title: "Resubmit your shipment",
+ };
default:
return null;
}
diff --git a/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentPage.tsx b/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentPage.tsx
index aa4ce723a..aab5d7de1 100644
--- a/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentPage.tsx
@@ -38,6 +38,7 @@ import {
CheckCircle2,
ChevronLeft,
FileDown,
+ FileText,
FileUp,
Flame,
MapPin,
@@ -59,6 +60,7 @@ import {
contractsService,
type ShipmentValidation,
} from "@/services/contracts.service";
+import { downloadStoredFile } from "@/services/files.service";
import {
SelectField,
StepCard,
@@ -245,6 +247,127 @@ function bulkUnitOfMeasure(
return hasPerItem ? "PER_ITEM" : "PER_TON";
}
+/**
+ * Prefill for a changes-requested resubmit: the booking's persisted cargo,
+ * currency and route become the form's starting values so the customer edits
+ * what exists instead of retyping it. The shipment day is deliberately left
+ * empty — a new day must be picked.
+ */
+function mapBookingToShipmentValues(
+ booking: Freight.IBooking,
+ contract: Freight.IContract,
+): Partial {
+ const b = booking as unknown as {
+ cargoFreeText?: string | null;
+ contractRouteId?: string | null;
+ cargoTotalWeightVgm?: number | string | null;
+ bulkTotalWeightTons?: number | string | null;
+ bulkHazardousQuantity?: number | string | null;
+ bulkReeferQuantity?: number | string | null;
+ bookingContainers?: Array<{
+ quantity?: number;
+ hazardousQuantity?: number | string | null;
+ reeferQuantity?: number | string | null;
+ returnQuantity?: number | string | null;
+ containerType?: { sizeFt?: number | null } | null;
+ units?: Array<{
+ containerNumber?: string;
+ sealNumber?: string | null;
+ vgmTons?: number | string;
+ isHazardous?: boolean;
+ isReefer?: boolean;
+ isReturn?: boolean;
+ }>;
+ }>;
+ };
+ const values: Partial = {
+ paymentCurrency: booking.paymentCurrency === "ETB" ? "ETB" : "USD",
+ withReturn: booking.equipmentReturn === "WITH_RETURN",
+ cargoDescription: b.cargoFreeText ?? "",
+ ...(b.contractRouteId ? { contractRouteId: b.contractRouteId } : {}),
+ };
+ if (contract.freightType === "CONTAINER") {
+ const rows = b.bookingContainers ?? [];
+ const lineFor = (size: "20ft" | "40ft") => {
+ const bc = rows.find(
+ (r) => (r.containerType?.sizeFt === 40 ? "40ft" : "20ft") === size,
+ );
+ return {
+ containerSize: size,
+ quantity: String(bc?.quantity ?? 0),
+ hazardousQuantity: String(Number(bc?.hazardousQuantity ?? 0)),
+ reeferQuantity: String(Number(bc?.reeferQuantity ?? 0)),
+ returnQuantity: String(Number(bc?.returnQuantity ?? 0)),
+ units: (bc?.units ?? []).map((u) => ({
+ containerNumber: u.containerNumber ?? "",
+ sealNumber: u.sealNumber ?? "",
+ vgmTons: String(Number(u.vgmTons ?? 0)),
+ isHazardous: Boolean(u.isHazardous),
+ isReefer: Boolean(u.isReefer),
+ isReturn: Boolean(u.isReturn),
+ })),
+ };
+ };
+ const sizes = (contract.cargoScope ?? [])
+ .map((s) => s.containerSize)
+ .filter((s): s is "20ft" | "40ft" => s === "20ft" || s === "40ft");
+ values.containers = (sizes.length ? sizes : ["20ft", "40ft"]).map(lineFor);
+ } else {
+ const perItem = bulkUnitOfMeasure(contract) === "PER_ITEM";
+ const amount = Number(b.cargoTotalWeightVgm ?? 0);
+ if (perItem) {
+ values.itemCount = amount ? String(amount) : "";
+ values.cargoWeightTons =
+ b.bulkTotalWeightTons != null
+ ? String(Number(b.bulkTotalWeightTons))
+ : "";
+ } else {
+ values.cargoWeightTons = amount ? String(amount) : "";
+ }
+ values.bulkHazardousQuantity = String(Number(b.bulkHazardousQuantity ?? 0));
+ values.bulkReeferQuantity = String(Number(b.bulkReeferQuantity ?? 0));
+ }
+ return values;
+}
+
+/** Read-only list of the booking's already-uploaded documents (resubmit view). */
+function UploadedDocumentsCard({ booking }: { booking: Freight.IBooking }) {
+ const files =
+ (booking as unknown as { files?: Array<{ id: string; name: string }> })
+ .files ?? [];
+ if (!files.length) return null;
+ return (
+
+
+
+
+ Your uploaded documents
+
+
+
+ These stay attached to the booking — no need to upload them again.
+
+
+ {files.map((f) => (
+
+
+ {f.name}
+
+ void downloadStoredFile(f.id, f.name)}
+ aria-label={`Download ${f.name}`}
+ >
+
+
+
+ ))}
+
+
+ );
+}
+
function NewShipmentBookingForm({
contract,
contractId,
@@ -306,6 +429,34 @@ function NewShipmentBookingForm({
: 0;
const hasOdd20ft = ft20Total % 2 === 1;
+ // COMPLETION mode: fetch the booking — a changes-requested resubmit prefills
+ // the form from it and shows the operations note + uploaded documents.
+ const { data: completeBooking } = useQuery(
+ api.bookings.get.queryOptions({
+ input: { id: completeBookingId! },
+ enabled: Boolean(completeBookingId),
+ }),
+ );
+ const isResubmit = Boolean(
+ completeBooking &&
+ ["OPERATION_CHANGES_REQUESTED", "EXPIRED"].includes(
+ completeBooking.status as string,
+ ) &&
+ (((completeBooking as unknown as { bookingContainers?: unknown[] })
+ .bookingContainers?.length ?? 0) > 0 ||
+ Number(completeBooking.cargoTotalWeightVgm ?? 0) > 0),
+ );
+ const prefilledRef = useRef(false);
+ useEffect(() => {
+ if (!isResubmit || prefilledRef.current || !completeBooking) return;
+ prefilledRef.current = true;
+ form.reset({
+ ...form.getValues(),
+ ...mapBookingToShipmentValues(completeBooking, contract),
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isResubmit]);
+
const submitMutation = useMutation({
mutationFn: (dto: Freight.CreateBookingUnderContractDto) =>
completeBookingId
@@ -488,12 +639,16 @@ function NewShipmentBookingForm({
style={{ letterSpacing: "-0.01em" }}
>
{completeBookingId
- ? "Complete Your Booking"
+ ? isResubmit
+ ? "Change Your Booking"
+ : "Complete Your Booking"
: "New Shipment Booking"}
{completeBookingId
- ? `Clearance is finalized — enter the cargo details and shipment day to complete your booking under contract ${contract.reference}.`
+ ? isResubmit
+ ? `Update the details below and pick a new shipment day, then resubmit your booking under contract ${contract.reference}.`
+ : `Clearance is finalized — enter the cargo details and shipment day to complete your booking under contract ${contract.reference}.`
: `Book a shipment against contract ${contract.reference}.`}
@@ -535,6 +690,16 @@ function NewShipmentBookingForm({
{/* Single-step form — all sections on one page. */}
+ {isResubmit && completeBooking?.latestChangeRequestNote && (
+ }
+ radius="md"
+ title="Operations requested changes"
+ >
+ {completeBooking.latestChangeRequestNote}
+
+ )}
{/* Legacy contracts only — WITH_RETURN contracts capture per-line
@@ -547,6 +712,9 @@ function NewShipmentBookingForm({
routes={routes}
completeBookingId={completeBookingId ?? null}
/>
+ {isResubmit && completeBooking && (
+
+ )}
{/* Notes are captured when the booking is initiated — completing
a bare booking does not re-ask for them. */}
{!completeBookingId && }
@@ -594,7 +762,7 @@ function NewShipmentBookingForm({
onClick={handleReview}
disabled={hasOdd20ft}
>
- Review price & book
+ {isResubmit ? "Change booking" : "Review price & book"}