mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 15:48:11 +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. */
|
/** Orders placed against a contract, with their lines and child booking. */
|
||||||
listByContract(contractBookingId: string): Promise<BookingOrder[]> {
|
async listByContract(contractBookingId: string): Promise<BookingOrder[]> {
|
||||||
return this.ordersRepository.findByContract(contractBookingId);
|
const orders = await this.ordersRepository.findByContract(contractBookingId);
|
||||||
|
await Promise.all(orders.map((o) => this.syncOrderFromChild(o)));
|
||||||
|
return orders;
|
||||||
}
|
}
|
||||||
|
|
||||||
findById(id: string): Promise<BookingOrder | null> {
|
async findById(id: string): Promise<BookingOrder | null> {
|
||||||
return this.ordersRepository.findById(id);
|
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,
|
getRouteDirection,
|
||||||
initialBookingFormValues,
|
initialBookingFormValues,
|
||||||
operationToProfileType,
|
operationToProfileType,
|
||||||
|
operationToTradeDirection,
|
||||||
stepFields,
|
stepFields,
|
||||||
type BookingDocuments,
|
type BookingDocuments,
|
||||||
type BookingFormValues,
|
type BookingFormValues,
|
||||||
@@ -271,6 +272,7 @@ export default function NewBookingPage() {
|
|||||||
|
|
||||||
const originYard = form.watch("originYard");
|
const originYard = form.watch("originYard");
|
||||||
const destinationYard = form.watch("destinationYard");
|
const destinationYard = form.watch("destinationYard");
|
||||||
|
const operationType = form.watch("operationType");
|
||||||
|
|
||||||
// The estimated shipment date lives in the Route step now; for general
|
// 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
|
// 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,
|
(y) => y.id === destinationYard,
|
||||||
);
|
);
|
||||||
|
|
||||||
const route = getRouteDirection(origin, destination);
|
// Country-based derivation is authoritative when both yards are tagged, but
|
||||||
return route;
|
// returns null if a yard is unselected or lacks a country (e.g. intercity
|
||||||
}, [originYard, destinationYard]);
|
// 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
|
// The company's onboarded profile types — drives which operations are offered
|
||||||
// and which profile each operation stamps the booking to.
|
// 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" },
|
FULLY_EXECUTED: { label: "Awaiting Payment", color: "#9A6700", bg: "#FFF6E5" },
|
||||||
CONTRACT_ACTIVE: { label: "Active", color: "#0A6F4D", bg: "#E7F6EE" },
|
CONTRACT_ACTIVE: { label: "Active", color: "#0A6F4D", bg: "#E7F6EE" },
|
||||||
CONTRACT_CLOSED: { label: "Closed", color: "#6B7C8E", bg: "#EEF2F6" },
|
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" },
|
EXPIRED: { label: "Expired", color: "#B42318", bg: "#FEECEB" },
|
||||||
CANCELLED: { label: "Cancelled", color: "#B42318", bg: "#FEECEB" },
|
CANCELLED: { label: "Cancelled", color: "#B42318", bg: "#FEECEB" },
|
||||||
REJECTED: { label: "Rejected", color: "#B42318", bg: "#FEECEB" },
|
REJECTED: { label: "Rejected", color: "#B42318", bg: "#FEECEB" },
|
||||||
|
|||||||
Reference in New Issue
Block a user