mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 06:28:12 +00:00
feat(train-scheduling): mid-route consist changes, audit history, safer workspace
- planned couples: loose wagons join the train at a route stop, added from the schedule yards tab; capacity credits them per corridor edge and coupling validates locomotive weight/length caps per leg - real-cut toggle: a cut wagon permanently leaves the train build at its cut yard (soft cut still sits out one trip only) - fix heaviest-leg display counting a shared slot's full cargo on every spanned edge (phantom pull-weight overload on S-2026-00045) - confirmation dialogs for workspace add/load/unload/remove actions - train-builder History and Detached-wagons tabs, backed by paginated endpoints; builder detaches now always write adjustment-log rows Migrations 3660 (planned_wagon_couples, planned_wagon_real_cuts) and 3670 (adjustment log train_schedule_id nullable) — both applied to the dev DB by hand; watch mode does not run migrations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -231,6 +231,8 @@ import {
|
||||
type BuildTrainPayload,
|
||||
type BuiltTrainListFilters,
|
||||
type BuiltTrainListResponse,
|
||||
type DetachedWagonRow,
|
||||
type TrainHistoryEntry,
|
||||
type ScheduleConsist,
|
||||
type ScheduleWagonYards,
|
||||
type UpdateScheduleWagonYardsPayload,
|
||||
@@ -2103,6 +2105,22 @@ export const api = {
|
||||
({ id }) => QUERY_KEYS.TRAIN_BUILDER.composition(id),
|
||||
),
|
||||
|
||||
// Keys derive to ["train-builder", "history"|"detachedWagons", input] — the
|
||||
// shared TRAIN_BUILDER.ROOT invalidation refreshes both after every edit.
|
||||
history: endpoint<
|
||||
{ id: string; page: number; pageSize: number },
|
||||
PaginatedResponse<TrainHistoryEntry>
|
||||
>("train-builder", "history", ({ id, page, pageSize }) =>
|
||||
trainBuilderService.getHistory(id, page, pageSize).then((r) => r.data),
|
||||
),
|
||||
|
||||
detachedWagons: endpoint<
|
||||
{ id: string; page: number; pageSize: number },
|
||||
PaginatedResponse<DetachedWagonRow>
|
||||
>("train-builder", "detachedWagons", ({ id, page, pageSize }) =>
|
||||
trainBuilderService.getDetachedWagons(id, page, pageSize).then((r) => r.data),
|
||||
),
|
||||
|
||||
// Key derives to ["train-builder", "usedTrainNumbers"], so the shared
|
||||
// TRAIN_BUILDER.ROOT invalidation refreshes it after every build/edit.
|
||||
usedTrainNumbers: endpoint<void, UsedTrainNumbers>(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { PaginatedResponse } from "@edr/types";
|
||||
|
||||
import { api as apiClient } from "../auth/http";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -300,6 +302,29 @@ export interface ScheduleHistoryEntry {
|
||||
/** Adjust response: fresh consist + schedule-impact warnings to surface. */
|
||||
export type AdjustConsistResult = ScheduleConsist & { warnings: string[] };
|
||||
|
||||
/** One wagon adjustment on a built train (History tab): builder edits and trip events alike. */
|
||||
export interface TrainHistoryEntry {
|
||||
id: string;
|
||||
action: "ADD" | "REMOVE" | "SWITCH";
|
||||
subject: string | null;
|
||||
yardLabel: string | null;
|
||||
actor: string | null;
|
||||
/** Set when the change came from a trip (schedule); null = train-builder edit. */
|
||||
scheduleReference: string | null;
|
||||
occurredAt: string;
|
||||
}
|
||||
|
||||
/** Wagon last detached from this train and still loose — the re-attach shortlist. */
|
||||
export interface DetachedWagonRow {
|
||||
wagonId: string;
|
||||
wagonNumber: string;
|
||||
wagonTypeCode: string | null;
|
||||
currentYardLabel: string | null;
|
||||
detachedAt: string;
|
||||
detachedYardLabel: string | null;
|
||||
detachedBy: string | null;
|
||||
}
|
||||
|
||||
/** One consist wagon in the schedule-yards tab: where this departure plans it vs where it stands. */
|
||||
export interface ScheduleWagonYardRow {
|
||||
id: string;
|
||||
@@ -313,6 +338,11 @@ export interface ScheduleWagonYardRow {
|
||||
/** Drop stop this departure cuts the wagon at; null = rides to the destination. */
|
||||
cutYardId: string | null;
|
||||
cutYardLabel: string | null;
|
||||
/** true = REAL cut: the built train permanently loses the wagon at the cut yard. */
|
||||
realCut: boolean;
|
||||
/** Set on planned-couple rows: the pickup stop this loose wagon joins the train at. */
|
||||
coupledYardId: string | null;
|
||||
coupledYardLabel: string | null;
|
||||
aligned: boolean;
|
||||
locked: boolean;
|
||||
lockReason: string | null;
|
||||
@@ -327,6 +357,8 @@ export interface ScheduleWagonYardStop {
|
||||
physical: number;
|
||||
/** Wagons this departure cuts (detaches and leaves) at this stop. */
|
||||
cut: number;
|
||||
/** Loose wagons this departure couples onto the train at this stop. */
|
||||
coupled: number;
|
||||
}
|
||||
|
||||
export interface ScheduleWagonYards {
|
||||
@@ -340,7 +372,16 @@ export interface ScheduleWagonYards {
|
||||
|
||||
export interface UpdateScheduleWagonYardsPayload {
|
||||
/** Omit a field to leave it unchanged; cutYardId null clears the cut (rides to destination). */
|
||||
moves: Array<{ wagonId: string; yardId?: string; cutYardId?: string | null }>;
|
||||
moves?: Array<{
|
||||
wagonId: string;
|
||||
yardId?: string;
|
||||
cutYardId?: string | null;
|
||||
realCut?: boolean;
|
||||
}>;
|
||||
/** Loose wagons to plan-couple at a pickup stop (they must stand at that yard). */
|
||||
couple?: Array<{ wagonId: string; yardId: string }>;
|
||||
/** Wagon ids to drop from the couple plan. */
|
||||
uncouple?: string[];
|
||||
}
|
||||
|
||||
export type UpdateScheduleWagonYardsResult = ScheduleWagonYards & { warnings: string[] };
|
||||
@@ -349,6 +390,14 @@ export const trainBuilderService = {
|
||||
list: (filters: BuiltTrainListFilters = {}) =>
|
||||
apiClient.get<BuiltTrainListResponse>(`${BASE}${toQuery(filters)}`),
|
||||
getComposition: (id: string) => apiClient.get<TrainComposition>(`${BASE}/${id}`),
|
||||
getHistory: (id: string, page: number, pageSize: number) =>
|
||||
apiClient.get<PaginatedResponse<TrainHistoryEntry>>(
|
||||
`${BASE}/${id}/history?page=${page}&pageSize=${pageSize}`,
|
||||
),
|
||||
getDetachedWagons: (id: string, page: number, pageSize: number) =>
|
||||
apiClient.get<PaginatedResponse<DetachedWagonRow>>(
|
||||
`${BASE}/${id}/detached-wagons?page=${page}&pageSize=${pageSize}`,
|
||||
),
|
||||
/** Import/export run numbers already claimed by existing trains. */
|
||||
usedTrainNumbers: () => apiClient.get<UsedTrainNumbers>(`${BASE}/used-train-numbers`),
|
||||
build: (payload: BuildTrainPayload) => apiClient.post<TrainComposition>(BASE, payload),
|
||||
|
||||
Reference in New Issue
Block a user