diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts index 6b1d65df8..7cc3d60d8 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts @@ -43,9 +43,10 @@ import { BookingClearanceChargeService } from './booking-clearance-charge.servic import { BookingPayablesService } from './booking-payables.service'; import { ClearanceEventService } from './clearance-event.service'; import { - BillClearanceChargeDto, + RejectClearanceChargeDto, } from './dto/clearance-charge.dto'; +import { BillClearanceChargeDto } from './dto/clearance-charge.dto'; import { AdditionalChargeService } from './additional-charge.service'; import { CancelAdditionalChargeDto, CreateAdditionalChargeDto } from './dto/additional-charge.dto'; import { BookingContractService } from './booking-contract.service'; diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainConsistView.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainConsistView.tsx index d29807ecc..2b22a2ccc 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainConsistView.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainConsistView.tsx @@ -18,6 +18,8 @@ interface TrainConsistViewProps { scheduleDetail: TrainScheduleDetail; scheduleId: string; maxWagons: number; + /** Hide the consist-wide Wagons stat tile (dispatch shows leg capacity instead). */ + showWagonStat?: boolean; /** Booking id selected in the side panel — highlights its wagons in the consist. */ highlightBookingId?: string | null; } @@ -45,6 +47,7 @@ export const TrainConsistView = ({ scheduleDetail, scheduleId, maxWagons, + showWagonStat = true, highlightBookingId, }: TrainConsistViewProps) => { const [selectedWagonId, setSelectedWagonId] = useState(null); @@ -172,6 +175,7 @@ export const TrainConsistView = ({ lengthMax={lengthMax} wagonCount={wagonsUsed} wagonMax={maxWagons} + showWagons={showWagonStat} /> {/* Consist panel */} diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainStatsBar.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainStatsBar.tsx index 3403258ef..b99c62254 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainStatsBar.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainStatsBar.tsx @@ -9,6 +9,12 @@ interface TrainStatsBarProps { lengthMax: number | null; wagonCount: number; wagonMax: number; + /** + * The wagon tile is a consist-wide count, which reads as wrong on a + * multi-leg schedule where per-leg capacity is the real number. Dispatch + * hides it (leg capacity is shown there instead); the batch board keeps it. + */ + showWagons?: boolean; } function pctColor(pct: number) { @@ -83,6 +89,7 @@ export const TrainStatsBar = ({ lengthMax, wagonCount, wagonMax, + showWagons = true, }: TrainStatsBarProps) => { const weightPct = weightMax ? (weightUsed / weightMax) * 100 : null; const lengthPct = lengthMax ? (lengthUsed / lengthMax) * 100 : null; @@ -95,7 +102,7 @@ export const TrainStatsBar = ({ withBorder style={{ borderColor: "var(--mantine-color-gray-2)", background: "white" }} > - + } label="Gross weight" @@ -108,7 +115,7 @@ export const TrainStatsBar = ({ px={{ base: 0, xs: "lg" }} style={{ borderLeft: "1px solid var(--mantine-color-gray-2)", - borderRight: "1px solid var(--mantine-color-gray-2)", + borderRight: showWagons ? "1px solid var(--mantine-color-gray-2)" : undefined, }} > - } - label="Wagons" - pct={wagonPct} - current={String(wagonCount)} - max={String(wagonMax)} - unit="" - /> + {showWagons ? ( + } + label="Wagons" + pct={wagonPct} + current={String(wagonCount)} + max={String(wagonMax)} + unit="" + /> + ) : null} ); diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx index 5d9616cec..69be66d15 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx @@ -848,6 +848,7 @@ export default function TrainScheduleV2DetailPage() { scheduleDetail={schedule} scheduleId={scheduleId ?? ""} maxWagons={schedule.maxWagons ?? 53} + showWagonStat={false} /> ) : ( ( - - - {row.original.routeName ?? "—"} - - {row.original.direction ? ( - - {row.original.direction} - - ) : null} - - + + {row.original.direction ? ( + + {row.original.direction} + + ) : null} + + ), }, @@ -888,12 +885,12 @@ export default function TrainScheduleV2ListPage() { } /** - * The row's wagon chips, matching the detail page's wagon plan: used is slots - * carrying a booking allocation, the denominator is the schedule's capacity - * (API-computed: the larger of coupled consist and planned `maxWagons`, since - * wagons are coupled on demand), and remaining excludes wagons reserved by - * bookings that have not paid yet — that space is claimed, so it is not - * bookable. + * The row's wagon chips: used is slots carrying a booking allocation, the + * denominator is the schedule's capacity (API-computed: the larger of coupled + * consist and planned `maxWagons`, since wagons are coupled on demand). Both + * are consist-wide totals, so on a multi-leg schedule they do not describe any + * single leg — the bookable/planned counts were dropped for that reason; the + * detail page's wagon plan is the per-leg source of truth. */ /** Green tint for departures dedicated to a shipping line (overrides direction tint). */ const SHIPPING_LINE_ROW_STYLE = { @@ -965,20 +962,15 @@ function WagonChips({ schedule }: { schedule: TrainScheduleListItem }) { const total = schedule.wagonsTotal ?? schedule.wagonCount; const used = schedule.wagonsUsed; const reserved = schedule.wagonsReserved ?? 0; - const remaining = schedule.wagonsRemaining; - if (used == null) { + if (used == null || schedule.wagonCount === 0) { return ; } return ( <> - + {reserved > used ? : null} - {remaining != null ? : null} ); } @@ -1049,9 +1041,6 @@ function ScheduleCard({ {schedule.reference} ) : null} - - {schedule.routeName ?? "Train schedule"} - {day} · {time} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx index 97400d580..f5906ceea 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx @@ -8,11 +8,12 @@ import { Package, TrainFront, Truck, + Wallet, XCircle, } from "lucide-react"; import { useState } from "react"; import toast from "react-hot-toast"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { useFileViewer } from "@/hooks/useFileViewer"; import { bookingsService } from "@/services/bookings.service"; @@ -44,17 +45,17 @@ import { HeaderButton, PageHeader } from "./components/PageHeader"; import { PaymentMethodModal } from "./components/PaymentMethodModal"; import { ScheduleCard } from "./components/ScheduleCard"; import { WarehousePaymentsSection } from "./components/WarehousePaymentsSection"; +import { PaymentsDueStrip, PaymentsTab } from "./components/PaymentsTab"; import { WarehouseLocationCard } from "./components/WarehouseLocationCard"; import { ShipmentDetailsCard } from "./components/ShipmentDetailsCard"; import { ShipmentTrackingCard } from "./components/ShipmentTrackingCard"; import { StatusHero } from "./components/StatusHero"; import { SupportCard } from "./components/SupportCard"; -import { WagonCancellationCard } from "./components/WagonCancellationCard"; import { WagonsTab } from "./components/WagonsTab"; import { fmtDate, isNegative, priceTotal } from "./utils"; import { useScrollToHash } from "@/hooks/useScrollToHash"; import { useBookingPayment } from "@/pages/bookings/payments/useBookingPayment"; -import { isUsdOfflineBooking } from "@/pages/bookings/payments/offline-payment"; +import { useBookingPayables } from "@/pages/bookings/payments/useBookingPayables"; // Pre-payment statuses the customer may self-cancel from this view (free of // charge). DRAFT / CHANGES_REQUESTED render their own views and drafts can @@ -87,6 +88,22 @@ export function ReadonlyBookingView({ const navigate = useNavigate(); // Deep-link support: e.g. /bookings/:id#warehouse-payments from an invoice. useScrollToHash(); + // Active tab lives in the URL (?tab=payments) so list/home "Pay" buttons and + // notifications can land straight on the Payments tab. + const [searchParams, setSearchParams] = useSearchParams(); + const tab = searchParams.get("tab") ?? "overview"; + const setTab = (next: string | null) => + setSearchParams( + (prev) => { + if (!next || next === "overview") prev.delete("tab"); + else prev.set("tab", next); + return prev; + }, + { replace: true }, + ); + // Everything the customer owes or must decide on — drives the header "Pay" + // button, the tab badge and the overview strip. + const payables = useBookingPayables(booking); const status = booking.status as string; const { viewer } = useFileViewer(); @@ -187,17 +204,19 @@ export function ReadonlyBookingView({ 0 && tab !== "payments") || + canCancel) && ( {canApproveDelivery && ( )} - {canPay && !showCountdown && !isUsdOfflineBooking(booking) && ( + {payables.items.length > 0 && tab !== "payments" && ( } - label="Pay now" - onClick={pay.open} + label="Pay" + onClick={() => setTab("payments")} /> )} {canCancel && ( @@ -260,7 +279,8 @@ export function ReadonlyBookingView({ }> Overview + } + rightSection={ + payables.items.length > 0 ? ( + + {payables.items.length} + + ) : undefined + } + > + Payments + }> Cargo @@ -298,12 +344,16 @@ export function ReadonlyBookingView({
+ {/* The only payment surface on Overview — everything payable lives + on the Payments tab. Hidden when nothing is outstanding. */} + setTab("payments")} /> + {isClearance && } - {/* Customs (Path B) shipments: GL's phased progress, the duty / - additional-duty payments and the final invoice — all per booking. */} + {/* Customs (Path B) shipments: GL's phased progress, draft + declaration and documents. Its payments moved to the Payments tab. */} - {/* Renders only on PAID + paid + contract-backed bookings. */} - @@ -346,6 +391,15 @@ export function ReadonlyBookingView({
+ + + +