diff --git a/apps/edr-freight-api/src/modules/booking-orders/booking-orders.service.ts b/apps/edr-freight-api/src/modules/booking-orders/booking-orders.service.ts index 8eb258a88..da7fce027 100644 --- a/apps/edr-freight-api/src/modules/booking-orders/booking-orders.service.ts +++ b/apps/edr-freight-api/src/modules/booking-orders/booking-orders.service.ts @@ -41,12 +41,54 @@ export class BookingOrdersService { ) {} /** Orders placed against a contract, with their lines and child booking. */ - listByContract(contractBookingId: string): Promise { - return this.ordersRepository.findByContract(contractBookingId); + async listByContract(contractBookingId: string): Promise { + const orders = await this.ordersRepository.findByContract(contractBookingId); + await Promise.all(orders.map((o) => this.syncOrderFromChild(o))); + return orders; } - findById(id: string): Promise { - return this.ordersRepository.findById(id); + async findById(id: string): Promise { + const order = await this.ordersRepository.findById(id); + if (order) await this.syncOrderFromChild(order); + return order; + } + + /** + * The order is a ledger row; the spawned child ONE_TIME booking is what + * actually moves through the workflow (clearance → marketing/ops accept → + * pay → allocate), exactly like a one-time booking. Nothing writes the order + * row after creation, so its stored status would stay 'PENDING' forever. + * + * Mirror the child onto the order whenever it is read: copy the child's + * status, schedulingStatus and trainScheduleId onto the order (mutating the + * in-memory instance the caller gets back), and persist that snapshot when it + * has drifted so list/detail views and any stored reporting stay in sync. + */ + private async syncOrderFromChild(order: BookingOrder): Promise { + const child = order.booking; + if (!child) return; + + const nextStatus = child.status; + const nextScheduling = child.schedulingStatus; + const nextTrainScheduleId = child.trainScheduleId ?? null; + + const drifted = + order.status !== nextStatus || + order.schedulingStatus !== nextScheduling || + (order.trainScheduleId ?? null) !== nextTrainScheduleId; + + // Reflect the child onto the instance returned to the caller. + order.status = nextStatus; + order.schedulingStatus = nextScheduling; + order.trainScheduleId = nextTrainScheduleId; + + if (drifted) { + await this.ordersRepository.update(order.id, { + status: nextStatus, + schedulingStatus: nextScheduling, + trainScheduleId: nextTrainScheduleId, + }); + } } /** 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 7b23d6b16..4061a6a89 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -39,6 +39,7 @@ import { getRouteDirection, initialBookingFormValues, operationToProfileType, + operationToTradeDirection, stepFields, type BookingDocuments, type BookingFormValues, @@ -271,6 +272,7 @@ export default function NewBookingPage() { const originYard = form.watch("originYard"); const destinationYard = form.watch("destinationYard"); + const operationType = form.watch("operationType"); // The estimated shipment date lives in the Route step now; for general // contracts that date field is simply hidden there (the date is chosen per @@ -298,9 +300,16 @@ export default function NewBookingPage() { (y) => y.id === destinationYard, ); - const route = getRouteDirection(origin, destination); - return route; - }, [originYard, destinationYard]); + // Country-based derivation is authoritative when both yards are tagged, but + // returns null if a yard is unselected or lacks a country (e.g. intercity + // yards with no country set → DOMESTIC). The operation chosen in step 0 is + // the user's explicit intent, so fall back to it to guarantee a valid value + // and avoid posting tradeDirection: null (which the API rejects with @IsIn). + return ( + getRouteDirection(origin, destination) ?? + (operationType ? operationToTradeDirection(operationType) : null) + ); + }, [originYard, destinationYard, operationType, referenceData]); // The company's onboarded profile types — drives which operations are offered // and which profile each operation stamps the booking to. diff --git a/apps/edr-freight-web/portal/src/pages/contracts/contract-ui.tsx b/apps/edr-freight-web/portal/src/pages/contracts/contract-ui.tsx index 9f34ba745..4496a67a5 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/contract-ui.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/contract-ui.tsx @@ -65,6 +65,21 @@ export const CONTRACT_STATUS_CONFIG: Record< FULLY_EXECUTED: { label: "Awaiting Payment", color: "#9A6700", bg: "#FFF6E5" }, CONTRACT_ACTIVE: { label: "Active", color: "#0A6F4D", bg: "#E7F6EE" }, CONTRACT_CLOSED: { label: "Closed", color: "#6B7C8E", bg: "#EEF2F6" }, + // Drawdown-order statuses, mirrored from the order's child booking as it moves + // through the same flow as a one-time booking (clearance → accept → pay → + // allocate). Reused by the order badge on ContractDetailPage. + PENDING: { label: "Pending", color: "#9A6700", bg: "#FFF6E5" }, + AWAITING_DOCUMENTS: { label: "Awaiting Documents", color: "#9A6700", bg: "#FFF6E5" }, + DOCUMENTS_UNDER_REVIEW: { label: "Documents Under Review", color: "#2E5B96", bg: "#EAF1FB" }, + CLEARANCE_READY: { label: "Clearance Ready", color: "#0A6F4D", bg: "#E7F6EE" }, + OPERATION_REQUEST_PENDING: { label: "Operation Review", color: "#9A6700", bg: "#FFF6E5" }, + OPERATION_CHANGES_REQUESTED: { label: "Changes Requested", color: "#9A6700", bg: "#FFF6E5" }, + OPERATION_PRICE_PENDING_CONFIRM: { label: "Confirm New Price", color: "#9A6700", bg: "#FFF6E5" }, + ROAD_DISPATCH_PENDING: { label: "Awaiting Dispatch", color: "#9A6700", bg: "#FFF6E5" }, + SELECTED_FOR_BATCH: { label: "Awaiting Payment", color: "#9A6700", bg: "#FFF6E5" }, + PAID: { label: "Paid", color: "#0A6F4D", bg: "#E7F6EE" }, + IN_TRANSIT: { label: "In Transit", color: "#2E5B96", bg: "#EAF1FB" }, + COMPLETED: { label: "Completed", color: "#0A6F4D", bg: "#E7F6EE" }, EXPIRED: { label: "Expired", color: "#B42318", bg: "#FEECEB" }, CANCELLED: { label: "Cancelled", color: "#B42318", bg: "#FEECEB" }, REJECTED: { label: "Rejected", color: "#B42318", bg: "#FEECEB" },