mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
Merge pull request #672 from Tria-plc/freight_feature/usermanagement
train
This commit is contained in:
@@ -156,6 +156,7 @@ import {
|
||||
import {
|
||||
locomotivesService,
|
||||
type Locomotive,
|
||||
type LocomotiveListFilters,
|
||||
type SaveLocomotivePayload,
|
||||
} from "./locomotives.service";
|
||||
import { overviewService } from "./overview.service";
|
||||
@@ -181,13 +182,24 @@ import {
|
||||
type SaveSignaturePayload,
|
||||
} from "./signatures.service";
|
||||
import { trainService, type Train } from "./trains.service";
|
||||
import {
|
||||
trainBuilderService,
|
||||
type AvailableTrain,
|
||||
type BuildTrainPayload,
|
||||
type BuiltTrainListFilters,
|
||||
type BuiltTrainListResponse,
|
||||
type TrainComposition,
|
||||
} from "./trainBuilder.service";
|
||||
import { trainSchedulingService } from "./trainScheduling.service";
|
||||
import { wagonTypesService, type WagonType } from "./wagon-types.service";
|
||||
import {
|
||||
wagonService,
|
||||
wagonTransferRequestService,
|
||||
type Wagon,
|
||||
type WagonListFilters,
|
||||
type WagonMovementRecord,
|
||||
type WagonTransferRequest,
|
||||
type CreateTransferRequestPayload,
|
||||
} from "./wagon.service";
|
||||
import { warehouseService } from "./warehouse.service";
|
||||
|
||||
@@ -213,6 +225,19 @@ const TRAIN_SCHEDULING_INVALIDATIONS: ReadonlyArray<readonly unknown[]> = [
|
||||
QUERY_KEYS.BOOKINGS.ROOT,
|
||||
];
|
||||
|
||||
/**
|
||||
* Train Builder mutations change wagon/locomotive availability and the
|
||||
* schedule-creation train picker alongside the builder's own lists.
|
||||
*/
|
||||
const TRAIN_BUILDER_INVALIDATIONS: ReadonlyArray<readonly unknown[]> = [
|
||||
QUERY_KEYS.TRAIN_BUILDER.ROOT,
|
||||
QUERY_KEYS.TRAIN_SCHEDULING.ROOT,
|
||||
["wagons"],
|
||||
["locomotives"],
|
||||
["trains"],
|
||||
QUERY_KEYS.FLEET.ROOT,
|
||||
];
|
||||
|
||||
export const api = {
|
||||
trainScheduling: {
|
||||
// ── Queries ────────────────────────────────────────────────────────────
|
||||
@@ -286,6 +311,14 @@ export const api = {
|
||||
({ routeId }) => QUERY_KEYS.TRAIN_SCHEDULING.locomotives(routeId),
|
||||
),
|
||||
|
||||
availableTrains: endpoint<{ routeId: string }, AvailableTrain[]>(
|
||||
"train-scheduling",
|
||||
"available-trains",
|
||||
({ routeId }) =>
|
||||
trainBuilderService.availableTrains(routeId).then((r) => r.data),
|
||||
({ routeId }) => QUERY_KEYS.TRAIN_SCHEDULING.availableTrains(routeId),
|
||||
),
|
||||
|
||||
bookableSchedules: endpoint<
|
||||
{ originYardId?: string | null; destinationYardId?: string | null },
|
||||
BookableSchedule[]
|
||||
@@ -1621,6 +1654,55 @@ export const api = {
|
||||
),
|
||||
},
|
||||
|
||||
wagonTransferRequests: {
|
||||
list: endpoint<
|
||||
{ status?: WagonTransferRequest["status"] },
|
||||
WagonTransferRequest[]
|
||||
>(
|
||||
"wagonTransferRequests",
|
||||
"list",
|
||||
({ status }) =>
|
||||
wagonTransferRequestService.list(status).then((r) => r.data),
|
||||
({ status }) => ["wagonTransferRequests", "list", status ?? "ALL"],
|
||||
),
|
||||
|
||||
getById: endpoint<{ id: string }, WagonTransferRequest>(
|
||||
"wagonTransferRequests",
|
||||
"getById",
|
||||
({ id }) => wagonTransferRequestService.getById(id).then((r) => r.data),
|
||||
({ id }) => ["wagonTransferRequests", "detail", id],
|
||||
),
|
||||
|
||||
create: endpoint<CreateTransferRequestPayload, WagonTransferRequest>(
|
||||
"wagonTransferRequests",
|
||||
"create",
|
||||
(payload) =>
|
||||
wagonTransferRequestService.create(payload).then((r) => r.data),
|
||||
undefined,
|
||||
() => [["wagonTransferRequests"]],
|
||||
),
|
||||
|
||||
fulfill: endpoint<
|
||||
{ id: string; wagonIds: string[] },
|
||||
WagonTransferRequest
|
||||
>(
|
||||
"wagonTransferRequests",
|
||||
"fulfill",
|
||||
({ id, wagonIds }) =>
|
||||
wagonTransferRequestService.fulfill(id, wagonIds).then((r) => r.data),
|
||||
undefined,
|
||||
() => [["wagonTransferRequests"], ["wagons"]],
|
||||
),
|
||||
|
||||
cancel: endpoint<{ id: string }, WagonTransferRequest>(
|
||||
"wagonTransferRequests",
|
||||
"cancel",
|
||||
({ id }) => wagonTransferRequestService.cancel(id).then((r) => r.data),
|
||||
undefined,
|
||||
() => [["wagonTransferRequests"]],
|
||||
),
|
||||
},
|
||||
|
||||
trains: {
|
||||
list: endpoint<void, Train[]>(
|
||||
"trains",
|
||||
@@ -1661,6 +1743,80 @@ export const api = {
|
||||
),
|
||||
},
|
||||
|
||||
// Train Builder — persistent coded consists (2+ locomotives + ordered wagons)
|
||||
// that train scheduling can reference as a unit. Every mutation also touches
|
||||
// wagon/locomotive availability, so those roots are invalidated together.
|
||||
trainBuilder: {
|
||||
list: endpoint<{ filters?: BuiltTrainListFilters }, BuiltTrainListResponse>(
|
||||
"train-builder",
|
||||
"list",
|
||||
({ filters }) => trainBuilderService.list(filters).then((r) => r.data),
|
||||
({ filters }) => QUERY_KEYS.TRAIN_BUILDER.list(filters),
|
||||
),
|
||||
|
||||
composition: endpoint<{ id: string }, TrainComposition>(
|
||||
"train-builder",
|
||||
"composition",
|
||||
({ id }) => trainBuilderService.getComposition(id).then((r) => r.data),
|
||||
({ id }) => QUERY_KEYS.TRAIN_BUILDER.composition(id),
|
||||
),
|
||||
|
||||
build: endpoint<BuildTrainPayload, TrainComposition>(
|
||||
"train-builder",
|
||||
"build",
|
||||
(payload) => trainBuilderService.build(payload).then((r) => r.data),
|
||||
undefined,
|
||||
() => TRAIN_BUILDER_INVALIDATIONS,
|
||||
),
|
||||
|
||||
setLocomotives: endpoint<
|
||||
{ id: string; locomotiveIds: string[] },
|
||||
TrainComposition
|
||||
>(
|
||||
"train-builder",
|
||||
"setLocomotives",
|
||||
({ id, locomotiveIds }) =>
|
||||
trainBuilderService.setLocomotives(id, locomotiveIds).then((r) => r.data),
|
||||
undefined,
|
||||
() => TRAIN_BUILDER_INVALIDATIONS,
|
||||
),
|
||||
|
||||
assignWagons: endpoint<{ id: string; wagonIds: string[] }, TrainComposition>(
|
||||
"train-builder",
|
||||
"assignWagons",
|
||||
({ id, wagonIds }) =>
|
||||
trainBuilderService.assignWagons(id, wagonIds).then((r) => r.data),
|
||||
undefined,
|
||||
() => TRAIN_BUILDER_INVALIDATIONS,
|
||||
),
|
||||
|
||||
removeWagon: endpoint<{ id: string; wagonId: string }, TrainComposition>(
|
||||
"train-builder",
|
||||
"removeWagon",
|
||||
({ id, wagonId }) =>
|
||||
trainBuilderService.removeWagon(id, wagonId).then((r) => r.data),
|
||||
undefined,
|
||||
() => TRAIN_BUILDER_INVALIDATIONS,
|
||||
),
|
||||
|
||||
reorderWagons: endpoint<{ id: string; wagonIds: string[] }, TrainComposition>(
|
||||
"train-builder",
|
||||
"reorderWagons",
|
||||
({ id, wagonIds }) =>
|
||||
trainBuilderService.reorderWagons(id, wagonIds).then((r) => r.data),
|
||||
undefined,
|
||||
() => TRAIN_BUILDER_INVALIDATIONS,
|
||||
),
|
||||
|
||||
disband: endpoint<string, void>(
|
||||
"train-builder",
|
||||
"disband",
|
||||
(id) => trainBuilderService.disband(id).then(() => undefined),
|
||||
undefined,
|
||||
() => TRAIN_BUILDER_INVALIDATIONS,
|
||||
),
|
||||
},
|
||||
|
||||
locomotives: {
|
||||
list: endpoint<void, Locomotive[]>(
|
||||
"locomotives",
|
||||
@@ -1669,6 +1825,13 @@ export const api = {
|
||||
() => ["locomotives"],
|
||||
),
|
||||
|
||||
listFiltered: endpoint<{ filters?: LocomotiveListFilters }, Locomotive[]>(
|
||||
"locomotives",
|
||||
"listFiltered",
|
||||
({ filters }) => locomotivesService.getAll(filters ?? {}).then((r) => r.data),
|
||||
({ filters }) => ["locomotives", "list", filters ?? {}],
|
||||
),
|
||||
|
||||
create: endpoint<Partial<SaveLocomotivePayload>, Locomotive>(
|
||||
"locomotives",
|
||||
"create",
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { api as apiClient } from "../auth/http";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types — mirror the freight API's train-builder responses
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type BuiltTrainStatus =
|
||||
| "AVAILABLE"
|
||||
| "SCHEDULED"
|
||||
| "IN_SERVICE"
|
||||
| "UNDER_MAINTENANCE"
|
||||
| "OUT_OF_SERVICE";
|
||||
|
||||
export interface YardRefLite {
|
||||
id: string;
|
||||
code: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface BuiltTrainSummary {
|
||||
id: string;
|
||||
code: string;
|
||||
trainName: string | null;
|
||||
status: BuiltTrainStatus;
|
||||
createdAt: string;
|
||||
currentYard: YardRefLite | null;
|
||||
locomotives: Array<{ id: string; code: string; name: string | null }>;
|
||||
wagonCount: number;
|
||||
maxGrossTons: number;
|
||||
totalLengthMeters: number;
|
||||
maxPullWeightTons: number;
|
||||
}
|
||||
|
||||
export interface TrainCompositionLocomotive {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string | null;
|
||||
locomotiveType: "DIESEL" | "ELECTRIC";
|
||||
status: string;
|
||||
sequenceNo: number;
|
||||
role: "LEAD" | "ASSIST";
|
||||
currentYardId: string | null;
|
||||
currentYard: YardRefLite | null;
|
||||
maxPullWeightTons: number;
|
||||
maxTrainLengthMeters: number;
|
||||
}
|
||||
|
||||
export interface TrainCompositionWagon {
|
||||
id: string;
|
||||
wagonNumber: string;
|
||||
sequenceNumber: number | null;
|
||||
status: string;
|
||||
wagonType: {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
capacityTons: number;
|
||||
tareWeightTons: number;
|
||||
lengthMeters: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface TrainCompositionTotals {
|
||||
wagonCount: number;
|
||||
totalTareTons: number;
|
||||
totalCapacityTons: number;
|
||||
maxGrossTons: number;
|
||||
totalLengthMeters: number;
|
||||
maxPullWeightTons: number;
|
||||
maxTrainLengthMeters: number;
|
||||
weightUtilizationPct: number | null;
|
||||
lengthUtilizationPct: number | null;
|
||||
}
|
||||
|
||||
export interface TrainComposition {
|
||||
id: string;
|
||||
code: string;
|
||||
trainName: string | null;
|
||||
status: BuiltTrainStatus;
|
||||
notes: string | null;
|
||||
createdAt: string;
|
||||
currentYard: YardRefLite | null;
|
||||
locomotives: TrainCompositionLocomotive[];
|
||||
wagons: TrainCompositionWagon[];
|
||||
totals: TrainCompositionTotals;
|
||||
activeSchedules: Array<{ id: string; status: string; reference: string | null }>;
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
export interface BuiltTrainListFilters {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
status?: BuiltTrainStatus;
|
||||
currentYardId?: string;
|
||||
sortBy?: "code" | "trainName" | "status" | "createdAt";
|
||||
sortOrder?: "ASC" | "DESC";
|
||||
}
|
||||
|
||||
export interface BuiltTrainListResponse {
|
||||
items: BuiltTrainSummary[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BuildTrainPayload {
|
||||
code: string;
|
||||
currentYardId: string;
|
||||
locomotiveIds: string[];
|
||||
wagonIds?: string[];
|
||||
trainName?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
/** Built train annotated for the schedule-creation picker. */
|
||||
export interface AvailableTrain {
|
||||
id: string;
|
||||
code: string;
|
||||
trainName: string | null;
|
||||
status: BuiltTrainStatus;
|
||||
currentYardId: string | null;
|
||||
currentYard: YardRefLite | null;
|
||||
locomotives: Array<{ id: string; code: string; name: string | null }>;
|
||||
wagonCount: number;
|
||||
maxGrossTons: number;
|
||||
totalLengthMeters: number;
|
||||
maxPullWeightTons: number;
|
||||
atOriginYard: boolean;
|
||||
futureScheduleCount: number;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BASE = "/train-builder";
|
||||
|
||||
const toQuery = (filters: BuiltTrainListFilters = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
});
|
||||
const qs = params.toString();
|
||||
return qs ? `?${qs}` : "";
|
||||
};
|
||||
|
||||
export const trainBuilderService = {
|
||||
list: (filters: BuiltTrainListFilters = {}) =>
|
||||
apiClient.get<BuiltTrainListResponse>(`${BASE}${toQuery(filters)}`),
|
||||
getComposition: (id: string) => apiClient.get<TrainComposition>(`${BASE}/${id}`),
|
||||
build: (payload: BuildTrainPayload) => apiClient.post<TrainComposition>(BASE, payload),
|
||||
setLocomotives: (id: string, locomotiveIds: string[]) =>
|
||||
apiClient.put<TrainComposition>(`${BASE}/${id}/locomotives`, { locomotiveIds }),
|
||||
assignWagons: (id: string, wagonIds: string[]) =>
|
||||
apiClient.post<TrainComposition>(`${BASE}/${id}/wagons`, { wagonIds }),
|
||||
removeWagon: (id: string, wagonId: string) =>
|
||||
apiClient.delete<TrainComposition>(`${BASE}/${id}/wagons/${wagonId}`),
|
||||
reorderWagons: (id: string, wagonIds: string[]) =>
|
||||
apiClient.post<TrainComposition>(`${BASE}/${id}/reorder-wagons`, { wagonIds }),
|
||||
disband: (id: string) => apiClient.delete<void>(`${BASE}/${id}`),
|
||||
/** Built trains schedulable on a route (train-scheduling picker). */
|
||||
availableTrains: (routeId: string) =>
|
||||
apiClient.get<AvailableTrain[]>(`/train-scheduling/available-trains`, {
|
||||
params: { routeId },
|
||||
}),
|
||||
};
|
||||
@@ -90,3 +90,53 @@ export const wagonService = {
|
||||
bulkSetStatus: (wagonIds: string[], status: Freight.WagonStatus) =>
|
||||
apiClient.post<{ updated: number }>('/wagons/bulk-status', { wagonIds, status }),
|
||||
};
|
||||
|
||||
/**
|
||||
* A two-person wagon-transfer request: a requester asks for N wagons of a type
|
||||
* to move between yards (count only); OCC hand-picks the wagons and fulfils it.
|
||||
*/
|
||||
export interface WagonTransferRequest {
|
||||
id: string;
|
||||
fromYardId: string;
|
||||
toYardId: string;
|
||||
wagonTypeId: string;
|
||||
quantity: number;
|
||||
status: Freight.WagonTransferRequestStatus;
|
||||
requestedByUserId: string | null;
|
||||
fulfilledByUserId: string | null;
|
||||
fulfilledAt: string | null;
|
||||
note: string | null;
|
||||
fromYard?: { id: string; label?: string; code?: string } | null;
|
||||
toYard?: { id: string; label?: string; code?: string } | null;
|
||||
wagonType?: { id: string; code?: string; name?: string } | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreateTransferRequestPayload {
|
||||
fromYardId: string;
|
||||
toYardId: string;
|
||||
wagonTypeId: string;
|
||||
quantity: number;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export const wagonTransferRequestService = {
|
||||
list: (status?: Freight.WagonTransferRequestStatus) =>
|
||||
apiClient.get<WagonTransferRequest[]>(
|
||||
`/wagon-transfer-requests${status ? `?status=${status}` : ''}`,
|
||||
),
|
||||
getById: (id: string) =>
|
||||
apiClient.get<WagonTransferRequest>(`/wagon-transfer-requests/${id}`),
|
||||
create: (data: CreateTransferRequestPayload) =>
|
||||
apiClient.post<WagonTransferRequest>('/wagon-transfer-requests', data),
|
||||
/** OCC: execute the transfer with the hand-picked wagons. */
|
||||
fulfill: (id: string, wagonIds: string[]) =>
|
||||
apiClient.post<WagonTransferRequest>(
|
||||
`/wagon-transfer-requests/${id}/fulfill`,
|
||||
{ wagonIds },
|
||||
),
|
||||
cancel: (id: string) =>
|
||||
apiClient.post<WagonTransferRequest>(
|
||||
`/wagon-transfer-requests/${id}/cancel`,
|
||||
),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user