Files
edr-platform/apps/edr-freight-web/portal/src/pages/contracts/contract-booking-action.ts
Marshal 4b7f6d2548 enhance contract and booking services with server-side search and validation improvements
- Added  parameter to  and  for server-side free-text search on contract reference, company name, and booking details.
- Introduced new validation errors in  for container clashes and space issues when creating bookings.
- Implemented paginated dropdown settings retrieval in .
- Updated  to fetch active yards using a new method that handles pagination.
- Enhanced  with a  method to fetch all records by walking through pages.
- Refactored  to support filtering and pagination in schedule listings.
- Improved  to return a paginated list of facilities.
- Updated UI components in  and  to utilize debounced search inputs for better performance.
- Added alerts in  to inform users about booking constraints related to splits and capacity.
- Enhanced  to display notifications for split bookings and capacity usage.
2026-07-12 10:51:31 +00:00

98 lines
3.5 KiB
TypeScript

import type { Freight } from "@edr/types";
/**
* Booking statuses that no longer occupy the single active-booking slot of a
* ONE_TIME contract. Mirrors the API source of truth in
* `contract-booking.service.ts`.
*/
export const TERMINAL_BOOKING_STATUSES = [
"EXPIRED",
"CANCELLED",
"COMPLETED",
"REJECTED",
];
/** Path A statuses where a customer (no customs) may book against the contract. */
const PATH_A_BOOKABLE = ["FULLY_EXECUTED", "CONTRACT_ACTIVE"];
/** Split bookings in these statuses released their quantity — no remainder to book. */
const RELEASING_BOOKING_STATUSES = ["CANCELLED", "REJECTED", "EXPIRED"];
export type ContractBookingActionKind =
| "book"
| "rebook"
| "request"
| "initiate"
| "none";
export interface ContractBookingAction {
kind: ContractBookingActionKind;
to: string;
/**
* ONE_TIME contract whose booking was split (partial batch payment): the
* customer may book again, but ONLY the whole outstanding remainder — the
* exact quantities come from the contract capacity endpoint.
*/
remainderOnly?: boolean;
}
/**
* The single source of truth for whether a contract row should show a
* Book / Re-book shipment button, and where it should navigate.
*
* - ONE_TIME: book only while there is no active booking. If the previous
* booking expired, offer Re-book. Once a live booking exists, no button.
* - GENERAL: bookable while CONTRACT_ACTIVE (CONTRACT_CLOSED / EXPIRED are
* already excluded by the PATH_A_BOOKABLE gate).
*/
export function getContractBookingAction(
contract: Freight.IContract,
bookings: Freight.IBooking[],
): ContractBookingAction {
// GENERAL + customs: customer submits a shipment request; GL creates the booking.
if (
contract.customsClearingEnabled &&
contract.contractKind === "GENERAL" &&
contract.status === "CONTRACT_ACTIVE"
) {
return {
kind: "request",
to: `/contracts/${contract.id}/shipment-requests/new`,
};
}
// Other customs (ONE_TIME): booked by GL — no customer action.
if (contract.customsClearingEnabled) return { kind: "none", to: "" };
if (!PATH_A_BOOKABLE.includes(contract.status)) return { kind: "none", to: "" };
const to = `/contracts/${contract.id}/bookings/new`;
if (contract.contractKind === "ONE_TIME") {
const mine = bookings.filter((b) => b.contractId === contract.id);
// Split chain: a paid partial split frees the single-booking slot for the
// remainder. The API enforces that the next booking takes the whole
// leftover; once it is booked the contract closes (CONTRACT_CLOSED), which
// the PATH_A_BOOKABLE gate above already excludes.
const hasLiveSplit = mine.some(
(b) => b.isSplit && !RELEASING_BOOKING_STATUSES.includes(b.status),
);
if (hasLiveSplit) return { kind: "book", to, remainderOnly: true };
const hasActive = mine.some(
(b) => !TERMINAL_BOOKING_STATUSES.includes(b.status),
);
if (hasActive) return { kind: "none", to: "" };
const hasExpired = mine.some((b) => b.status === "EXPIRED");
return { kind: hasExpired ? "rebook" : "book", to };
}
// GENERAL non-customs import/export: one-click bare booking instance — the
// per-booking clearance runs first, cargo + shipment day come at completion.
// No window gate here; the window is checked when the booking is completed.
// DOMESTIC (intercity) keeps the direct booking form.
if (contract.tradeDirection !== "DOMESTIC") {
return { kind: "initiate", to: `/contracts/${contract.id}` };
}
return { kind: "book", to };
}