mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 11:55:42 +00:00
- Added `unplaced` method in `BookingNotifierService` to log warnings for bookings that cannot be placed on any train. - Introduced `getAvailableDays` method in `TrainSchedulingService` to retrieve distinct days with open departures for a given route. - Created `AvailableDaysQueryDto` for querying available days based on origin and destination yards. - Updated `TrainSchedulingController` to expose an endpoint for available days. - Modified frontend components to support day-level booking, allowing customers to select only a day without pinning to a specific train. - Removed references to train schedules in booking forms and review steps, emphasizing day selection. - Added a database migration to create an index for efficient querying of bookings by route and day.
213 lines
6.1 KiB
TypeScript
213 lines
6.1 KiB
TypeScript
import type { Freight, PaginatedResponse } from "@edr/types";
|
|
|
|
import { URL_CONSTANTS } from "@/constants/URLS";
|
|
import type { BookingDocuments } from "@/pages/bookings/new-booking-form/schema";
|
|
import { buildBookingFormData } from "./booking-form-data";
|
|
import { client } from "../utils/api";
|
|
|
|
const B = URL_CONSTANTS.BOOKINGS;
|
|
|
|
export type CreateBookingPayload = Freight.CreateBookingDto;
|
|
|
|
export interface ContractView {
|
|
bookingId: string;
|
|
reference: string;
|
|
status: string;
|
|
templateKey: string;
|
|
title: string;
|
|
html: string;
|
|
canSignCustomer: boolean;
|
|
canSignStaff: boolean;
|
|
hasContractDocument: boolean;
|
|
signatures: Array<{
|
|
role: string;
|
|
signerDisplayName: string;
|
|
signedAt: string;
|
|
signatureImageUrl?: string | null;
|
|
}>;
|
|
/** Current viewer's reusable saved signature, if they have one. */
|
|
savedSignature?: {
|
|
signerDisplayName: string;
|
|
signatureImageUrl?: string | null;
|
|
} | null;
|
|
}
|
|
|
|
export interface PriceLineItem {
|
|
code: string;
|
|
description: string;
|
|
amount: number;
|
|
currency: string;
|
|
}
|
|
|
|
export interface GeneratePriceResponse {
|
|
bookingId: string;
|
|
totalAmount: number;
|
|
currency: string;
|
|
lineItems: PriceLineItem[];
|
|
warnings: string[];
|
|
}
|
|
|
|
export interface SubmitBookingResponse {
|
|
bookingId: string;
|
|
status: string;
|
|
priceChanged: boolean;
|
|
previousTotalAmount?: number;
|
|
totalAmount: number;
|
|
currency: string;
|
|
lineItems?: PriceLineItem[];
|
|
message?: string;
|
|
}
|
|
|
|
export interface SignContractPayload {
|
|
role: "CUSTOMER" | "STAFF";
|
|
signatureImageBase64: string;
|
|
signerDisplayName: string;
|
|
consentText?: string;
|
|
}
|
|
|
|
export interface BookingListFilter {
|
|
status?: string;
|
|
/** Comma-separated statuses (overrides `status` when set). */
|
|
statuses?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
sortBy?: string;
|
|
sortOrder?: "ASC" | "DESC";
|
|
}
|
|
|
|
export const bookingsService = {
|
|
list: async (
|
|
filter: BookingListFilter | void = {},
|
|
): Promise<PaginatedResponse<Freight.IBooking>> => {
|
|
const { data } = await client.get("/api/bookings", { params: filter });
|
|
return data.data;
|
|
},
|
|
get: async (id: string): Promise<Freight.IBooking> => {
|
|
const { data } = await client.get(`/api/bookings/${id}`);
|
|
return data.data;
|
|
},
|
|
tracking: async (id: string): Promise<Freight.IBookingTracking> => {
|
|
const { data } = await client.get(`/api/bookings/${id}/tracking`);
|
|
return data.data;
|
|
},
|
|
create: async (
|
|
payload: CreateBookingPayload,
|
|
documents?: BookingDocuments,
|
|
): Promise<Freight.IBooking> => {
|
|
const formData = buildBookingFormData(payload, documents);
|
|
const { data } = await client.post("/api/bookings", formData, {
|
|
headers: { "Content-Type": "multipart/form-data" },
|
|
});
|
|
return data.data.booking;
|
|
},
|
|
getReferenceData: async (): Promise<Freight.BookingReferenceData> => {
|
|
const { data } = await client.get("/api/bookings/reference-data");
|
|
return data.data;
|
|
},
|
|
update: async (
|
|
id: string,
|
|
payload: Partial<CreateBookingPayload>,
|
|
documents?: BookingDocuments,
|
|
): Promise<{ booking: Freight.IBooking; warnings: string[] }> => {
|
|
const formData = buildBookingFormData(payload, documents);
|
|
const { data } = await client.patch(`/api/bookings/${id}`, formData, {
|
|
headers: { "Content-Type": "multipart/form-data" },
|
|
});
|
|
return data.data;
|
|
},
|
|
|
|
remove: async (id: string): Promise<void> => {
|
|
await client.delete(`/api/bookings/${id}`);
|
|
},
|
|
|
|
cancel: async (id: string, reason: string): Promise<Freight.IBooking> => {
|
|
const { data } = await client.post(`/api/bookings/${id}/cancel`, { reason });
|
|
return data.data;
|
|
},
|
|
|
|
generatePrice: async (id: string): Promise<GeneratePriceResponse> => {
|
|
const { data } = await client.post(`/api/bookings/${id}/generate-price`);
|
|
return data.data;
|
|
},
|
|
|
|
submit: async (id: string): Promise<SubmitBookingResponse> => {
|
|
const { data } = await client.post(`/api/bookings/${id}/submit`);
|
|
return data.data;
|
|
},
|
|
|
|
confirmSubmit: async (id: string): Promise<SubmitBookingResponse> => {
|
|
const { data } = await client.post(`/api/bookings/${id}/confirm-submit`);
|
|
return data.data;
|
|
},
|
|
|
|
uploadDocuments: async (
|
|
id: string,
|
|
files: Record<string, File | File[] | null>,
|
|
): Promise<Freight.IBooking> => {
|
|
const formData = new FormData();
|
|
for (const [key, fileOrFiles] of Object.entries(files)) {
|
|
if (!fileOrFiles) continue;
|
|
if (Array.isArray(fileOrFiles)) {
|
|
for (const f of fileOrFiles) formData.append(key, f);
|
|
} else {
|
|
formData.append(key, fileOrFiles);
|
|
}
|
|
}
|
|
const { data } = await client.post(
|
|
`/api/bookings/${id}/documents`,
|
|
formData,
|
|
{ headers: { "Content-Type": "multipart/form-data" } },
|
|
);
|
|
return data.data;
|
|
},
|
|
|
|
getContractView: async (id: string): Promise<ContractView> => {
|
|
const { data } = await client.get(B.CONTRACT_VIEW(id));
|
|
return data.data ?? data;
|
|
},
|
|
|
|
downloadContractDocument: async (id: string): Promise<Blob> => {
|
|
const { data } = await client.get(B.CONTRACT_DOCUMENT(id), {
|
|
responseType: "blob",
|
|
});
|
|
return data;
|
|
},
|
|
|
|
checkPayment: async (orderId: string): Promise<{ status: string }> => {
|
|
const { data } = await client.post(`/api/payments/bookings/check-payment/${orderId}`);
|
|
return data.data ?? data;
|
|
},
|
|
|
|
signContract: async (
|
|
id: string,
|
|
payload: SignContractPayload,
|
|
): Promise<Freight.IBooking> => {
|
|
const { data } = await client.post(B.CONTRACT_SIGN(id), payload);
|
|
return data.data ?? data;
|
|
},
|
|
|
|
getBookableSchedules: async (
|
|
query: Freight.BookableSchedulesQuery = {},
|
|
): Promise<Freight.BookableScheduleItem[]> => {
|
|
const { data } = await client.get(
|
|
URL_CONSTANTS.TRAIN_SCHEDULING.BOOKABLE_SCHEDULES,
|
|
{ params: query },
|
|
);
|
|
return data.data;
|
|
},
|
|
|
|
/**
|
|
* Day-level pool: the days that have a departure on the route. The customer
|
|
* picks a day; the engine assigns the train. No capacity is returned.
|
|
*/
|
|
getAvailableDays: async (
|
|
query: Freight.AvailableDaysQuery = {},
|
|
): Promise<string[]> => {
|
|
const { data } = await client.get(
|
|
URL_CONSTANTS.TRAIN_SCHEDULING.AVAILABLE_DAYS,
|
|
{ params: query },
|
|
);
|
|
return (data.data as Freight.AvailableDaysResponse).days;
|
|
},
|
|
};
|