mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 01:48: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:
@@ -347,6 +347,64 @@ export function ScheduleWorkspacePanel({
|
||||
const pct = capacity > 0 ? Math.min(100, Math.round((used / capacity) * 100)) : 0;
|
||||
const over = capacity > 0 && used > capacity;
|
||||
|
||||
// One confirmation dialog for every booking action; the action fires only
|
||||
// after staff confirm, and the existing toasts report the outcome.
|
||||
const [confirmAction, setConfirmAction] = useState<{
|
||||
kind: "add" | "load" | "truckToTrain" | "unload" | "remove";
|
||||
bookingId: string;
|
||||
ref: string;
|
||||
weightTons?: number;
|
||||
} | null>(null);
|
||||
const confirmMeta: Record<
|
||||
NonNullable<typeof confirmAction>["kind"],
|
||||
{ title: string; message: string; color: string; confirmLabel: string }
|
||||
> = {
|
||||
add: {
|
||||
title: "Add booking to this train?",
|
||||
message:
|
||||
"The booking is assigned to this departure and wagons are auto-pinned. Adding past the pull-weight limit is allowed but flagged for review.",
|
||||
color: "edr-green",
|
||||
confirmLabel: "Add to train",
|
||||
},
|
||||
load: {
|
||||
title: "Load cargo onto the train?",
|
||||
message:
|
||||
"Stamps the booking as loaded at this yard. The server checks the train is actually standing here.",
|
||||
color: "edr-green",
|
||||
confirmLabel: "Load",
|
||||
},
|
||||
truckToTrain: {
|
||||
title: "Load as direct truck-to-train?",
|
||||
message:
|
||||
"Sets direct truck-to-train handover (no warehouse receipt, no GRN — the carriage acceptance sheet becomes the handover document) and loads the cargo.",
|
||||
color: "blue",
|
||||
confirmLabel: "Load direct",
|
||||
},
|
||||
unload: {
|
||||
title: "Unload cargo at this yard?",
|
||||
message: "Stamps the booking's arrival at this yard and frees its wagons for reuse.",
|
||||
color: "orange",
|
||||
confirmLabel: "Unload",
|
||||
},
|
||||
remove: {
|
||||
title: "Remove booking from this train?",
|
||||
message:
|
||||
"Returns the booking to the unassigned pool, writes a removal log entry, and notifies the customer.",
|
||||
color: "red",
|
||||
confirmLabel: "Remove",
|
||||
},
|
||||
};
|
||||
const runConfirmedAction = () => {
|
||||
if (!confirmAction) return;
|
||||
const { kind, bookingId, ref, weightTons } = confirmAction;
|
||||
setConfirmAction(null);
|
||||
if (kind === "add") forceAdd(bookingId, ref, weightTons ?? 0);
|
||||
else if (kind === "load") doLoad(bookingId, ref);
|
||||
else if (kind === "truckToTrain") doTruckToTrain(bookingId, ref);
|
||||
else if (kind === "unload") doUnload(bookingId, ref);
|
||||
else removeFromTrain(bookingId, ref);
|
||||
};
|
||||
|
||||
const forceAdd = (bookingId: string, ref: string, weightTons: number) => {
|
||||
const wouldOverfill = capacity > 0 && used + (weightTons || 0) > capacity;
|
||||
assign
|
||||
@@ -649,7 +707,14 @@ export function ScheduleWorkspacePanel({
|
||||
radius="md"
|
||||
rightSection={<ArrowRight size={14} />}
|
||||
loading={assign.isPending}
|
||||
onClick={() => forceAdd(b.id, b.reference, b.weightTons)}
|
||||
onClick={() =>
|
||||
setConfirmAction({
|
||||
kind: "add",
|
||||
bookingId: b.id,
|
||||
ref: b.reference,
|
||||
weightTons: b.weightTons,
|
||||
})
|
||||
}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
@@ -794,7 +859,9 @@ export function ScheduleWorkspacePanel({
|
||||
loadJourney.isPending &&
|
||||
loadJourney.variables?.bookingId === b.id
|
||||
}
|
||||
onClick={() => doLoad(b.id, ref)}
|
||||
onClick={() =>
|
||||
setConfirmAction({ kind: "load", bookingId: b.id, ref })
|
||||
}
|
||||
>
|
||||
Load
|
||||
</Button>
|
||||
@@ -812,7 +879,13 @@ export function ScheduleWorkspacePanel({
|
||||
radius="md"
|
||||
leftSection={<Truck size={13} />}
|
||||
loading={truckToTrainPending === b.id}
|
||||
onClick={() => doTruckToTrain(b.id, ref)}
|
||||
onClick={() =>
|
||||
setConfirmAction({
|
||||
kind: "truckToTrain",
|
||||
bookingId: b.id,
|
||||
ref,
|
||||
})
|
||||
}
|
||||
>
|
||||
Truck to Train
|
||||
</Button>
|
||||
@@ -838,7 +911,9 @@ export function ScheduleWorkspacePanel({
|
||||
unloadJourney.isPending &&
|
||||
unloadJourney.variables?.bookingId === b.id
|
||||
}
|
||||
onClick={() => doUnload(b.id, ref)}
|
||||
onClick={() =>
|
||||
setConfirmAction({ kind: "unload", bookingId: b.id, ref })
|
||||
}
|
||||
>
|
||||
Unload
|
||||
</Button>
|
||||
@@ -857,7 +932,9 @@ export function ScheduleWorkspacePanel({
|
||||
unassign.isPending &&
|
||||
unassign.variables?.bookingId === b.id
|
||||
}
|
||||
onClick={() => removeFromTrain(b.id, ref)}
|
||||
onClick={() =>
|
||||
setConfirmAction({ kind: "remove", bookingId: b.id, ref })
|
||||
}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
@@ -961,6 +1038,82 @@ export function ScheduleWorkspacePanel({
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Confirm add / load / unload / remove */}
|
||||
<Modal
|
||||
opened={Boolean(confirmAction)}
|
||||
onClose={() => setConfirmAction(null)}
|
||||
centered
|
||||
radius="lg"
|
||||
size="md"
|
||||
withCloseButton={false}
|
||||
title={
|
||||
confirmAction ? (
|
||||
<Group gap={10} wrap="nowrap">
|
||||
<ThemeIcon
|
||||
size={40}
|
||||
radius="md"
|
||||
variant="light"
|
||||
color={confirmMeta[confirmAction.kind].color}
|
||||
>
|
||||
{confirmAction.kind === "remove" ? (
|
||||
<X size={21} />
|
||||
) : confirmAction.kind === "unload" ? (
|
||||
<PackageOpen size={21} />
|
||||
) : confirmAction.kind === "truckToTrain" ? (
|
||||
<Truck size={21} />
|
||||
) : (
|
||||
<PackageCheck size={21} />
|
||||
)}
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={800}>{confirmMeta[confirmAction.kind].title}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{confirmAction.ref}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{confirmAction ? (
|
||||
<Stack gap="md">
|
||||
<Text size="sm">{confirmMeta[confirmAction.kind].message}</Text>
|
||||
{confirmAction.kind === "add" &&
|
||||
capacity > 0 &&
|
||||
used + (confirmAction.weightTons ?? 0) > capacity ? (
|
||||
<Group
|
||||
gap={8}
|
||||
p="xs"
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
background: "var(--mantine-color-red-0)",
|
||||
border: "1px solid var(--mantine-color-red-2)",
|
||||
}}
|
||||
>
|
||||
<AlertTriangle size={16} color="#B42318" />
|
||||
<Text size="xs" c="red.8" fw={500}>
|
||||
This add pushes the heaviest leg past the locomotive pull weight (
|
||||
{(used + (confirmAction.weightTons ?? 0)).toFixed(1)}T / {capacity.toFixed(0)}T).
|
||||
</Text>
|
||||
</Group>
|
||||
) : null}
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="default" radius="md" onClick={() => setConfirmAction(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color={confirmMeta[confirmAction.kind].color}
|
||||
radius="md"
|
||||
onClick={runConfirmedAction}
|
||||
>
|
||||
{confirmMeta[confirmAction.kind].confirmLabel}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
) : null}
|
||||
</Modal>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user