mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fix issue
This commit is contained in:
@@ -54,6 +54,8 @@ export function ScheduleWagonYardPanel({ scheduleId, canEdit }: Props) {
|
||||
|
||||
/** wagonId → yardId queued but not yet saved. */
|
||||
const [pending, setPending] = useState<Record<string, string>>({});
|
||||
/** wagonId → cut yard queued but not yet saved; null = queued clear (rides to destination). */
|
||||
const [pendingCut, setPendingCut] = useState<Record<string, string | null>>({});
|
||||
const [bulkType, setBulkType] = useState<string | null>(null);
|
||||
const [bulkFrom, setBulkFrom] = useState<string | null>(null);
|
||||
const [bulkTo, setBulkTo] = useState<string | null>(null);
|
||||
@@ -70,6 +72,32 @@ export function ScheduleWagonYardPanel({ scheduleId, canEdit }: Props) {
|
||||
"—";
|
||||
|
||||
const effectiveYard = (w: ScheduleWagonYardRow) => pending[w.id] ?? w.plannedYardId;
|
||||
const effectiveCut = (w: ScheduleWagonYardRow) =>
|
||||
w.id in pendingCut ? pendingCut[w.id] : w.cutYardId;
|
||||
const stopIndexOf = (yardId: string | null) =>
|
||||
yardId == null ? -1 : (data?.stops ?? []).findIndex((s) => s.yardId === yardId);
|
||||
/** Drop stops a wagon boarding at `boardYardId` can be cut at — strictly after
|
||||
* boarding, excluding the destination (that's the cleared/default state). */
|
||||
const cutOptionsFor = (boardYardId: string | null) => {
|
||||
const stops = data?.stops ?? [];
|
||||
const boardIdx = Math.max(0, stopIndexOf(boardYardId));
|
||||
return stops
|
||||
.slice(boardIdx + 1, stops.length - 1)
|
||||
.map((s) => ({ value: s.yardId, label: s.label }));
|
||||
};
|
||||
/** Board-yard changes can invalidate a cut (server rejects cut ≤ board) — queue a clear. */
|
||||
const clearInvalidCut = (
|
||||
next: Record<string, string | null>,
|
||||
w: ScheduleWagonYardRow,
|
||||
boardYardId: string | null,
|
||||
) => {
|
||||
const cut = w.id in next ? next[w.id] : w.cutYardId;
|
||||
if (cut != null && stopIndexOf(cut) <= stopIndexOf(boardYardId)) {
|
||||
if (w.cutYardId == null) delete next[w.id];
|
||||
else next[w.id] = null;
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
const perStop = useMemo(
|
||||
() =>
|
||||
@@ -77,8 +105,11 @@ export function ScheduleWagonYardPanel({ scheduleId, canEdit }: Props) {
|
||||
...s,
|
||||
planned: (data?.wagons ?? []).filter((w) => (pending[w.id] ?? w.plannedYardId) === s.yardId)
|
||||
.length,
|
||||
cut: (data?.wagons ?? []).filter(
|
||||
(w) => (w.id in pendingCut ? pendingCut[w.id] : w.cutYardId) === s.yardId,
|
||||
).length,
|
||||
})),
|
||||
[data, pending],
|
||||
[data, pending, pendingCut],
|
||||
);
|
||||
const typeOptions = useMemo(() => {
|
||||
const seen = new Map<string, string>();
|
||||
@@ -86,7 +117,7 @@ export function ScheduleWagonYardPanel({ scheduleId, canEdit }: Props) {
|
||||
return [...seen].map(([value, label]) => ({ value, label }));
|
||||
}, [data]);
|
||||
|
||||
const pendingCount = Object.keys(pending).length;
|
||||
const pendingCount = new Set([...Object.keys(pending), ...Object.keys(pendingCut)]).size;
|
||||
|
||||
const queueBulk = () => {
|
||||
if (!data || !bulkFrom || !bulkTo || bulkFrom === bulkTo) return;
|
||||
@@ -111,18 +142,29 @@ export function ScheduleWagonYardPanel({ scheduleId, canEdit }: Props) {
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setPendingCut((prev) => {
|
||||
let next = { ...prev };
|
||||
for (const w of picked) next = clearInvalidCut(next, w, bulkTo);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!pendingCount) return;
|
||||
try {
|
||||
const wagonIds = [...new Set([...Object.keys(pending), ...Object.keys(pendingCut)])];
|
||||
const result = await save.mutateAsync({
|
||||
scheduleId,
|
||||
payload: {
|
||||
moves: Object.entries(pending).map(([wagonId, yardId]) => ({ wagonId, yardId })),
|
||||
moves: wagonIds.map((wagonId) => ({
|
||||
wagonId,
|
||||
...(wagonId in pending ? { yardId: pending[wagonId] } : {}),
|
||||
...(wagonId in pendingCut ? { cutYardId: pendingCut[wagonId] } : {}),
|
||||
})),
|
||||
},
|
||||
});
|
||||
setPending({});
|
||||
setPendingCut({});
|
||||
toast({
|
||||
title: `Schedule yards updated — ${pendingCount} wagon(s) re-planned`,
|
||||
description: result.warnings.length ? result.warnings.join(" ") : undefined,
|
||||
@@ -153,8 +195,10 @@ export function ScheduleWagonYardPanel({ scheduleId, canEdit }: Props) {
|
||||
<Stack gap="md">
|
||||
<Alert color="blue" icon={<MapPin size={16} />} variant="light">
|
||||
<b>Planned</b> = where this departure boards the wagon (what customers can book per
|
||||
origin). <b>Physical</b> = where the wagon stands now (train builder). Dispatch is blocked
|
||||
until every wagon stands at its planned yard.
|
||||
origin). <b>Physical</b> = where the wagon stands now (train builder). <b>Cut at</b> ={" "}
|
||||
where this departure detaches the wagon and leaves it — blank means it rides to the
|
||||
destination; booking capacity past the cut shrinks accordingly. Dispatch is blocked until
|
||||
every wagon stands at its planned yard.
|
||||
{data.misaligned > 0 ? (
|
||||
<Text component="span" c="orange" fw={600}>
|
||||
{" "}
|
||||
@@ -183,6 +227,16 @@ export function ScheduleWagonYardPanel({ scheduleId, canEdit }: Props) {
|
||||
<Badge color={s.physical === s.planned ? "gray" : "orange"} variant="light">
|
||||
Physical {s.physical}
|
||||
</Badge>
|
||||
{s.cut > 0 ? (
|
||||
<Badge color="red" variant="light">
|
||||
Cut {s.cut}
|
||||
</Badge>
|
||||
) : null}
|
||||
{!s.pickup ? (
|
||||
<Badge color="blue" variant="light">
|
||||
Through {data.wagons.length - perStop.reduce((sum, p) => sum + p.cut, 0)}
|
||||
</Badge>
|
||||
) : null}
|
||||
</Group>
|
||||
</Paper>
|
||||
))}
|
||||
@@ -229,13 +283,15 @@ export function ScheduleWagonYardPanel({ scheduleId, canEdit }: Props) {
|
||||
<Table.Th>Type</Table.Th>
|
||||
<Table.Th>Physical yard</Table.Th>
|
||||
<Table.Th>Planned yard (this schedule)</Table.Th>
|
||||
<Table.Th>Cut at (rides to)</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{data.wagons.map((w) => {
|
||||
const planned = effectiveYard(w);
|
||||
const changed = w.id in pending;
|
||||
const cut = effectiveCut(w);
|
||||
const changed = w.id in pending || w.id in pendingCut;
|
||||
return (
|
||||
<Table.Tr key={w.id} bg={changed ? "var(--mantine-color-yellow-light)" : undefined}>
|
||||
<Table.Td>{w.sequenceNumber ?? "—"}</Table.Td>
|
||||
@@ -252,14 +308,17 @@ export function ScheduleWagonYardPanel({ scheduleId, canEdit }: Props) {
|
||||
size="xs"
|
||||
data={yardOptions}
|
||||
value={planned}
|
||||
onChange={(v) =>
|
||||
onChange={(v) => {
|
||||
setPending((prev) => {
|
||||
const next = { ...prev };
|
||||
if (!v || v === w.plannedYardId) delete next[w.id];
|
||||
else next[w.id] = v;
|
||||
return next;
|
||||
})
|
||||
}
|
||||
});
|
||||
setPendingCut((prev) =>
|
||||
clearInvalidCut({ ...prev }, w, v ?? w.plannedYardId),
|
||||
);
|
||||
}}
|
||||
w={180}
|
||||
/>
|
||||
) : (
|
||||
@@ -273,6 +332,30 @@ export function ScheduleWagonYardPanel({ scheduleId, canEdit }: Props) {
|
||||
</Group>
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{editable ? (
|
||||
// Locked wagons stay editable here — the server enforces the
|
||||
// cargo-destination floor and the toast explains a 409.
|
||||
<Select
|
||||
size="xs"
|
||||
clearable
|
||||
placeholder="Destination"
|
||||
data={cutOptionsFor(planned)}
|
||||
value={cut}
|
||||
onChange={(v) =>
|
||||
setPendingCut((prev) => {
|
||||
const next = { ...prev };
|
||||
if ((v ?? null) === w.cutYardId) delete next[w.id];
|
||||
else next[w.id] = v ?? null;
|
||||
return next;
|
||||
})
|
||||
}
|
||||
w={180}
|
||||
/>
|
||||
) : (
|
||||
<Text size="sm">{cut ? yardLabel(cut) : "Destination"}</Text>
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{planned === w.physicalYardId ? (
|
||||
<Badge color="teal" variant="light" size="sm">
|
||||
@@ -295,7 +378,14 @@ export function ScheduleWagonYardPanel({ scheduleId, canEdit }: Props) {
|
||||
<Text size="sm" c="dimmed">
|
||||
{pendingCount} pending change(s)
|
||||
</Text>
|
||||
<Button variant="default" onClick={() => setPending({})} disabled={!pendingCount}>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
setPending({});
|
||||
setPendingCut({});
|
||||
}}
|
||||
disabled={!pendingCount}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -310,6 +310,9 @@ export interface ScheduleWagonYardRow {
|
||||
physicalYardLabel: string | null;
|
||||
plannedYardId: string | null;
|
||||
plannedYardLabel: string | null;
|
||||
/** Drop stop this departure cuts the wagon at; null = rides to the destination. */
|
||||
cutYardId: string | null;
|
||||
cutYardLabel: string | null;
|
||||
aligned: boolean;
|
||||
locked: boolean;
|
||||
lockReason: string | null;
|
||||
@@ -322,6 +325,8 @@ export interface ScheduleWagonYardStop {
|
||||
pickup: boolean;
|
||||
planned: number;
|
||||
physical: number;
|
||||
/** Wagons this departure cuts (detaches and leaves) at this stop. */
|
||||
cut: number;
|
||||
}
|
||||
|
||||
export interface ScheduleWagonYards {
|
||||
@@ -334,7 +339,8 @@ export interface ScheduleWagonYards {
|
||||
}
|
||||
|
||||
export interface UpdateScheduleWagonYardsPayload {
|
||||
moves: Array<{ wagonId: string; yardId: string }>;
|
||||
/** Omit a field to leave it unchanged; cutYardId null clears the cut (rides to destination). */
|
||||
moves: Array<{ wagonId: string; yardId?: string; cutYardId?: string | null }>;
|
||||
}
|
||||
|
||||
export type UpdateScheduleWagonYardsResult = ScheduleWagonYards & { warnings: string[] };
|
||||
|
||||
Reference in New Issue
Block a user