mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat: synchronize order status with child booking and enhance contract status labels
This commit is contained in:
@@ -41,12 +41,54 @@ export class BookingOrdersService {
|
||||
) {}
|
||||
|
||||
/** Orders placed against a contract, with their lines and child booking. */
|
||||
listByContract(contractBookingId: string): Promise<BookingOrder[]> {
|
||||
return this.ordersRepository.findByContract(contractBookingId);
|
||||
async listByContract(contractBookingId: string): Promise<BookingOrder[]> {
|
||||
const orders = await this.ordersRepository.findByContract(contractBookingId);
|
||||
await Promise.all(orders.map((o) => this.syncOrderFromChild(o)));
|
||||
return orders;
|
||||
}
|
||||
|
||||
findById(id: string): Promise<BookingOrder | null> {
|
||||
return this.ordersRepository.findById(id);
|
||||
async findById(id: string): Promise<BookingOrder | null> {
|
||||
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<void> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user