This commit is contained in:
Marshal
2026-07-14 11:06:49 +00:00
parent 957a185a4d
commit 6d0cf50b4d
64 changed files with 4896 additions and 404 deletions

View File

@@ -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",

View File

@@ -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 },
}),
};

View File

@@ -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`,
),
};