import { api as client } from "../auth/http"; import { unwrap } from "@/utils/endpoint"; import { URL_CONSTANTS } from "@/constants/URLS"; import type { BookingDetail } from "@/types/booking"; const B = URL_CONSTANTS.BOOKINGS; export interface BookingListFilter { status?: string; /** Comma-separated statuses for grouped tabs */ statuses?: string; schedulingStatuses?: string; assignedToSchedule?: "true" | "false"; /** Tab key for React Query cache (not sent to API) */ tab?: string; // customerId?: string; companyId?: string; freightType?: string; tradeDirection?: string; paymentCurrency?: string; page?: number; pageSize?: number; sortBy?: string; sortOrder?: "ASC" | "DESC"; } export interface PaginatedBookings { items: BookingDetail[]; total: number; } export interface BookingListSummaryMetrics { inQueue: number; onThisPage: number; needsAction: number; urgent: number; } export interface BookingListSummaryTabs { all: number; intake: number; in_approval: number; approved_contract: number; payment: number; operations: number; completed: number; closed: number; } export interface BookingListSummary { metrics: BookingListSummaryMetrics; tabs: BookingListSummaryTabs; } export interface ApproveStepPayload { id: string; stepId: string; requiredRole: string; } export interface RejectStepPayload { id: string; stepId: string; reason: string; } 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 ConsolidationWagonSlot { containerTypeCode: string; quantity: number; containersPerWagon: number; remainder: number; slotsNeeded: number; } export interface ConsolidationDetails { statusMessage: string; wagonSlots: ConsolidationWagonSlot[]; partner: { id: string; reference: string } | null; splitBilling: { bookingShare: number; partnerShare: number } | null; } export interface SignContractPayload { role: "CUSTOMER" | "STAFF"; signatureImageBase64: string; signerDisplayName: string; consentText?: string; } async function postBooking(url: string, body?: unknown): Promise { const response = await client.post(url, body ?? {}); return unwrap(response.data); } export const bookingsService = { getListSummary: async (filter?: BookingListFilter): Promise => { const params: Record = {}; if (filter) { if (filter.statuses) params.statuses = filter.statuses; else if (filter.status) params.status = filter.status; if (filter.page != null) params.page = filter.page; if (filter.pageSize != null) params.pageSize = filter.pageSize; if (filter.companyId) params.companyId = filter.companyId; if (filter.freightType) params.freightType = filter.freightType; if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection; if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency; } const response = await client.get(B.LIST_SUMMARY, { params, }); return unwrap(response.data) as BookingListSummary; }, list: async (filter?: BookingListFilter): Promise => { const params: Record = {}; if (filter) { if (filter.statuses) params.statuses = filter.statuses; else if (filter.status) params.status = filter.status; // filter.tab is intentionally omitted from API params if (filter.page != null) params.page = filter.page; if (filter.pageSize != null) params.pageSize = filter.pageSize; if (filter.sortBy) params.sortBy = filter.sortBy; if (filter.sortOrder) params.sortOrder = filter.sortOrder; if (filter.schedulingStatuses) params.schedulingStatuses = filter.schedulingStatuses; if (filter.assignedToSchedule) params.assignedToSchedule = filter.assignedToSchedule; if (filter.companyId) params.companyId = filter.companyId; if (filter.freightType) params.freightType = filter.freightType; if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection; if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency; } const response = await client.get(B.BASE, { params, }); const data = unwrap(response.data); return { items: (data.items ?? []) as BookingDetail[], total: data.total ?? 0, }; }, getById: async (id: string): Promise => { const response = await client.get(B.BY_ID(id)); return unwrap(response.data) as BookingDetail; }, remove: async (id: string): Promise => { await client.delete(B.BY_ID(id)); }, staffAccept: (id: string) => postBooking(B.STAFF_ACCEPT(id)), requestChanges: (id: string, note: string) => postBooking(B.STAFF_REQUEST_CHANGES(id), { note }), staffReject: (id: string, reason: string) => postBooking(B.STAFF_REJECT(id), { reason }), approveStep: ({ id, stepId, requiredRole }: ApproveStepPayload) => postBooking(B.APPROVE_STEP(id, stepId), { requiredRole }), rejectStep: ({ id, stepId, reason }: RejectStepPayload) => postBooking(B.REJECT_STEP(id, stepId), { reason }), generateContract: (id: string) => postBooking(B.CONTRACT_GENERATE(id)), getContractView: async (id: string): Promise => { const response = await client.get(B.CONTRACT_VIEW(id)); return unwrap(response.data) as ContractView; }, downloadContract: async (id: string): Promise => { const response = await client.get(B.CONTRACT_DOWNLOAD(id), { responseType: "blob", }); return ensurePdfBlob(response.data as Blob); }, downloadContractDocument: async (id: string): Promise => { const response = await client.get(B.CONTRACT_DOCUMENT(id), { responseType: "blob", }); return ensurePdfBlob(response.data as Blob); }, signContract: (id: string, payload: SignContractPayload) => postBooking(B.CONTRACT_SIGN(id), payload), getSummary: async (id: string): Promise<{ summary: string }> => { const response = await client.get<{ summary: string }>(B.SUMMARY(id)); return unwrap(response.data); }, getConsolidationDetails: async ( id: string, ): Promise => { const response = await client.get(B.CONSOLIDATION(id)); return unwrap(response.data) as ConsolidationDetails; }, customerSign: (id: string, payload: SignContractPayload) => postBooking(B.CUSTOMER_SIGN(id), { ...payload, role: "CUSTOMER", }), marketingApprove: (id: string, payload: SignContractPayload) => postBooking(B.MARKETING_APPROVE(id), { ...payload, role: "STAFF", }), payBooking: (id: string) => postBooking(B.PAYMENT_PAY(id)), startTransit: (id: string) => postBooking(B.START_TRANSIT(id)), complete: (id: string) => postBooking(B.COMPLETE(id)), cancel: (id: string, reason: string) => postBooking(B.CANCEL(id), { reason }), create: async (payload: Record): Promise => { const response = await client.post<{ booking: BookingDetail } | BookingDetail>( B.BASE, payload, ); const data = unwrap(response.data) as { booking?: BookingDetail }; return (data.booking ?? data) as BookingDetail; }, getReferenceData: async () => { const response = await client.get(B.REFERENCE_DATA); return unwrap(response.data); }, governmentExpedite: (id: string) => postBooking(B.GOVERNMENT_EXPEDITE(id)), }; async function ensurePdfBlob(blob: Blob): Promise { if (blob.type.includes("application/json")) { const body = JSON.parse(await blob.text()) as { message?: string }; throw new Error(body.message ?? "Contract PDF download failed"); } return blob; }