diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts
index 12c56ac13..1d2300d9a 100644
--- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts
+++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts
@@ -1425,7 +1425,46 @@ export class BookingsService {
tradeDirection,
);
}
- if (dto.scheduledDate) updates.scheduledDate = new Date(dto.scheduledDate);
+ // Re-pinning the departure day on an edit (e.g. fixing a CHANGES_REQUESTED
+ // booking) must obey the same gate as creation: the route needs an OPEN
+ // departure on that EAT day that can carry the cargo. Skipped when the day
+ // didn't change, for general contracts (period-based, no pinned day) and
+ // for intercity (staff assign a passing train later).
+ if (dto.scheduledDate) {
+ const day = eatDay(new Date(dto.scheduledDate));
+ const dayChanged =
+ !existing.scheduledDate || eatDay(existing.scheduledDate) !== day;
+ if (
+ dayChanged &&
+ existing.bookingType !== 'GENERAL_CONTRACT' &&
+ tradeDirection !== 'DOMESTIC'
+ ) {
+ const { hasDeparture, hasCompatible } =
+ await this.trainSchedulingService.checkDayCargoCompatibility(
+ originYardId,
+ destinationYardId,
+ day,
+ {
+ freightType: freightType as 'CONTAINER' | 'BULK',
+ cargoTypeId,
+ containerTypeIds: containers
+ .map((c) => c.containerTypeId)
+ .filter((cid): cid is string => Boolean(cid)),
+ },
+ );
+ if (!hasDeparture) {
+ throw new BadRequestException(
+ 'No departures available on the selected day for this route',
+ );
+ }
+ if (!hasCompatible) {
+ throw new BadRequestException(
+ 'No wagon on the selected day can carry this cargo type — please choose another day',
+ );
+ }
+ }
+ updates.scheduledDate = new Date(dto.scheduledDate);
+ }
if (dto.estimatedShipmentDate)
updates.estimatedShipmentDate = new Date(dto.estimatedShipmentDate);
if (dto.startDate) updates.startDate = new Date(dto.startDate);
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ChangesRequestedView.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ChangesRequestedView.tsx
index 3cb3feb9e..6ae046f1d 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ChangesRequestedView.tsx
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ChangesRequestedView.tsx
@@ -1,18 +1,30 @@
import {
Alert,
+ Box,
Button,
Group,
Modal,
Stack,
Text,
TextInput,
+ UnstyledButton,
} from "@mantine/core";
import { useMutation, useQuery } from "@tanstack/react-query";
-import { AlertCircle, Pencil, Send, XCircle } from "lucide-react";
+import {
+ AlertCircle,
+ CalendarDays,
+ ChevronRight,
+ Package,
+ Pencil,
+ Send,
+ XCircle,
+} from "lucide-react";
+import type { ReactNode } from "react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { api } from "@/services/api";
+import { bookingsService } from "@/services/bookings.service";
import type { Freight } from "@edr/types";
import { PriceChangeModal } from "@/pages/bookings/resubmit/PriceChangeModal";
@@ -25,13 +37,55 @@ import { CompanyInfoCard } from "./components/CompanyInfoCard";
import { ContainersCard } from "./components/ContainersCard";
import { ContractInfoCard } from "./components/ContractInfoCard";
import { ActionRequiredBanner, MutationErrors } from "./components/Notices";
-import { PageHeader } from "./components/PageHeader";
+import { HeaderButton, PageHeader } from "./components/PageHeader";
import { EstimateCard } from "./components/pricing";
import { ScheduleCard } from "./components/ScheduleCard";
import { ShipmentDetailsCard } from "./components/ShipmentDetailsCard";
import { StatusHero } from "./components/StatusHero";
import { SupportCard } from "./components/SupportCard";
+/** Row linking straight to one section of the edit-booking form. */
+function EditLink({
+ icon,
+ title,
+ description,
+ onClick,
+}: {
+ icon: ReactNode;
+ title: string;
+ description: string;
+ onClick: () => void;
+}) {
+ return (
+
+
+
+ {icon}
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+
+
+
+
+ );
+}
+
/**
* Detail-page view for a booking staff returned with CHANGES_REQUESTED.
*
@@ -64,8 +118,9 @@ export function ChangesRequestedView({
null) as Freight.PricingBreakdown | null;
const cancelMutation = useMutation({
+ // Customer-facing cancel endpoint — the plain /cancel route is staff-only.
mutationFn: (reason: string) =>
- api.bookings.cancel.call({ id: booking.id, reason }),
+ bookingsService.customerCancel(booking.id, reason),
onSuccess: () => {
setCancelDialogOpen(false);
onBookingUpdated();
@@ -76,10 +131,14 @@ export function ChangesRequestedView({
setCancelDialogOpen(true),
- onSupport: () => navigate("/support"),
- }}
+ actions={
+ }
+ label="Cancel booking"
+ onClick={() => setCancelDialogOpen(true)}
+ />
+ }
/>
@@ -101,6 +160,41 @@ export function ChangesRequestedView({
+
+ Fix your booking
+
+ Staff asked for changes on this booking. Update whatever needs
+ fixing below, then resubmit for review — the booking stays in
+ place, no need to start over.
+
+
+ }
+ title="Cargo & containers"
+ description="Add or remove containers, change container type, quantity or VGM — or for bulk cargo, change the commodity and tonnage."
+ onClick={() =>
+ navigate(`/bookings/${booking.id}/edit?section=cargo`)
+ }
+ />
+ }
+ title="Schedule date"
+ description="Pick a different departure day — only days with an open schedule on your route can be selected."
+ onClick={() =>
+ navigate(`/bookings/${booking.id}/edit?section=schedule`)
+ }
+ />
+ }
+ title="Route, service & other details"
+ description="Change the origin or destination yard, service type, trucking options or notes."
+ onClick={() =>
+ navigate(`/bookings/${booking.id}/edit?section=service`)
+ }
+ />
+
+
+
Your documents
@@ -108,25 +202,8 @@ export function ChangesRequestedView({
Update the documents for this booking, then resubmit for review.
Replace any that changed and attach any that are still required.
- Need to change the cargo itself — containers, route, schedule or
- other details? Edit the booking first, then come back and
- resubmit.
- }
- onClick={() => navigate(`/bookings/${booking.id}/edit`)}
- styles={{
- root: { height: 46 },
- label: { fontSize: 14, fontWeight: 700 },
- }}
- >
- Edit booking details
-
-
{flow.validationError && (
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/DraftBookingView.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/DraftBookingView.tsx
index 277bd1a24..65af9f45d 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/DraftBookingView.tsx
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/DraftBookingView.tsx
@@ -24,7 +24,10 @@ import { useNavigate } from "react-router-dom";
import { api } from "@/services/api";
import { downloadStoredFile } from "@/services/files.service";
-import type { SubmitBookingResponse } from "@/services/bookings.service";
+import {
+ bookingsService,
+ type SubmitBookingResponse,
+} from "@/services/bookings.service";
import type { Freight } from "@edr/types";
import { REQUIRED_DOC_FIELDS } from "./constants";
@@ -116,8 +119,9 @@ export function DraftBookingView({
});
const cancelMutation = useMutation({
+ // Customer-facing cancel endpoint — the plain /cancel route is staff-only.
mutationFn: (reason: string) =>
- api.bookings.cancel.call({ id: booking.id, reason }),
+ bookingsService.customerCancel(booking.id, reason),
onSuccess: () => {
setCancelDialogOpen(false);
onBookingUpdated();
@@ -157,17 +161,21 @@ export function DraftBookingView({
}
- label="Continue editing"
- onClick={() => navigate(`/bookings/${booking.id}/edit`)}
- />
+
+ }
+ label="Continue editing"
+ onClick={() => navigate(`/bookings/${booking.id}/edit`)}
+ />
+ }
+ label="Cancel"
+ onClick={() => setCancelDialogOpen(true)}
+ />
+
}
- menuActions={{
- onCancel: () => setCancelDialogOpen(true),
- onSupport: () => navigate("/support"),
- }}
/>
{canApproveDelivery && (
@@ -185,14 +186,17 @@ export function ReadonlyBookingView({
onClick={pay.open}
/>
)}
+ {canCancel && (
+ }
+ label="Cancel booking"
+ onClick={() => setCancelOpen(true)}
+ />
+ )}
)
}
- menuActions={{
- onRebook: canSelfRebook ? onRebook : undefined,
- onSupport: () => navigate("/support"),
- onCancel: canCancel ? () => setCancelOpen(true) : undefined,
- }}
/>
{isNegative(status) ? (
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/PageHeader.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/PageHeader.tsx
index 455b71a69..95cfeab11 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/PageHeader.tsx
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/PageHeader.tsx
@@ -1,14 +1,5 @@
-import { ActionIcon, Button, Group, Menu, Stack, Text } from "@mantine/core";
-import {
- ArrowDownLeft,
- ArrowUpRight,
- Edit2,
- FileText,
- HelpCircle,
- MoreHorizontal,
- RefreshCw,
- XCircle,
-} from "lucide-react";
+import { Button, Group, Stack, Text } from "@mantine/core";
+import { ArrowDownLeft, ArrowUpRight } from "lucide-react";
import type { ReactNode } from "react";
import type { Freight } from "@edr/types";
@@ -20,22 +11,12 @@ import {
import { bookingSubtitle, isDraftLike, isNegative } from "../utils";
-export interface PageHeaderMenuActions {
- onViewContract?: () => void;
- onCancel?: () => void;
- onEdit?: () => void;
- onSupport?: () => void;
- onRebook?: () => void;
-}
-
export function PageHeader({
booking,
actions,
- menuActions,
}: {
booking: Freight.IBooking;
actions?: ReactNode;
- menuActions?: PageHeaderMenuActions;
}) {
const status = booking.status as string;
const negative = isNegative(status);
@@ -47,8 +28,6 @@ export function PageHeader({
const pillText = negative ? "#A93226" : draft ? "#475569" : "#0A6F4D";
const isExport = booking.tradeDirection === "EXPORT";
- const hasMenu = menuActions && Object.values(menuActions).some(Boolean);
-
return (
@@ -83,66 +62,6 @@ export function PageHeader({
{actions}
- {hasMenu && (
-
- )}
);
@@ -154,6 +73,7 @@ export function HeaderButton({
onClick,
dark,
green,
+ red,
disabled,
}: {
label: string;
@@ -161,6 +81,7 @@ export function HeaderButton({
onClick?: () => void;
dark?: boolean;
green?: boolean;
+ red?: boolean;
disabled?: boolean;
}) {
return (
@@ -169,14 +90,14 @@ export function HeaderButton({
disabled={disabled}
leftSection={icon}
radius={10}
- variant={green || dark ? "filled" : "default"}
- color={green ? "edr-green" : dark ? "#0C1A2B" : undefined}
+ variant={green || dark ? "filled" : red ? "outline" : "default"}
+ color={green ? "edr-green" : dark ? "#0C1A2B" : red ? "red" : undefined}
styles={{
root: { height: 42, paddingInline: 16 },
label: {
fontSize: 13,
fontWeight: 700,
- color: green || dark ? "#fff" : "#10202F",
+ color: green || dark ? "#fff" : red ? undefined : "#10202F",
},
}}
>