Marshaling document Receive Export and import handover to customer

This commit is contained in:
hagiye
2026-06-29 13:48:58 +03:00
310 changed files with 35735 additions and 5958 deletions

View File

@@ -284,6 +284,32 @@ export const api = {
],
),
availableDaysForCargo: endpoint<
{
originYardId?: string;
destinationYardId?: string;
freightType: "CONTAINER" | "BULK";
cargoTypeCode?: string;
totalWeightTons?: number;
containers?: { containerSize: string; quantity: number }[];
},
string[]
>(
"train-scheduling",
"available-days-for-cargo",
(input) => trainSchedulingService.getAvailableDaysForCargo(input),
(input) => [
...QUERY_KEYS.TRAIN_SCHEDULING.ROOT,
"available-days-for-cargo",
input.originYardId ?? "",
input.destinationYardId ?? "",
input.freightType,
input.cargoTypeCode ?? "",
input.totalWeightTons ?? 0,
JSON.stringify(input.containers ?? []),
],
),
trainTrack: endpoint<{ id: string }, TrainTrackResponse>(
"train-scheduling",
"track",
@@ -1845,13 +1871,12 @@ export const api = {
reviewOperation: endpoint<
{
id: string;
decision: "ACCEPT" | "REQUEST_CHANGES" | "ADJUST_PRICE";
decision: "ACCEPT" | "REQUEST_CHANGES";
note?: string;
amount?: number;
},
BookingDetail
>("bookings", "reviewOperation", ({ id, decision, note, amount }) =>
bookingsService.reviewOperation(id, decision, { note, amount }),
>("bookings", "reviewOperation", ({ id, decision, note }) =>
bookingsService.reviewOperation(id, decision, { note }),
),
approveStep: endpoint<ApproveStepPayload, BookingDetail>(

View File

@@ -212,21 +212,14 @@ export const bookingsService = {
/** Marketing/operations review of a drawdown order's operation request. */
reviewOperation: (
id: string,
decision: "ACCEPT" | "REQUEST_CHANGES" | "ADJUST_PRICE",
options: { note?: string; amount?: number } = {},
decision: "ACCEPT" | "REQUEST_CHANGES",
options: { note?: string } = {},
) =>
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

@@ -0,0 +1,369 @@
import { api as client } from "../auth/http";
import { unwrap } from "@/utils/endpoint";
import { URL_CONSTANTS } from "@/constants/URLS";
import type { Freight } from "@edr/types";
const C = URL_CONSTANTS.CONTRACTS;
export interface ContractListFilter {
status?: string;
/** Comma-separated statuses for grouped tabs. */
statuses?: string;
/** Tab key for React Query cache (not sent to API). */
tab?: string;
companyId?: string;
freightType?: string;
tradeDirection?: string;
contractKind?: string;
paymentCurrency?: string;
page?: number;
pageSize?: number;
sortBy?: string;
sortOrder?: "ASC" | "DESC";
}
export interface PaginatedContracts {
items: Freight.IContract[];
total: number;
}
export interface ContractListSummaryMetrics {
inQueue: number;
needsAction: number;
urgent: number;
completed: number;
}
export interface ContractListSummaryTabs {
all: number;
intake: number;
in_approval: number;
approved_contract: number;
clearance: number;
active: number;
closed: number;
}
export interface ContractListSummary {
metrics: ContractListSummaryMetrics;
tabs: ContractListSummaryTabs;
}
export interface ContractView {
contractId: 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;
}>;
savedSignature?: {
signerDisplayName: string;
signatureImageUrl?: string | null;
} | null;
}
export interface SignContractPayload {
role: "CUSTOMER" | "STAFF";
signatureImageBase64: string;
signerDisplayName: string;
consentText?: string;
}
async function postContract<T>(url: string, body?: unknown): Promise<T> {
const response = await client.post<T>(url, body ?? {});
return unwrap(response.data);
}
function buildListParams(filter?: ContractListFilter) {
const params: Record<string, string | number | boolean | undefined> = {};
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.sortBy) params.sortBy = filter.sortBy;
if (filter.sortOrder) params.sortOrder = filter.sortOrder;
if (filter.companyId) params.companyId = filter.companyId;
if (filter.freightType) params.freightType = filter.freightType;
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
if (filter.contractKind) params.contractKind = filter.contractKind;
if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency;
}
return params;
}
export const contractsService = {
getListSummary: async (
filter?: ContractListFilter,
): Promise<ContractListSummary> => {
const response = await client.get<ContractListSummary>(C.LIST_SUMMARY, {
params: buildListParams(filter),
});
return unwrap(response.data) as ContractListSummary;
},
list: async (filter?: ContractListFilter): Promise<PaginatedContracts> => {
const response = await client.get<PaginatedContracts>(C.BASE, {
params: buildListParams(filter),
});
const data = unwrap(response.data);
return {
items: (data.items ?? []) as Freight.IContract[],
total: data.total ?? 0,
};
},
getById: async (id: string): Promise<Freight.IContract> => {
const response = await client.get<Freight.IContract>(C.BY_ID(id));
return unwrap(response.data) as Freight.IContract;
},
// ── Staff review ──
staffAccept: (id: string, validityDays: number) =>
postContract<Freight.IContract>(C.STAFF_ACCEPT(id), { validityDays }),
requestChanges: (id: string, note: string) =>
postContract<Freight.IContract>(C.STAFF_REQUEST_CHANGES(id), { note }),
reject: (id: string, reason: string) =>
postContract<Freight.IContract>(C.STAFF_REJECT(id), { reason }),
approveStep: ({
id,
stepId,
requiredRole,
}: {
id: string;
stepId: string;
requiredRole: string;
}) =>
postContract<Freight.IContract>(C.APPROVE_STEP(id, stepId), {
requiredRole,
}),
// ── Contract document ──
generateContract: (id: string) =>
postContract<Freight.IContract>(C.CONTRACT_GENERATE(id)),
getContractView: async (id: string): Promise<ContractView> => {
const response = await client.get<ContractView>(C.CONTRACT_VIEW(id));
return unwrap(response.data) as ContractView;
},
signContract: (id: string, payload: SignContractPayload) =>
postContract<Freight.IContract>(C.CONTRACT_SIGN(id), payload),
// ── Pre-booking clearance (Path B — GL ET) ──
getClearanceQueue: async (): Promise<PaginatedContracts> => {
const response = await client.get<PaginatedContracts>(C.CLEARANCE_QUEUE);
const data = unwrap(response.data);
return {
items: (data.items ?? []) as Freight.IContract[],
total: data.total ?? 0,
};
},
getClearance: async (id: string): Promise<Freight.ContractClearanceView> => {
const response = await client.get(C.CLEARANCE(id));
return unwrap(response.data) as Freight.ContractClearanceView;
},
reviewClearanceDocument: (
id: string,
payload: { fileKey: string; status: "APPROVED" | "QUERIED"; note?: string },
) =>
postContract<Freight.IContract>(C.CLEARANCE_REVIEW(id), payload),
uploadClearanceOutput: async (
id: string,
files: Record<string, File | null>,
): Promise<Freight.IContract> => {
const form = new FormData();
for (const [key, file] of Object.entries(files)) {
if (file) form.append(key, file);
}
const response = await client.post(C.CLEARANCE_OUTPUT_DOCUMENTS(id), form, {
headers: { "Content-Type": "multipart/form-data" },
});
return unwrap(response.data) as Freight.IContract;
},
finalizeClearance: (id: string) =>
postContract<Freight.IContract>(C.CLEARANCE_FINALIZE(id)),
// ── Path A self-clearance (Operations review) ──
getOpsClearanceQueue: async (): Promise<PaginatedContracts> => {
const response = await client.get<PaginatedContracts>(
C.OPS_CLEARANCE_QUEUE,
);
const data = unwrap(response.data);
return {
items: (data.items ?? []) as Freight.IContract[],
total: data.total ?? 0,
};
},
getClearanceHistory: async (): Promise<PaginatedContracts> => {
const response = await client.get<PaginatedContracts>(C.CLEARANCE_HISTORY);
const data = unwrap(response.data);
return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0 };
},
getOpsClearanceHistory: async (): Promise<PaginatedContracts> => {
const response = await client.get<PaginatedContracts>(C.OPS_CLEARANCE_HISTORY);
const data = unwrap(response.data);
return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0 };
},
opsReviewClearanceDocument: (
id: string,
payload: { fileKey: string; status: "APPROVED" | "QUERIED"; note?: string },
) => postContract<Freight.IContract>(C.OPS_CLEARANCE_REVIEW(id), payload),
opsFinalizeClearance: (id: string) =>
postContract<Freight.IContract>(C.OPS_CLEARANCE_FINALIZE(id)),
// ── Booking under contract (GL ET — Path B) ──
createBookingUnderContract: (
id: string,
payload: Freight.CreateBookingUnderContractDto,
) => postContract<{ id: string; reference: string }>(C.BOOKINGS(id), payload),
/** Remaining bookable quantity per cargo line (GENERAL draw-down cap). */
getCapacity: async (id: string): Promise<Freight.ContractCapacityLine[]> => {
const response = await client.get(C.CAPACITY(id));
return (unwrap(response.data) ?? []) as Freight.ContractCapacityLine[];
},
// ── Shipment requests (GENERAL + customs) ──
/** GL queue of pending shipment requests across contracts. */
getBookingRequestQueue: async (): Promise<Freight.IBookingRequest[]> => {
const response = await client.get(C.BOOKING_REQUEST_QUEUE);
return (unwrap(response.data) ?? []) as Freight.IBookingRequest[];
},
listBookingRequests: async (
id: string,
): Promise<Freight.IBookingRequest[]> => {
const response = await client.get(C.BOOKING_REQUESTS(id));
return (unwrap(response.data) ?? []) as Freight.IBookingRequest[];
},
getBookingRequest: async (
reqId: string,
): Promise<Freight.IBookingRequest> => {
const response = await client.get(C.BOOKING_REQUEST_BY_ID(reqId));
return unwrap(response.data) as Freight.IBookingRequest;
},
acceptBookingRequest: (reqId: string, bookingId: string) =>
postContract<Freight.IBookingRequest>(C.BOOKING_REQUEST_ACCEPT(reqId), {
bookingId,
}),
rejectBookingRequest: (reqId: string, note?: string) =>
postContract<Freight.IBookingRequest>(C.BOOKING_REQUEST_REJECT(reqId), {
note,
}),
// ── Clearance milestones ──
listMilestonesForContract: async (
id: string,
): Promise<Freight.IClearanceMilestone[]> => {
const response = await client.get(C.MILESTONES(id));
return (unwrap(response.data) ?? []) as Freight.IClearanceMilestone[];
},
listMilestonesForBooking: async (
bookingId: string,
): Promise<Freight.IClearanceMilestone[]> => {
const response = await client.get(C.BOOKING_MILESTONES(bookingId));
return (unwrap(response.data) ?? []) as Freight.IClearanceMilestone[];
},
completeMilestone: (bookingId: string, code: string, note?: string) =>
postContract<Freight.IClearanceMilestone>(
C.COMPLETE_BOOKING_MILESTONE(bookingId, code),
{ note },
),
// ── GL post-booking operational actions ──
assignRisk: (
bookingId: string,
payload: { riskLevel: Freight.CustomsRiskLevel; note?: string },
) =>
postContract<Freight.IClearanceMilestone>(
C.BOOKING_RISK(bookingId),
payload,
),
adviseDuty: (
bookingId: string,
payload: {
amount: number;
currency: string;
declarationSerial?: string;
note?: string;
},
) =>
postContract<Freight.IClearanceMilestone>(
C.BOOKING_DUTY(bookingId),
payload,
),
assignStation: (
bookingId: string,
payload: { stationYardId: string; staffId?: string },
) => postContract(C.BOOKING_STATION_ASSIGN(bookingId), payload),
uploadGlDocuments: async (
bookingId: string,
files: Record<string, File | null>,
): Promise<{ uploaded: number; completedMilestones: string[] }> => {
const form = new FormData();
for (const [key, file] of Object.entries(files)) {
if (file) form.append(key, file);
}
const response = await client.post(C.BOOKING_GL_DOCUMENTS(bookingId), form, {
headers: { "Content-Type": "multipart/form-data" },
});
return unwrap(response.data) as {
uploaded: number;
completedMilestones: string[];
};
},
listIncidents: async (
bookingId: string,
): Promise<Freight.IClearanceIncident[]> => {
const response = await client.get(C.BOOKING_INCIDENTS(bookingId));
return (unwrap(response.data) ?? []) as Freight.IClearanceIncident[];
},
reportIncident: async (
bookingId: string,
payload: {
incidentType: Freight.IncidentType;
description: string;
photos: File[];
},
): Promise<Freight.IClearanceIncident> => {
const form = new FormData();
form.append("incidentType", payload.incidentType);
form.append("description", payload.description);
for (const photo of payload.photos) form.append("photos", photo);
const response = await client.post(C.BOOKING_INCIDENTS(bookingId), form, {
headers: { "Content-Type": "multipart/form-data" },
});
return unwrap(response.data) as Freight.IClearanceIncident;
},
};

View File

@@ -4,6 +4,7 @@ import { URL_CONSTANTS } from "@/constants/URLS";
import type {
IOverviewBillingTab,
IOverviewBookingsTab,
IOverviewContractsTab,
IOverviewCustomersTab,
IOverviewDashboard,
IOverviewOperationsTab,
@@ -28,6 +29,13 @@ export const overviewService = {
return unwrap(response);
},
getContractsTab: async (range?: OverviewRange): Promise<IOverviewContractsTab> => {
const response = await client.get<IOverviewContractsTab>(O.CONTRACTS, {
params: range ? { range } : undefined,
});
return unwrap(response);
},
getBillingTab: async (range?: OverviewRange): Promise<IOverviewBillingTab> => {
const response = await client.get<IOverviewBillingTab>(O.BILLING, {
params: range ? { range } : undefined,

View File

@@ -0,0 +1,35 @@
import { api } from '../auth/http';
export interface Rate {
id: string;
rateType: string;
appliesTo: string;
trigger: string;
containerTypeId: string | null;
cargoTypeId: string | null;
tradeDirection: string | null;
currency: string;
rateValue: string;
rateUnit: string;
status: string;
proposedByStaffId: string;
approvedByCeoId: string | null;
approvedAt: string | null;
effectiveFrom: string;
effectiveTo: string | null;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
export interface RatesListResponse {
data: Rate[];
meta: { total: number; page: number; pageSize: number; totalPages: number };
}
export const ratesService = {
list: (pageSize = 1000) =>
api.get<RatesListResponse>(`/rates?pageSize=${pageSize}`),
getByType: (rateType: string) =>
api.get<RatesListResponse>(`/rates?rateType=${rateType}&pageSize=1000`),
};

View File

@@ -1,3 +1,4 @@
import type { Freight } from "@edr/types";
import { api as client } from "../auth/http";
import { unwrap } from "@/utils/endpoint";
import { URL_CONSTANTS } from "@/constants/URLS";
@@ -131,6 +132,24 @@ export const trainSchedulingService = {
return unwrap(response.data).days;
},
// Cargo-aware day pool (matching wagons + open train capacity). `containers`
// is serialized as a JSON string param (the server parses it).
getAvailableDaysForCargo: async (
query: Freight.AvailableDaysForCargoQuery,
): Promise<string[]> => {
const { containers, ...rest } = query;
const response = await client.get<{ days: string[] }>(
URL_CONSTANTS.TRAIN_SCHEDULING.AVAILABLE_DAYS_FOR_CARGO,
{
params: {
...rest,
...(containers ? { containers: JSON.stringify(containers) } : {}),
},
},
);
return unwrap(response.data).days;
},
runBatch: async (scheduleId: string): Promise<BatchBoardScheduleDetail> => {
const response = await client.post<BatchBoardScheduleDetail>(
URL_CONSTANTS.TRAIN_SCHEDULING.RUN_BATCH(scheduleId),