Release Order plus Storage Allocation Rule and fee

This commit is contained in:
hagiye
2026-06-24 15:27:38 +03:00
174 changed files with 11729 additions and 3162 deletions

View File

@@ -1817,10 +1817,10 @@ export const api = {
({ id }) => bookingsService.remove(id),
),
staffAccept: endpoint<{ id: string }, BookingDetail>(
staffAccept: endpoint<{ id: string; validityDays: number }, BookingDetail>(
"bookings",
"staffAccept",
({ id }) => bookingsService.staffAccept(id),
({ id, validityDays }) => bookingsService.staffAccept(id, validityDays),
),
requestChanges: endpoint<{ id: string; note: string }, BookingDetail>(
@@ -1835,6 +1835,18 @@ export const api = {
({ id, reason }) => bookingsService.staffReject(id, reason),
),
reviewOperation: endpoint<
{
id: string;
decision: "ACCEPT" | "REQUEST_CHANGES" | "ADJUST_PRICE";
note?: string;
amount?: number;
},
BookingDetail
>("bookings", "reviewOperation", ({ id, decision, note, amount }) =>
bookingsService.reviewOperation(id, decision, { note, amount }),
),
approveStep: endpoint<ApproveStepPayload, BookingDetail>(
"bookings",
"approveStep",
@@ -1947,6 +1959,18 @@ export const api = {
QUERY_KEYS.CUSTOMERS.ROOT,
],
),
setCompanyStatus: endpoint<{ companyId: string; status: string }, unknown>(
"customers",
"setCompanyStatus",
({ companyId, status }) =>
customersService.setCompanyStatus(companyId, status),
undefined,
(input) => [
QUERY_KEYS.CUSTOMERS.byId(input.companyId),
QUERY_KEYS.CUSTOMERS.ROOT,
],
),
},
overview: {

View File

@@ -2,6 +2,7 @@ import { api as client } from "../auth/http";
import { unwrap } from "@/utils/endpoint";
import { URL_CONSTANTS } from "@/constants/URLS";
import type { BookingDetail } from "@/types/booking";
import type { Freight } from "@edr/types";
const B = URL_CONSTANTS.BOOKINGS;
@@ -169,7 +170,38 @@ export const bookingsService = {
await client.delete(B.BY_ID(id));
},
staffAccept: (id: string) => postBooking<BookingDetail>(B.STAFF_ACCEPT(id)),
// ── Document clearance (GL workflow) ──
getClearance: async (id: string): Promise<Freight.ClearanceView> => {
const response = await client.get(`/bookings/${id}/clearance`);
return unwrap(response.data) as Freight.ClearanceView;
},
reviewClearanceDocument: (
id: string,
payload: { fileKey: string; status: "APPROVED" | "QUERIED"; note?: string },
) => postBooking<BookingDetail>(`/bookings/${id}/clearance/review`, payload),
uploadClearanceOutput: async (
id: string,
files: Record<string, File | null>,
): Promise<BookingDetail> => {
const form = new FormData();
for (const [key, file] of Object.entries(files)) {
if (file) form.append(key, file);
}
const response = await client.post(
`/bookings/${id}/clearance/output-documents`,
form,
{ headers: { "Content-Type": "multipart/form-data" } },
);
return unwrap(response.data) as BookingDetail;
},
finalizeClearance: (id: string) =>
postBooking<BookingDetail>(`/bookings/${id}/clearance/finalize`),
staffAccept: (id: string, validityDays: number) =>
postBooking<BookingDetail>(B.STAFF_ACCEPT(id), { validityDays }),
requestChanges: (id: string, note: string) =>
postBooking<BookingDetail>(B.STAFF_REQUEST_CHANGES(id), { note }),
@@ -177,6 +209,24 @@ export const bookingsService = {
staffReject: (id: string, reason: string) =>
postBooking<BookingDetail>(B.STAFF_REJECT(id), { reason }),
/** Marketing/operations review of a drawdown order's operation request. */
reviewOperation: (
id: string,
decision: "ACCEPT" | "REQUEST_CHANGES" | "ADJUST_PRICE",
options: { note?: string; amount?: number } = {},
) =>
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

@@ -88,4 +88,11 @@ export const customersService = {
)
.then((r) => r.data);
},
/** Approve / change a company's status (e.g. pending → active). */
setCompanyStatus(companyId: string, status: string): Promise<unknown> {
return apiClient
.patch(URL_CONSTANTS.COMPANIES.BY_ID(companyId), { status })
.then((r) => r.data);
},
};

View File

@@ -0,0 +1,64 @@
import { api } from '../auth/http';
import { URL_CONSTANTS } from '@/constants/URLS';
export const FIRST_MILE_STATUSES = [
'PAYMENT_PENDING',
'READY_TO_TRANSIT',
'IN_TRANSIT',
'RECEIVED_TO_PORT',
] as const;
export type FirstMileApiStatus = (typeof FIRST_MILE_STATUSES)[number];
export interface FirstMileBooking {
id: string;
reference: string;
firstMilePickupAddress?: string | null;
cargoFreeText?: string | null;
cargoTotalWeightVgm: number;
totalAmount: number;
scheduledDate?: string | null;
company?: { id: string; name?: string; phone?: string | null; contactPersonName?: string | null; contactPersonPhone?: string | null } | null;
serviceType?: { id: string; name?: string } | null;
originYard?: { id: string; name?: string } | null;
destinationYard?: { id: string; name?: string } | null;
cargoType?: { id: string; name?: string } | null;
}
export interface FirstMileVehicle {
id: string;
plateNumber: string;
manufacturer: string;
model: string;
}
export interface FirstMileRecord {
id: string;
bookingId: string;
status: FirstMileApiStatus;
advancedPayment: number;
remainingPayment: number;
estimatedKm?: number | null;
exactKm?: number | null;
vehicleId?: string | null;
booking?: FirstMileBooking | null;
vehicle?: FirstMileVehicle | null;
createdAt: string;
updatedAt: string;
}
export interface FirstMileListResponse {
data: FirstMileRecord[];
meta: { total: number; page: number; pageSize: number; totalPages: number };
}
const FM = URL_CONSTANTS.FIRST_MILE;
export const firstMileService = {
list: (pageSize = 1000) =>
api.get<FirstMileListResponse>(`${FM.BASE}?pageSize=${pageSize}`),
getById: (id: string) => api.get<FirstMileRecord>(FM.BY_ID(id)),
update: (id: string, data: { status?: FirstMileApiStatus; vehicleId?: string | null }) =>
api.patch<FirstMileRecord>(FM.BY_ID(id), data),
accept: (bookingReference: string) =>
api.post<FirstMileRecord>(FM.ACCEPT(bookingReference)),
};

View File

@@ -0,0 +1,64 @@
import { api } from '../auth/http';
import { URL_CONSTANTS } from '@/constants/URLS';
export const LAST_MILE_STATUSES = [
'PAYMENT_PENDING',
'READY_TO_TRANSIT',
'IN_TRANSIT',
'DELIVERED',
] as const;
export type LastMileApiStatus = (typeof LAST_MILE_STATUSES)[number];
export interface LastMileBooking {
id: string;
reference: string;
lastMileDeliveryAddress?: string | null;
cargoFreeText?: string | null;
cargoTotalWeightVgm: number;
totalAmount: number;
scheduledDate?: string | null;
company?: { id: string; name?: string; phone?: string | null; contactPersonName?: string | null; contactPersonPhone?: string | null } | null;
serviceType?: { id: string; name?: string } | null;
originYard?: { id: string; name?: string } | null;
destinationYard?: { id: string; name?: string } | null;
cargoType?: { id: string; name?: string } | null;
}
export interface LastMileVehicle {
id: string;
plateNumber: string;
manufacturer: string;
model: string;
}
export interface LastMileRecord {
id: string;
bookingId: string;
status: LastMileApiStatus;
advancedPayment: number;
remainingPayment: number;
estimatedKm?: number | null;
exactKm?: number | null;
vehicleId?: string | null;
booking?: LastMileBooking | null;
vehicle?: LastMileVehicle | null;
createdAt: string;
updatedAt: string;
}
export interface LastMileListResponse {
data: LastMileRecord[];
meta: { total: number; page: number; pageSize: number; totalPages: number };
}
const LM = URL_CONSTANTS.LAST_MILE;
export const lastMileService = {
list: (pageSize = 1000) =>
api.get<LastMileListResponse>(`${LM.BASE}?pageSize=${pageSize}`),
getById: (id: string) => api.get<LastMileRecord>(LM.BY_ID(id)),
update: (id: string, data: { status?: LastMileApiStatus; vehicleId?: string | null }) =>
api.patch<LastMileRecord>(LM.BY_ID(id), data),
accept: (bookingReference: string) =>
api.post<LastMileRecord>(LM.ACCEPT(bookingReference)),
};

View File

@@ -30,7 +30,6 @@ const RESOURCE_BASE: Record<RuleEngineResourceSlug, string> = {
"wagon-types": URL_CONSTANTS.RULE_ENGINE.WAGON_TYPES,
"priority-configs": URL_CONSTANTS.RULE_ENGINE.PRIORITY_CONFIGS,
"service-types": URL_CONSTANTS.RULE_ENGINE.SERVICE_TYPES,
"surcharge-types": URL_CONSTANTS.RULE_ENGINE.SURCHARGE_TYPES,
"weight-limit-rules": URL_CONSTANTS.RULE_ENGINE.WEIGHT_LIMIT_RULES,
yards: URL_CONSTANTS.RULE_ENGINE.YARDS,
"shipping-lines": URL_CONSTANTS.RULE_ENGINE.SHIPPING_LINES,
@@ -50,8 +49,6 @@ const byIdPath = (resource: RuleEngineResourceSlug, id: string): string => {
return URL_CONSTANTS.RULE_ENGINE.PRIORITY_CONFIG_BY_ID(id);
case "service-types":
return URL_CONSTANTS.RULE_ENGINE.SERVICE_TYPE_BY_ID(id);
case "surcharge-types":
return URL_CONSTANTS.RULE_ENGINE.SURCHARGE_TYPE_BY_ID(id);
case "weight-limit-rules":
return URL_CONSTANTS.RULE_ENGINE.WEIGHT_LIMIT_RULE_BY_ID(id);
case "yards":