Merge pull request #982 from Tria-plc/freight_feature/usermanagement

Freight feature/usermanagement
This commit is contained in:
marshal
2026-07-28 08:03:48 +03:00
committed by GitHub
90 changed files with 3132 additions and 3773 deletions

View File

@@ -1632,17 +1632,25 @@ export const api = {
},
wagons: {
/** Every match, page-walked — for pickers and yard views. Lists use `listPaged`. */
list: endpoint<{ filters?: WagonListFilters }, Wagon[]>(
"wagons",
"list",
({ filters }) => wagonService.getAll(filters ?? {}).then((r) => r.data),
({ filters }) => wagonService.listAll(filters ?? {}),
({ filters }) => ["wagons", "list", filters ?? {}],
),
listPaged: endpoint<{ filters?: WagonListFilters }, PaginatedResponse<Wagon>>(
"wagons",
"listPaged",
({ filters }) => wagonService.getAll(filters ?? {}).then((r) => r.data),
({ filters }) => ["wagons", "listPaged", filters ?? {}],
),
listByTrain: endpoint<{ trainId: string }, Wagon[]>(
"wagons",
"listByTrain",
({ trainId }) => wagonService.getByTrain(trainId).then((r) => r.data),
({ trainId }) => wagonService.getByTrain(trainId),
({ trainId }) => ["wagons", "train", trainId],
),

View File

@@ -245,6 +245,14 @@ export const contractsService = {
reject: (id: string, reason: string) =>
postContract<Freight.IContract>(C.STAFF_REJECT(id), { reason }),
/** Freeze a signed contract. Reversible — see {@link resume}. */
suspend: (id: string, reason: string) =>
postContract<Freight.IContract>(C.SUSPEND(id), { reason }),
/** Lift a suspension; the contract returns to the status it was frozen at. */
resume: (id: string, note?: string) =>
postContract<Freight.IContract>(C.RESUME(id), { note }),
/**
* Approve the next pending step. The server resolves the step's required role
* and authorizes against it — the client never declares its own role.
@@ -377,22 +385,29 @@ export const contractsService = {
finalizeClearance: (id: string) =>
postContract<Freight.IContract>(C.CLEARANCE_FINALIZE(id)),
getEtClearanceQueue: async (): Promise<PaginatedContracts> => {
const response = await client.get<PaginatedContracts>(C.CLEARANCE_ET_QUEUE);
/**
* GL worklist: executed one-time customs contracts with no shipment instance
* yet. GL initiates the booking; the customer then uploads his clearance
* documents on it.
*/
getAwaitingShipmentContracts: async (): Promise<Freight.IContract[]> => {
const response = await client.get(C.AWAITING_SHIPMENT);
const data = unwrap(response.data);
return {
items: (data.items ?? []) as Freight.IContract[],
total: data.total ?? 0,
};
return (Array.isArray(data) ? data : (data?.items ?? [])) as Freight.IContract[];
},
getDjClearanceQueue: async (): Promise<PaginatedContracts> => {
const response = await client.get<PaginatedContracts>(C.CLEARANCE_DJ_QUEUE);
const data = unwrap(response.data);
return {
items: (data.items ?? []) as Freight.IContract[],
total: data.total ?? 0,
};
/** Open a bare shipment instance under a contract (no cargo, no day). */
initiateBookingUnderContract: async (
id: string,
contractRouteId?: string,
): Promise<{ id: string; reference: string }> => {
const result = await postContract<{
booking?: { id: string; reference: string };
id?: string;
reference?: string;
}>(C.BOOKINGS_INITIATE(id), contractRouteId ? { contractRouteId } : {});
const booking = result.booking ?? result;
return { id: booking.id ?? "", reference: booking.reference ?? "" };
},
uploadDeclaration: async (
@@ -575,25 +590,6 @@ export const contractsService = {
return unwrap(response.data) as { advised: boolean; skipped: boolean };
},
// ── Path A self-clearance (Operations review) ──
getOpsClearanceQueue: async (filter?: {
page?: number;
pageSize?: number;
search?: string;
/** Comma-separated ops-clearance lifecycle statuses; omitted → under-review queue. */
statuses?: string;
}): Promise<PaginatedContracts> => {
const response = await client.get<PaginatedContracts>(
C.OPS_CLEARANCE_QUEUE,
{ params: filter },
);
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);

View File

@@ -23,7 +23,7 @@ const listHandlers: Record<
> = {
locomotives: (filters) => locomotivesService.getAll(filters ?? {}).then((r) => r.data),
trains: () => trainService.getAll().then((r) => r.data),
wagons: (filters) => wagonService.getAll(filters ?? {}).then((r) => r.data),
wagons: (filters) => wagonService.listAll(filters ?? {}),
containers: () => containerService.getAll().then((r) => r.data),
cargoes: () => cargoService.getAll().then((r) => r.data),
vehicles: (filters) => vehiclesService.getAll(filters ?? {}).then((r) => r.data),
@@ -38,7 +38,7 @@ const pagedHandlers: Partial<
Record<FleetResourceSlug, (filters: FleetListFilters) => Promise<PaginatedResponse<FleetRecord>>>
> = {
locomotives: (filters) => locomotivesService.getPaged(filters).then((r) => r.data),
wagons: (filters) => wagonService.getPaged(filters).then((r) => r.data),
wagons: (filters) => wagonService.getAll(filters).then((r) => r.data),
};
export const isFleetServerPaginated = (slug: FleetResourceSlug): boolean =>

View File

@@ -95,15 +95,28 @@ export interface WagonMovementRecord {
}
export const wagonService = {
/** One page ({items, meta}); 10 rows unless `pageSize` says otherwise. */
getAll: (filters: WagonListFilters = {}) =>
apiClient.get<Wagon[]>(`/wagons${wagonListQuery(filters)}`),
/** Same filters as `getAll`, server-paginated ({items, meta}). */
getPaged: (filters: WagonListFilters = {}) =>
apiClient.get<PaginatedResponse<Wagon>>(`/wagons/paged${wagonListQuery(filters)}`),
apiClient.get<PaginatedResponse<Wagon>>(`/wagons${wagonListQuery(filters)}`),
/**
* Every matching wagon, page-walked at the API's 100-row cap. For the pickers
* and yard views that filter the whole fleet in the browser — a list page
* should use `getAll` and show the real page controls instead.
*/
listAll: async (filters: WagonListFilters = {}): Promise<Wagon[]> => {
const pageSize = 100;
const first = await wagonService.getAll({ ...filters, page: 1, pageSize });
const items = [...first.data.items];
for (let page = 2; page <= (first.data.meta.totalPages ?? 1); page += 1) {
const next = await wagonService.getAll({ ...filters, page, pageSize });
items.push(...next.data.items);
}
return items;
},
getById: (id: string) => apiClient.get<Wagon>(`/wagons/${id}`),
getMovements: (id: string) =>
apiClient.get<WagonMovementRecord[]>(`/wagons/${id}/movements`),
getByTrain: (trainId: string) => apiClient.get<Wagon[]>(`/wagons?trainId=${trainId}`),
getByTrain: (trainId: string) => wagonService.listAll({ trainId }),
assignToTrain: (wagonId: string, trainId: string, sequenceNumber?: number) =>
apiClient.post(`/wagons/${wagonId}/assign-train`, { trainId, sequenceNumber }),
unassign: (wagonId: string) => apiClient.post(`/wagons/${wagonId}/unassign-train`),