mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
Marshaling document Receive Export and import handover to customer
This commit is contained in:
@@ -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>(
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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`),
|
||||
};
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user