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 interface MileVehicleSummary { plate: string | null; code: string | null; driverName: string | null; containerNumber: string | null; distanceKm: number | null; } export interface MileLegSummary { status: string; exactKm: number | null; remainingPayment: number | null; currency: string; invoiced: boolean; vehicles: MileVehicleSummary[]; } export interface MileSummaryResponse { firstMile: MileLegSummary | null; lastMile: MileLegSummary | null; } 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; /** Computed line total (unitAmount × quantity). */ amount: number; /** Price for a single unit of this charge (e.g. one 20ft container, one ton). */ unitAmount?: number; /** Unit the rate is charged per: PER_CONTAINER | PER_TON | PER_WAGON | PER_KM | FLAT. */ unit?: string; /** How many units this charge applies to (containers, tons, wagons; 1 for FLAT). */ quantity?: number; currency: string; } /** * An announced upcoming/open booking window, shown to every signed-in customer * regardless of contract. Import trains open a window on one booking day; * export trains open 24h before departure (first come, first served). */ export interface MyBookingWindow { scheduleId: string; /** Train schedule reference (e.g. TS-2026-000123), shown on the window card. */ reference: string | null; /** * The customer's active contract on this lane, when they hold one — enables * "Book now" to target it. Null for lanes they have no contract on. */ contractId: string | null; /** ONE_TIME contracts can't draw down against a window — button is hidden. */ contractKind: "ONE_TIME" | "GENERAL" | null; direction: "IMPORT" | "EXPORT" | null; windowPhase: string | null; isOpenNow: boolean; windowOpensAt: string | null; windowClosesAt: string | null; docReviewEndsAt: string | null; paymentPhaseEndsAt: string | null; bookingWindowStatus: string; bookingCycleNo: number; departureDate: string; origin: string | null; destination: string | null; /** * Full ordered corridor for the window's route — origin, every intermediate * milestone stop, then destination (e.g. Djibouti → Adama → Dire Dawa). * Falls back to [origin, destination] when the route has no milestones. */ routeStations: 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; /** Sudo-mode OTP challenge; required when role=CUSTOMER. */ otp?: string; } export interface ApproveDeliveryResponse { bookingId: string; inventoryId: string; approvedAt: string; signerDisplayName: string; } /** One import handover record — booking-level or per truck (EDR last-mile). */ export interface BookingHandoverRecord { id: string; reference: string; truckPlate: string | null; mileType: "SELF_HAUL" | "EDR_LAST_MILE"; generatedAt: string; signedAt: string | null; signerName: string | null; deliveredAt: string | null; } export interface SignHandoverResponse { handoverId: string; bookingId: string; signedAt: string | null; signerDisplayName: string; allSigned: boolean; } export interface CustomerTruckAssignmentPayload { truckPlateNumber: string; driverName: string; truckType: string; containerNumberToLoad: string; } export interface BookingListFilter { status?: string; /** Comma-separated statuses (overrides `status` when set). */ statuses?: string; /** ONE_TIME or GENERAL_CONTRACT. */ bookingType?: string; /** CONTAINER or BULK. */ freightType?: string; /** IMPORT / EXPORT / DOMESTIC. */ tradeDirection?: string; /** Narrow to a single operational profile (importer/exporter/freight_forwarder). */ companyProfileId?: string; /** Created-date range (ISO). */ createdFrom?: string; createdTo?: string; /** Free-text search: booking reference, company name, contract reference (server-side). */ search?: string; page?: number; pageSize?: number; sortBy?: string; sortOrder?: "ASC" | "DESC"; } export const bookingsService = { list: async ( filter: BookingListFilter | void = {}, ): Promise> => { const { data } = await client.get("/api/bookings", { params: filter }); return data.data; }, get: async (id: string): Promise => { const { data } = await client.get(`/api/bookings/${id}`); return data.data; }, mileSummary: async (id: string): Promise => { const { data } = await client.get(`/api/bookings/${id}/mile-summary`); return data.data; }, assignCustomerTruck: async ( id: string, payload: CustomerTruckAssignmentPayload, ): Promise => { const { data } = await client.post( `/api/bookings/${id}/customer-truck-assignment`, payload, ); return data.data; }, downloadCustomerTruckFreightOrder: async (id: string, copies?: number[]): Promise => { const { data } = await client.get( `/api/bookings/${id}/customer-truck-assignment/freight-order`, { responseType: "blob", params: copies?.length ? { copies: copies.join(",") } : undefined }, ); return data; }, downloadHandoverDocument: async (inventoryId: string): Promise => { const { data } = await client.get( `/api/warehouse-inventory/${inventoryId}/handover-document`, { responseType: "blob" }, ); return data; }, downloadBookingHandoverDocument: async ( bookingId: string, handoverId?: string, ): Promise => { const { data } = await client.get( `/api/warehouse-inventory/bookings/${bookingId}/handover-document`, { responseType: "blob", params: handoverId ? { handoverId } : undefined }, ); return data; }, listBookingHandovers: async ( bookingId: string, ): Promise => { const { data } = await client.get( `/api/warehouse-inventory/bookings/${bookingId}/handovers`, ); return data.data ?? data; }, signHandover: async ( handoverId: string, signerName: string, ): Promise => { const { data } = await client.post( `/api/warehouse-inventory/handovers/${handoverId}/sign`, { signerName }, ); return data.data ?? data; }, downloadBookingGrnDocument: async (bookingId: string): Promise => { const { data } = await client.get( `/api/warehouse-inventory/bookings/${bookingId}/grn-document`, { responseType: "blob" }, ); return data; }, downloadBookingReleaseDocument: async (bookingId: string): Promise => { const { data } = await client.get( `/api/warehouse-inventory/bookings/${bookingId}/release-document`, { responseType: "blob" }, ); return data; }, tracking: async (id: string): Promise => { const { data } = await client.get(`/api/bookings/${id}/tracking`); return data.data; }, create: async ( payload: CreateBookingPayload, documents?: BookingDocuments, ): Promise => { 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 => { const { data } = await client.get("/api/bookings/reference-data"); return data.data; }, update: async ( id: string, payload: Partial, 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 => { await client.delete(`/api/bookings/${id}`); }, cancel: async (id: string, reason: string): Promise => { const { data } = await client.post(`/api/bookings/${id}/cancel`, { reason }); return data.data; }, reject: async (id: string, reason?: string): Promise => { const { data } = await client.post(`/api/bookings/${id}/reject`, { reason }); return data.data; }, generatePrice: async (id: string): Promise => { const { data } = await client.post(`/api/bookings/${id}/generate-price`); return data.data; }, submit: async (id: string): Promise => { const { data } = await client.post(`/api/bookings/${id}/submit`); return data.data; }, confirmSubmit: async (id: string): Promise => { const { data } = await client.post(`/api/bookings/${id}/confirm-submit`); return data.data; }, uploadDocuments: async ( id: string, files: Record, ): Promise => { 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; }, // ── Document clearance ── getClearance: async (id: string): Promise => { const { data } = await client.get(`/api/bookings/${id}/clearance`); return data.data ?? data; }, submitClearanceDocuments: async ( id: string, files: Record, ): Promise => { const formData = new FormData(); for (const [key, file] of Object.entries(files)) { if (file) formData.append(key, file); } const { data } = await client.post( `/api/bookings/${id}/clearance/documents`, formData, { headers: { "Content-Type": "multipart/form-data" } }, ); return data.data; }, proceedToOperation: async ( id: string, scheduledDate: string, ): Promise => { const { data } = await client.post( `/api/bookings/${id}/clearance/proceed`, { scheduledDate }, ); return data.data; }, uploadBookingClearanceDutySlip: async ( id: string, file: File, ): Promise => { const form = new FormData(); form.append("file", file); const { data } = await client.post(`/api/bookings/${id}/clearance/duty-slip`, form, { headers: { "Content-Type": "multipart/form-data" }, }); return data.data ?? data; }, getContractView: async (id: string): Promise => { const { data } = await client.get(B.CONTRACT_VIEW(id)); return data.data ?? data; }, downloadContractDocument: async (id: string): Promise => { 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 => { const { data } = await client.post(B.CONTRACT_SIGN(id), payload); return data.data ?? data; }, approveDelivery: async ( id: string, signerName: string, ): Promise => { const { data } = await client.post( `/api/warehouse-inventory/bookings/${id}/approve-delivery`, { signerName }, ); return data.data ?? data; }, getBookableSchedules: async ( query: Freight.BookableSchedulesQuery = {}, ): Promise => { 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 => { const { data } = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.AVAILABLE_DAYS, { params: query }, ); return (data.data as Freight.AvailableDaysResponse).days; }, // Cargo-aware day pool: only days where a train has remaining capacity AND a // wagon TYPE that can carry this cargo. `containers`/`containerTypeIds` are // serialized as JSON string params (the server parses them). Days only — no // capacity counts are ever returned. getAvailableDaysForCargo: async ( query: Freight.AvailableDaysForCargoQuery, ): Promise => { const { containers, containerTypeIds, ...rest } = query; const { data } = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.AVAILABLE_DAYS_FOR_CARGO, { params: { ...rest, ...(containers ? { containers: JSON.stringify(containers) } : {}), ...(containerTypeIds?.length ? { containerTypeIds: JSON.stringify(containerTypeIds) } : {}), }, }, ); return (data.data as Freight.AvailableDaysResponse).days; }, // Days bookable for an EXISTING booking (operation-request step): the server // derives the cargo from the booking and applies the wagon-type gate. getAvailableDaysForBooking: async (bookingId: string): Promise => { const { data } = await client.get( `/api/bookings/${bookingId}/available-days`, ); return (data.data as Freight.AvailableDaysResponse).days; }, // Advisory free-wagon count for a shipment day (planning hint, not enforced). getDayAvailability: async ( bookingId: string, date: string, ): Promise => { const { data } = await client.get( `/api/bookings/${bookingId}/day-availability`, { params: { date } }, ); return data.data as Freight.DayAvailabilityResponse; }, /** * Upcoming/open booking windows on the signed-in customer's active-contract * lanes (import booking-day windows + export 24h pre-departure windows). */ getMyBookingWindows: async (): Promise => { const { data } = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.MY_BOOKING_WINDOWS, ); return data.data ?? data; }, /** * Booking windows for a single contract's routes (same row shape as * `getMyBookingWindows`). Used to gate the direct "New shipment booking" * entry on the contract detail page and the new-shipment form. */ getContractBookingWindows: async ( contractId: string, ): Promise => { const { data } = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.CONTRACT_BOOKING_WINDOWS(contractId), ); return data.data ?? data; }, };