add booking request functionality for GENERAL customs contracts

- Create migration for booking_requests table with necessary fields and indexes.
- Implement BookingRequestRepository for database operations related to booking requests.
- Develop BookingRequestService to handle business logic for submitting, accepting, rejecting, and canceling booking requests.
- Create DTOs for creating booking requests and reviewing them.
- Define BookingRequest entity to map to the booking_requests table.
- Add UI components for managing shipment requests, including detail and list pages.
- Implement OperationDatePicker component for selecting available shipment days.
This commit is contained in:
Marshal
2026-06-29 09:30:44 +00:00
parent aeb5e0046e
commit 0f7cac2b68
46 changed files with 2665 additions and 992 deletions

View File

@@ -284,6 +284,32 @@ export const api = {
],
),
availableDaysForCargo: endpoint<
{
originYardId?: string;
destinationYardId?: string;
freightType: "CONTAINER" | "BULK";
cargoTypeCode?: string;
totalWeightTons?: number;
containers?: { containerSize: string; quantity: number }[];
},
string[]
>(
"train-scheduling",
"available-days-for-cargo",
(input) => trainSchedulingService.getAvailableDaysForCargo(input),
(input) => [
...QUERY_KEYS.TRAIN_SCHEDULING.ROOT,
"available-days-for-cargo",
input.originYardId ?? "",
input.destinationYardId ?? "",
input.freightType,
input.cargoTypeCode ?? "",
input.totalWeightTons ?? 0,
JSON.stringify(input.containers ?? []),
],
),
trainTrack: endpoint<{ id: string }, TrainTrackResponse>(
"train-scheduling",
"track",
@@ -1838,13 +1864,12 @@ export const api = {
reviewOperation: endpoint<
{
id: string;
decision: "ACCEPT" | "REQUEST_CHANGES" | "ADJUST_PRICE";
decision: "ACCEPT" | "REQUEST_CHANGES";
note?: string;
amount?: number;
},
BookingDetail
>("bookings", "reviewOperation", ({ id, decision, note, amount }) =>
bookingsService.reviewOperation(id, decision, { note, amount }),
>("bookings", "reviewOperation", ({ id, decision, note }) =>
bookingsService.reviewOperation(id, decision, { note }),
),
approveStep: endpoint<ApproveStepPayload, BookingDetail>(

View File

@@ -212,21 +212,14 @@ export const bookingsService = {
/** Marketing/operations review of a drawdown order's operation request. */
reviewOperation: (
id: string,
decision: "ACCEPT" | "REQUEST_CHANGES" | "ADJUST_PRICE",
options: { note?: string; amount?: number } = {},
decision: "ACCEPT" | "REQUEST_CHANGES",
options: { note?: string } = {},
) =>
postBooking<BookingDetail>(`/bookings/${id}/operation/review`, {
decision,
...options,
}),
/** Adjust a booking's total price (pass null amount to clear the adjustment). */
adjustPrice: (id: string, amount: number | null, reason?: string) =>
postBooking<BookingDetail>(`/bookings/${id}/adjust-price`, {
amount,
reason,
}),
approveStep: ({ id, stepId, requiredRole }: ApproveStepPayload) =>
postBooking<BookingDetail>(B.APPROVE_STEP(id, stepId), { requiredRole }),

View File

@@ -244,6 +244,37 @@ export const contractsService = {
return (unwrap(response.data) ?? []) as Freight.ContractCapacityLine[];
},
// ── Shipment requests (GENERAL + customs) ──
/** GL queue of pending shipment requests across contracts. */
getBookingRequestQueue: async (): Promise<Freight.IBookingRequest[]> => {
const response = await client.get(C.BOOKING_REQUEST_QUEUE);
return (unwrap(response.data) ?? []) as Freight.IBookingRequest[];
},
listBookingRequests: async (
id: string,
): Promise<Freight.IBookingRequest[]> => {
const response = await client.get(C.BOOKING_REQUESTS(id));
return (unwrap(response.data) ?? []) as Freight.IBookingRequest[];
},
getBookingRequest: async (
reqId: string,
): Promise<Freight.IBookingRequest> => {
const response = await client.get(C.BOOKING_REQUEST_BY_ID(reqId));
return unwrap(response.data) as Freight.IBookingRequest;
},
acceptBookingRequest: (reqId: string, bookingId: string) =>
postContract<Freight.IBookingRequest>(C.BOOKING_REQUEST_ACCEPT(reqId), {
bookingId,
}),
rejectBookingRequest: (reqId: string, note?: string) =>
postContract<Freight.IBookingRequest>(C.BOOKING_REQUEST_REJECT(reqId), {
note,
}),
// ── Clearance milestones ──
listMilestonesForContract: async (
id: string,

View File

@@ -1,3 +1,4 @@
import type { Freight } from "@edr/types";
import { api as client } from "../auth/http";
import { unwrap } from "@/utils/endpoint";
import { URL_CONSTANTS } from "@/constants/URLS";
@@ -127,6 +128,24 @@ export const trainSchedulingService = {
return unwrap(response.data).days;
},
// Cargo-aware day pool (matching wagons + open train capacity). `containers`
// is serialized as a JSON string param (the server parses it).
getAvailableDaysForCargo: async (
query: Freight.AvailableDaysForCargoQuery,
): Promise<string[]> => {
const { containers, ...rest } = query;
const response = await client.get<{ days: string[] }>(
URL_CONSTANTS.TRAIN_SCHEDULING.AVAILABLE_DAYS_FOR_CARGO,
{
params: {
...rest,
...(containers ? { containers: JSON.stringify(containers) } : {}),
},
},
);
return unwrap(response.data).days;
},
runBatch: async (scheduleId: string): Promise<BatchBoardScheduleDetail> => {
const response = await client.post<BatchBoardScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.RUN_BATCH(scheduleId),