implement booking flow

This commit is contained in:
marshal
2026-06-04 15:16:27 +03:00
parent a5578ce714
commit 125ee18308
89 changed files with 6190 additions and 1724 deletions

View File

@@ -1,3 +1,4 @@
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
import { endpoint } from "@/utils/endpoint";
import type {
CreateFileUploadFieldDto,
@@ -16,7 +17,6 @@ import {
UpdateDropdownSettingDto,
} from "@/types/dropdownSettings";
import {
ApproveRatePayload,
RuleEngineListResult,
RuleEngineRecord,
RuleEngineResourceSlug,
@@ -27,8 +27,14 @@ import {
ruleEngineService,
RuleEngineListParams,
} from "./ruleEngine/ruleEngine.service";
import { bookingsService, BookingListFilter } from "./bookings.service";
import type { Freight, PaginatedResponse } from "@edr/types";
import {
bookingsService,
BookingListFilter,
type ApproveStepPayload,
type PaginatedBookings,
type RejectStepPayload,
} from "./bookings.service";
import type { BookingDetail } from "@/types/booking";
export const api = {
fileUploadSettings: {
@@ -167,15 +173,21 @@ export const api = {
list: endpoint<
{ resource: RuleEngineResourceSlug; params?: RuleEngineListParams },
RuleEngineListResult<RuleEngineRecord>
>("rule-engine", "list", ({ resource, params }) =>
ruleEngineService.list(resource, params),
>(
"rule-engine",
"list",
({ resource, params }) => ruleEngineService.list(resource, params),
({ resource, params }) => QUERY_KEYS.RULE_ENGINE.list(resource, params),
),
getById: endpoint<
{ resource: RuleEngineResourceSlug; id: string },
RuleEngineRecord
>("rule-engine", "getById", ({ resource, id }) =>
ruleEngineService.getById(resource, id),
>(
"rule-engine",
"getById",
({ resource, id }) => ruleEngineService.getById(resource, id),
({ resource, id }) => QUERY_KEYS.RULE_ENGINE.detail(resource, id),
),
create: endpoint<
@@ -209,37 +221,33 @@ export const api = {
({ id }) => ruleEngineService.submitRate(id),
),
approveRate: endpoint<
{ id: string; payload: ApproveRatePayload },
RuleEngineRecord
>("rule-engine", "approveRate", ({ id, payload }) =>
ruleEngineService.approveRate(id, payload),
approveRate: endpoint<{ id: string }, RuleEngineRecord>(
"rule-engine",
"approveRate",
({ id }) => ruleEngineService.approveRate(id),
),
getApprovalChain: endpoint<void, RuleEngineRecord[]>(
"rule-engine",
"getApprovalChain",
() => ruleEngineService.getApprovalChain(),
() => QUERY_KEYS.RULE_ENGINE.chain,
),
},
bookings: {
list: endpoint<
{ filter?: BookingListFilter },
PaginatedResponse<Freight.IBooking>
>("bookings", "list", ({ filter }) => bookingsService.list(filter)),
list: endpoint<{ filter?: BookingListFilter }, PaginatedBookings>(
"bookings",
"list",
({ filter }) => bookingsService.list(filter),
({ filter }) => QUERY_KEYS.BOOKINGS.list(filter),
),
getById: endpoint<{ id: string }, Freight.IBooking>(
getById: endpoint<{ id: string }, BookingDetail>(
"bookings",
"getById",
({ id }) => bookingsService.getById(id),
),
updateStatus: endpoint<
{ id: string; action: string; reason?: string },
Freight.IBooking
>("bookings", "updateStatus", ({ id, action, reason }) =>
bookingsService.updateStatus(id, { action, reason }),
({ id }) => QUERY_KEYS.BOOKINGS.byId(id),
),
remove: endpoint<{ id: string }, void>(
@@ -247,5 +255,84 @@ export const api = {
"remove",
({ id }) => bookingsService.remove(id),
),
staffAccept: endpoint<{ id: string }, BookingDetail>(
"bookings",
"staffAccept",
({ id }) => bookingsService.staffAccept(id),
),
requestChanges: endpoint<{ id: string; note: string }, BookingDetail>(
"bookings",
"requestChanges",
({ id, note }) => bookingsService.requestChanges(id, note),
),
staffReject: endpoint<{ id: string; reason: string }, BookingDetail>(
"bookings",
"staffReject",
({ id, reason }) => bookingsService.staffReject(id, reason),
),
approveStep: endpoint<ApproveStepPayload, BookingDetail>(
"bookings",
"approveStep",
(payload) => bookingsService.approveStep(payload),
),
rejectStep: endpoint<RejectStepPayload, BookingDetail>(
"bookings",
"rejectStep",
(payload) => bookingsService.rejectStep(payload),
),
generateContract: endpoint<{ id: string }, BookingDetail>(
"bookings",
"generateContract",
({ id }) => bookingsService.generateContract(id),
),
getContractView: endpoint<{ id: string }, import("./bookings.service").ContractView>(
"bookings",
"getContractView",
({ id }) => bookingsService.getContractView(id),
),
signContract: endpoint<
{ id: string } & import("./bookings.service").SignContractPayload,
BookingDetail
>("bookings", "signContract", ({ id, ...payload }) =>
bookingsService.signContract(id, payload),
),
generatePnr: endpoint<{ id: string }, BookingDetail>(
"bookings",
"generatePnr",
({ id }) => bookingsService.generatePnr(id),
),
verifyPayment: endpoint<{ id: string }, BookingDetail>(
"bookings",
"verifyPayment",
({ id }) => bookingsService.verifyPayment(id),
),
startTransit: endpoint<{ id: string }, BookingDetail>(
"bookings",
"startTransit",
({ id }) => bookingsService.startTransit(id),
),
complete: endpoint<{ id: string }, BookingDetail>(
"bookings",
"complete",
({ id }) => bookingsService.complete(id),
),
cancel: endpoint<{ id: string; reason: string }, BookingDetail>(
"bookings",
"cancel",
({ id, reason }) => bookingsService.cancel(id, reason),
),
},
};

View File

@@ -1,51 +1,172 @@
import type { Freight, PaginatedResponse } from "@edr/types";
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 BASE = URL_CONSTANTS.BOOKINGS.BASE;
const B = URL_CONSTANTS.BOOKINGS;
export interface BookingListFilter {
status?: string;
customerId?: string;
search?: 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 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;
}>;
}
export interface SignContractPayload {
role: "CUSTOMER" | "STAFF";
signatureImageBase64: string;
signerDisplayName: string;
consentText?: string;
}
async function postBooking<T>(url: string, body?: unknown): Promise<T> {
const response = await client.post<T>(url, body ?? {});
return unwrap(response.data);
}
export const bookingsService = {
list: async (
filter?: BookingListFilter,
): Promise<PaginatedResponse<Freight.IBooking>> => {
const response = await client.get<PaginatedResponse<Freight.IBooking>>(
BASE,
{ params: filter },
);
return unwrap(response.data);
list: async (filter?: BookingListFilter): Promise<PaginatedBookings> => {
const response = await client.get<PaginatedBookings>(B.BASE, {
params: filter,
});
const data = unwrap(response.data);
return {
items: (data.items ?? []) as BookingDetail[],
total: data.total ?? 0,
};
},
getById: async (id: string): Promise<Freight.IBooking> => {
const response = await client.get<Freight.IBooking>(
URL_CONSTANTS.BOOKINGS.BY_ID(id),
);
return unwrap(response.data);
},
updateStatus: async (
id: string,
payload: { action: string; reason?: string },
): Promise<Freight.IBooking> => {
const response = await client.patch<Freight.IBooking>(
`${URL_CONSTANTS.BOOKINGS.BY_ID(id)}/status`,
payload,
);
return unwrap(response.data);
getById: async (id: string): Promise<BookingDetail> => {
const response = await client.get<BookingDetail>(B.BY_ID(id));
return unwrap(response.data) as BookingDetail;
},
remove: async (id: string): Promise<void> => {
await client.delete(URL_CONSTANTS.BOOKINGS.BY_ID(id));
await client.delete(B.BY_ID(id));
},
staffAccept: (id: string) => postBooking<BookingDetail>(B.STAFF_ACCEPT(id)),
requestChanges: (id: string, note: string) =>
postBooking<BookingDetail>(B.STAFF_REQUEST_CHANGES(id), { note }),
staffReject: (id: string, reason: string) =>
postBooking<BookingDetail>(B.STAFF_REJECT(id), { reason }),
approveStep: ({ id, stepId, requiredRole }: ApproveStepPayload) =>
postBooking<BookingDetail>(B.APPROVE_STEP(id, stepId), { requiredRole }),
rejectStep: ({ id, stepId, reason }: RejectStepPayload) =>
postBooking<BookingDetail>(B.REJECT_STEP(id, stepId), { reason }),
generateContract: (id: string) =>
postBooking<BookingDetail>(B.CONTRACT_GENERATE(id)),
getContractView: async (id: string): Promise<ContractView> => {
const response = await client.get<ContractView>(B.CONTRACT_VIEW(id));
return unwrap(response.data) as ContractView;
},
downloadContract: async (id: string): Promise<Blob> => {
const response = await client.get(B.CONTRACT_DOWNLOAD(id), {
responseType: "blob",
});
return response.data as Blob;
},
downloadContractDocument: async (id: string): Promise<Blob> => {
const response = await client.get(B.CONTRACT_DOCUMENT(id), {
responseType: "blob",
});
return response.data as Blob;
},
signContract: (id: string, payload: SignContractPayload) =>
postBooking<BookingDetail>(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);
},
customerSign: (id: string, payload: SignContractPayload) =>
postBooking<BookingDetail>(B.CUSTOMER_SIGN(id), {
...payload,
role: "CUSTOMER",
}),
marketingApprove: (id: string, payload: SignContractPayload) =>
postBooking<BookingDetail>(B.MARKETING_APPROVE(id), {
...payload,
role: "STAFF",
}),
generatePnr: (id: string) => postBooking<BookingDetail>(B.PAYMENT_PNR(id)),
submitPaymentProof: async (id: string, file: File): Promise<BookingDetail> => {
const form = new FormData();
form.append("file", file);
const response = await client.post<BookingDetail>(B.PAYMENT_PROOF(id), form, {
headers: { "Content-Type": "multipart/form-data" },
});
return unwrap(response.data) as BookingDetail;
},
verifyPayment: (id: string) =>
postBooking<BookingDetail>(B.PAYMENT_VERIFY(id)),
downloadPaymentRequestLetter: async (id: string): Promise<Blob> => {
const response = await client.get(B.PAYMENT_REQUEST_LETTER(id), {
responseType: "blob",
});
return response.data as Blob;
},
startTransit: (id: string) =>
postBooking<BookingDetail>(B.START_TRANSIT(id)),
complete: (id: string) => postBooking<BookingDetail>(B.COMPLETE(id)),
cancel: (id: string, reason: string) =>
postBooking<BookingDetail>(B.CANCEL(id), { reason }),
};

View File

@@ -1,7 +1,6 @@
import { api as client } from "@/auth/http";
import { URL_CONSTANTS } from "@/constants/URLS";
import type {
ApproveRatePayload,
RuleEngineListMeta,
RuleEngineListResult,
RuleEngineRecord,
@@ -143,11 +142,8 @@ export const ruleEngineService = {
return normalizeEntity<T>(response.data);
},
approveRate: async <T extends RuleEngineRecord>(
id: string,
payload: ApproveRatePayload,
): Promise<T> => {
const response = await client.post(URL_CONSTANTS.RULE_ENGINE.RATE_APPROVE(id), payload);
approveRate: async <T extends RuleEngineRecord>(id: string): Promise<T> => {
const response = await client.post(URL_CONSTANTS.RULE_ENGINE.RATE_APPROVE(id));
return normalizeEntity<T>(response.data);
},