fix issue

This commit is contained in:
Marshal
2026-07-31 12:23:41 +00:00
parent 8b9dd76710
commit 7e85c16141
5 changed files with 318 additions and 465 deletions

View File

@@ -83,6 +83,11 @@ export class BookingJourneyService {
loadedByUserId: userId ?? null,
} as never);
await this.setAllocationStatuses(manager, scheduleId, bookingId, 'LOADED');
// Keep the schedule↔booking link's tracking flag in sync — the dispatch
// readiness warnings and workspace badges read loading_status, not loadedAt.
await manager
.getRepository(TrainScheduleBooking)
.update({ trainScheduleId: scheduleId, bookingId }, { loadingStatus: 'LOADED' });
// The facility handed the cargo over — raise its GRN. No-ops for yards
// without a facility (import/export terminals), which keep their own flow.
await this.facilityHandling.recordHandling(manager, {
@@ -236,7 +241,10 @@ export class BookingJourneyService {
return {
scheduleId,
scheduleStatus: schedule.status,
trainAtYardId: latest?.yardId ?? (schedule.status === 'DISPATCHED' ? null : schedule.originStationId),
// No checkpoint yet ⇒ the train is still at its origin, even just after
// dispatch — assertTrainAtYard allows origin loading in that state, so
// the UI position must agree or origin Load buttons grey out wrongly.
trainAtYardId: latest?.yardId ?? schedule.originStationId,
yards: [...byYard.values()],
};
}

View File

@@ -1977,6 +1977,16 @@ export class TrainSchedulingService {
manager,
);
// Unassign only runs pre-dispatch, so an IN_TRANSIT status here is stale
// (e.g. auto-loaded by an earlier dispatch that was rolled back). Left as
// is, the booking becomes invisible: the eligible pool only admits PAID,
// so it can never be re-added to any train. Revert it to PAID.
if (booking?.status === 'IN_TRANSIT' && !booking.arrivedAt) {
await manager
.getRepository(Booking)
.update(bookingId, { status: 'PAID', loadedAt: null } as never);
}
// Recompute the train-set composition from whatever survives this removal.
// The removed booking's allocations were already deleted above, so any slot
// left with zero allocations was ridden only by this booking — release it

View File

@@ -22,10 +22,13 @@ import {
ArrowRight,
CheckCircle2,
Inbox,
Landmark,
MapPin,
PackageCheck,
PackageX,
PackageOpen,
// Repeat, // used by the hidden Move (reassign) button
Train,
TrainFront,
Weight,
X,
} from "lucide-react";
@@ -39,6 +42,7 @@ import type {
EligibleContainerBooking,
FreightType,
TrainScheduleDetail,
YardWorkBookingRow,
} from "@/types/trainScheduling";
interface ScheduleWorkspacePanelProps {
@@ -151,6 +155,9 @@ export function ScheduleWorkspacePanel({
const locked = ["DISPATCHED", "ARRIVED"].includes(schedule.status);
const canManage = ["DRAFT", "SCHEDULED"].includes(schedule.status);
// Loading follows the train: it keeps working AFTER dispatch, per yard, as
// checkpoints are logged — only add/remove is closed once the train rolls.
const canWork = ["DRAFT", "SCHEDULED", "DISPATCHED"].includes(schedule.status);
// Pool = accepted, ready-to-pay bookings on THIS train's route+day that are not
// yet linked to any schedule (same filter the auto-batch uses).
@@ -180,14 +187,83 @@ export function ScheduleWorkspacePanel({
const onTrain = schedule.bookings ?? [];
// ── Corridor position: which yard the train currently stands at ───────────
// The journey worklist knows the train's latest checkpoint AND per-booking
// load/unload eligibility — the same server rules that gate the mutations.
const yardWorkQuery = useQuery(
api.trainScheduling.yardWork.queryOptions({
input: { scheduleId: schedule.id },
refetchInterval: 60_000,
}),
);
const trainAtYardId = yardWorkQuery.data?.trainAtYardId ?? null;
const journeyById = useMemo(() => {
const map = new Map<string, YardWorkBookingRow>();
for (const yard of yardWorkQuery.data?.yards ?? []) {
for (const row of [...yard.toLoad, ...yard.toUnload]) map.set(row.id, row);
}
return map;
}, [yardWorkQuery.data]);
// Ordered corridor (origin → stops → destination). Falls back to the two
// endpoints when the schedule has no stops recorded.
const stations = useMemo(() => {
const stops = schedule.stops ?? [];
if (stops.length) return stops;
return [
{ yardId: schedule.originStation?.id ?? "origin", label: schedule.originStation?.label ?? "Origin" },
{
yardId: schedule.destinationStation?.id ?? "destination",
label: schedule.destinationStation?.label ?? "Destination",
},
];
}, [schedule.stops, schedule.originStation, schedule.destinationStation]);
const stationIdx = useMemo(
() => new Map(stations.map((s, i) => [s.yardId, i])),
[stations],
);
const trainIdx = trainAtYardId != null ? (stationIdx.get(trainAtYardId) ?? null) : null;
const trainAtLabel =
trainIdx != null ? stations[trainIdx]?.label : null;
// On-train bookings grouped by BOARDING yard, in corridor order. A booking
// whose origin is off this corridor (through cargo on legacy data) groups
// under the train's own origin.
const corridorGroups = useMemo(() => {
const groups = new Map<string, { yardId: string; label: string; rows: typeof onTrain }>();
for (const b of onTrain) {
const yardId =
b.originYardId && stationIdx.has(b.originYardId)
? b.originYardId
: (stations[0]?.yardId ?? "origin");
let group = groups.get(yardId);
if (!group) {
group = {
yardId,
label:
stations[stationIdx.get(yardId) ?? 0]?.label ?? b.origin ?? "Origin",
rows: [],
};
groups.set(yardId, group);
}
group.rows.push(b);
}
return [...groups.values()].sort(
(a, b) => (stationIdx.get(a.yardId) ?? 0) - (stationIdx.get(b.yardId) ?? 0),
);
}, [onTrain, stationIdx, stations]);
// ── Mutations (reuse the existing endpoints) ───────────────────────────────
const assign = useMutation(api.trainScheduling.assignBookings.mutationOptions());
const assignUnassigned = useMutation(
api.trainScheduling.assignUnassignedBooking.mutationOptions(),
);
const unassign = useMutation(api.trainScheduling.unassignBooking.mutationOptions());
const setLoading = useMutation(
api.trainScheduling.setLoadingStatus.mutationOptions(),
const loadJourney = useMutation(
api.trainScheduling.loadScheduleBooking.mutationOptions(),
);
const unloadJourney = useMutation(
api.trainScheduling.unloadScheduleBooking.mutationOptions(),
);
const moveSchedule = useMutation(
api.trainScheduling.moveBookingSchedule.mutationOptions(),
@@ -296,26 +372,42 @@ export function ScheduleWorkspacePanel({
);
};
const toggleLoaded = (
bookingId: string,
ref: string,
next: "LOADED" | "UNLOADED",
) => {
setLoading
.mutateAsync({ id: schedule.id, bookingIds: [bookingId], loadingStatus: next })
// Journey load/unload — the server checks the train's recorded position, so
// a stale UI can never load cargo at the wrong yard.
const doLoad = (bookingId: string, ref: string) => {
loadJourney
.mutateAsync({ scheduleId: schedule.id, bookingId })
.then(() => {
toast({
title:
next === "LOADED"
? `${ref} marked loaded`
: `${ref} marked unloaded`,
});
toast({ title: `${ref} loaded onto the train` });
onChanged();
void yardWorkQuery.refetch();
})
.catch((error) =>
toast({
title: "Could not update loading status",
description: apiErrorMessage(error, "Please try again."),
title: "Could not load cargo",
description: apiErrorMessage(error, "Train may not be at the boarding yard."),
variant: "destructive",
}),
);
};
const doUnload = (bookingId: string, ref: string) => {
unloadJourney
.mutateAsync({ scheduleId: schedule.id, bookingId })
.then((result) => {
toast({
title:
result.status === "COMPLETED"
? `${ref} unloaded — booking completed`
: `${ref} unloaded — booking arrived`,
});
onChanged();
void yardWorkQuery.refetch();
})
.catch((error) =>
toast({
title: "Could not unload cargo",
description: apiErrorMessage(error, "Train may not be at the destination yard."),
variant: "destructive",
}),
);
@@ -382,7 +474,8 @@ export function ScheduleWorkspacePanel({
<div>
<Text fw={700}>Allocation workspace</Text>
<Text size="xs" c="dimmed">
Manually add paid, unassigned bookings, remove, or reassign them
Add or remove bookings, then load each one when the train is at
its boarding yard
</Text>
</div>
</Group>
@@ -458,7 +551,10 @@ export function ScheduleWorkspacePanel({
{locked ? (
<Text size="sm" c="dimmed">
This train is {schedule.status.toLowerCase()} bookings can no longer be
changed.
added or removed.
{schedule.status === "DISPATCHED"
? " Loading continues per yard as checkpoints are logged on the track page."
: ""}
</Text>
) : null}
@@ -485,6 +581,10 @@ export function ScheduleWorkspacePanel({
weightTons={b.weightTons}
status={b.status}
waitingForWagon={b.schedulingStatus === "WAITING_FOR_WAGON"}
government={b.isGovernment}
leg={
b.origin && b.destination ? `${b.origin}${b.destination}` : null
}
right={
canManage ? (
<Group gap={6} wrap="nowrap" justify="flex-end">
@@ -525,114 +625,174 @@ export function ScheduleWorkspacePanel({
))}
</PanelColumn>
{/* On train */}
{/* On train — grouped by boarding yard, walked in corridor order.
Load is only offered where the train actually stands; the journey
endpoints re-validate the position server-side. */}
<PanelColumn
title="On this train"
hint="Allocated bookings"
hint={
trainAtLabel ? `Train at ${trainAtLabel}` : "Grouped by boarding yard"
}
count={onTrain.length}
accent="#0EA371"
emptyIcon={Train}
emptyText="No bookings allocated yet. Add one from the pool."
>
{onTrain.map((b) => (
<BookingCard
key={b.id}
reference={b.reference ?? b.id.slice(0, 8)}
customer={b.customer}
weightTons={b.weightTons}
status={b.status}
intercity={b.tradeDirection === "DOMESTIC"}
leg={
b.origin &&
b.destination &&
(b.originYardId !== schedule.originStation?.id ||
b.destinationYardId !== schedule.destinationStation?.id)
? `${b.origin}${b.destination}`
: null
}
loadingStatus={b.wagonAssigned ? b.loadingStatus ?? "UNLOADED" : undefined}
right={
canManage ? (
<Group gap={6} wrap="nowrap" justify="flex-end">
{b.wagonAssigned ? (
<Tooltip
label={
(b.loadingStatus ?? "UNLOADED") === "LOADED"
? "Mark cargo unloaded from wagon"
: "Mark cargo loaded onto wagon"
}
withArrow
>
<Button
size="compact-sm"
variant={
(b.loadingStatus ?? "UNLOADED") === "LOADED"
? "light"
: "filled"
}
color="edr-green"
radius="md"
leftSection={
(b.loadingStatus ?? "UNLOADED") === "LOADED" ? (
<PackageX size={13} />
) : (
<PackageCheck size={13} />
{corridorGroups.map((group) => {
const groupIdx = stationIdx.get(group.yardId) ?? 0;
const trainHere = trainAtYardId === group.yardId;
const passed = trainIdx != null && groupIdx < trainIdx;
return (
<Stack key={group.yardId} gap={6}>
<Group gap={8} align="center" mt={4}>
<MapPin size={13} color="var(--mantine-color-gray-6)" />
<Text size="xs" fw={700}>
{group.label}
</Text>
{trainHere ? (
<Badge
size="sm"
radius="sm"
variant="filled"
color="edr-green"
leftSection={<TrainFront size={11} />}
>
Train here
</Badge>
) : passed ? (
<Badge size="sm" radius="sm" variant="light" color="gray">
Passed
</Badge>
) : (
<Badge size="sm" radius="sm" variant="outline" color="gray">
Ahead
</Badge>
)}
<Badge size="sm" radius="sm" variant="light" color="gray">
{group.rows.length}
</Badge>
</Group>
{group.rows.map((b) => {
const ref = b.reference ?? b.id.slice(0, 8);
const journey = journeyById.get(b.id);
const riding = b.status === "IN_TRANSIT";
const done = ["ARRIVED", "COMPLETED", "DELIVERED"].includes(
b.status ?? "",
);
const boardHere = trainHere;
const alightHere = trainAtYardId != null && b.destinationYardId === trainAtYardId;
const showLoad = canWork && !riding && !done && (journey?.canLoad ?? false);
const showUnload = canWork && riding && (journey?.canUnload ?? false);
return (
<BookingCard
key={b.id}
reference={ref}
customer={b.customer}
weightTons={b.weightTons}
status={b.status}
government={journey?.isGovernment ?? false}
intercity={b.tradeDirection === "DOMESTIC"}
leg={
b.origin &&
b.destination &&
(b.originYardId !== schedule.originStation?.id ||
b.destinationYardId !== schedule.destinationStation?.id)
? `${b.origin}${b.destination}`
: null
}
loadingStatus={
riding || Boolean(b.loadedAt)
? "LOADED"
: b.wagonAssigned
? (b.loadingStatus ?? "UNLOADED")
: undefined
}
right={
<Group gap={6} wrap="nowrap" justify="flex-end">
{showLoad ? (
<Tooltip
label={
boardHere
? `Load cargo onto the train at ${group.label}`
: passed
? `Train already passed ${group.label} — this cargo missed its stop`
: `Loads at ${group.label} — train is ${
trainAtLabel ? `at ${trainAtLabel}` : "not there yet"
}`
}
withArrow
>
<Button
size="compact-sm"
variant="filled"
color="edr-green"
radius="md"
disabled={!boardHere}
leftSection={<PackageCheck size={13} />}
loading={
loadJourney.isPending &&
loadJourney.variables?.bookingId === b.id
}
onClick={() => doLoad(b.id, ref)}
>
Load
</Button>
</Tooltip>
) : null}
{showUnload ? (
<Tooltip
label={
alightHere
? "Unload at this yard — stamps the booking's arrival"
: "Unloads when the train reaches its destination yard"
}
withArrow
>
<Button
size="compact-sm"
variant="light"
color="orange"
radius="md"
disabled={!alightHere}
leftSection={<PackageOpen size={13} />}
loading={
unloadJourney.isPending &&
unloadJourney.variables?.bookingId === b.id
}
onClick={() => doUnload(b.id, ref)}
>
Unload
</Button>
</Tooltip>
) : null}
{canManage && !riding && !done ? (
journey?.isGovernment ? null : (
<Tooltip label="Remove from this train" withArrow>
<Button
size="compact-sm"
variant="light"
color="red"
radius="md"
leftSection={<X size={13} />}
loading={
unassign.isPending &&
unassign.variables?.bookingId === b.id
}
onClick={() => removeFromTrain(b.id, ref)}
>
Remove
</Button>
</Tooltip>
)
}
loading={setLoading.isPending}
onClick={() =>
toggleLoaded(
b.id,
b.reference ?? b.id.slice(0, 8),
(b.loadingStatus ?? "UNLOADED") === "LOADED"
? "UNLOADED"
: "LOADED",
)
}
>
{(b.loadingStatus ?? "UNLOADED") === "LOADED"
? "Unload"
: "Load"}
</Button>
</Tooltip>
) : null}
{/* Reassign-to-another-train — hidden for now.
<Tooltip label="Reassign to another train" withArrow>
<Button
size="compact-sm"
variant="subtle"
color="orange"
radius="md"
leftSection={<Repeat size={13} />}
onClick={() => {
setMoveBookingId(b.id);
setMoveTarget(null);
}}
>
Move
</Button>
</Tooltip>
*/}
<Tooltip label="Remove from this train" withArrow>
<Button
size="compact-sm"
variant="light"
color="red"
radius="md"
leftSection={<X size={13} />}
loading={unassign.isPending}
onClick={() =>
removeFromTrain(b.id, b.reference ?? b.id.slice(0, 8))
}
>
Remove
</Button>
</Tooltip>
</Group>
) : null
}
/>
))}
) : null}
</Group>
}
/>
);
})}
</Stack>
);
})}
</PanelColumn>
</Group>
</Stack>
@@ -807,6 +967,7 @@ function BookingCard({
loadingStatus,
waitingForWagon,
intercity,
government,
leg,
right,
}: {
@@ -819,6 +980,8 @@ function BookingCard({
waitingForWagon?: boolean;
/** DOMESTIC ride-along riding only part of this train's corridor. */
intercity?: boolean;
/** Government booking — remove is blocked, only switch. */
government?: boolean;
/** "Origin → Destination" when the booking rides a sub-corridor leg. */
leg?: string | null;
right?: React.ReactNode;
@@ -856,6 +1019,22 @@ function BookingCard({
</Badge>
</Tooltip>
) : null}
{government ? (
<Tooltip
label="Government booking — cannot be removed, only switched"
withArrow
>
<Badge
size="sm"
radius="sm"
variant="light"
color="yellow"
leftSection={<Landmark size={10} />}
>
Government
</Badge>
</Tooltip>
) : null}
{waitingForWagon ? (
<Tooltip
label="Paid, but no wagon of the required type was free. Free a wagon or assign it to a same-day train that has one."

View File

@@ -1,342 +0,0 @@
import {
Alert,
Badge,
Button,
Divider,
Group,
Loader,
Paper,
Stack,
Table,
Text,
Tooltip,
} from "@mantine/core";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AlertCircle, MapPin, PackageCheck, PackageOpen, TrainFront } from "lucide-react";
import { Freight } from "@edr/types";
import { api } from "@/services/api";
import { useToast } from "@/hooks/use-toast";
import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
import type { YardWorkBookingRow, YardWorkYard } from "@/types/trainScheduling";
const parseError = (error: unknown, fallback: string) => {
const message = (error as { response?: { data?: { message?: string | string[] } } })
?.response?.data?.message;
if (Array.isArray(message)) return message.join("; ");
return message || (error as Error)?.message || fallback;
};
const fmtDate = (iso: string) => {
const d = new Date(iso);
return Number.isNaN(d.getTime()) ? iso : d.toLocaleString();
};
const DIRECTION_COLORS: Record<string, string> = {
IMPORT: "blue",
EXPORT: "teal",
DOMESTIC: "violet",
};
/** DOMESTIC displays as "Intercity" — shared with the rest of the platform. */
const DIRECTION_LABELS: Record<string, string> = Freight.TRADE_DIRECTION_LABELS;
function DirectionChip({ direction }: { direction: string }) {
return (
<Badge size="sm" variant="light" color={DIRECTION_COLORS[direction] ?? "gray"}>
{DIRECTION_LABELS[direction] ?? direction}
</Badge>
);
}
function BookingCell({ row }: { row: YardWorkBookingRow }) {
return (
<Group gap={6} wrap="nowrap">
<Text size="sm" fw={600}>
{row.reference ?? row.id.slice(0, 8)}
</Text>
{row.isGovernment && (
<Badge size="xs" variant="light" color="grape">
GOV
</Badge>
)}
</Group>
);
}
function WorkTable({
rows,
side,
trainHere,
onLoad,
onUnload,
pendingBookingId,
}: {
rows: YardWorkBookingRow[];
side: "load" | "unload";
trainHere: boolean;
onLoad: (bookingId: string) => void;
onUnload: (bookingId: string) => void;
pendingBookingId: string | null;
}) {
if (rows.length === 0) {
return (
<Text size="sm" c="dimmed">
{side === "load" ? "No bookings board here." : "No bookings alight here."}
</Text>
);
}
return (
<Table.ScrollContainer minWidth={720}>
<Table verticalSpacing="xs" highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Booking</Table.Th>
<Table.Th>Customer</Table.Th>
<Table.Th>Direction</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>{side === "load" ? "Loaded" : "Arrived"}</Table.Th>
<Table.Th />
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{rows.map((row) => {
const timestamp = side === "load" ? row.loadedAt : row.arrivedAt;
const canAct = side === "load" ? row.canLoad : row.canUnload;
return (
<Table.Tr key={row.id}>
<Table.Td>
<BookingCell row={row} />
</Table.Td>
<Table.Td>
<Text size="sm">{row.customer}</Text>
</Table.Td>
<Table.Td>
<DirectionChip direction={row.tradeDirection} />
</Table.Td>
<Table.Td>
<BookingStatusBadge status={row.status} />
</Table.Td>
<Table.Td>
{timestamp ? (
<Text size="xs" c="dimmed">
{fmtDate(timestamp)}
</Text>
) : (
<Text size="xs" c="dimmed">
</Text>
)}
</Table.Td>
<Table.Td>
<Group gap="xs" justify="flex-end">
{side === "load" ? (
<Tooltip
label={
trainHere
? "Confirm cargo loaded at this yard"
: "Train must be at this yard"
}
disabled={!canAct && Boolean(row.loadedAt)}
>
<Button
size="compact-xs"
variant="light"
leftSection={<PackageCheck size={13} />}
disabled={!canAct || !trainHere}
loading={pendingBookingId === row.id}
onClick={() => onLoad(row.id)}
>
Load
</Button>
</Tooltip>
) : (
<Tooltip
label={
trainHere
? "Confirm cargo unloaded at this yard"
: "Train must be at this yard"
}
disabled={!canAct && Boolean(row.arrivedAt)}
>
<Button
size="compact-xs"
variant="light"
color="orange"
leftSection={<PackageOpen size={13} />}
disabled={!canAct || !trainHere}
loading={pendingBookingId === row.id}
onClick={() => onUnload(row.id)}
>
Unload
</Button>
</Tooltip>
)}
</Group>
</Table.Td>
</Table.Tr>
);
})}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
);
}
/**
* Per-yard load/unload worklist for one schedule — every trade direction. Each
* booking boards at its origin yard and alights at its destination yard; the
* operator confirms both while the train's last recorded checkpoint is at that
* yard (the server validates the position). Unloading stamps the booking's own
* arrival — ARRIVED for import/export, COMPLETED for intercity.
*/
export function YardWorkPanel({ scheduleId }: { scheduleId: string }) {
const { toast } = useToast();
const queryClient = useQueryClient();
const yardWorkQuery = useQuery(
api.trainScheduling.yardWork.queryOptions({
input: { scheduleId },
refetchInterval: 60_000,
}),
);
// Loading/unloading changes booking status on the schedule detail and the
// intercity panel too — refresh all three so no surface shows a stale state.
const invalidate = () => {
void queryClient.invalidateQueries({
queryKey: api.trainScheduling.yardWork.queryKey({ scheduleId }),
});
void queryClient.invalidateQueries({
queryKey: api.trainScheduling.intercityCandidates.queryKey({ scheduleId }),
});
void queryClient.invalidateQueries({
queryKey: api.trainScheduling.scheduleDetail.queryKey({ id: scheduleId }),
});
};
const load = useMutation(
api.trainScheduling.loadScheduleBooking.mutationOptions({
onSuccess: () => {
void invalidate();
toast({ title: "Cargo loaded" });
},
onError: (err) =>
toast({
title: "Load failed",
description: parseError(err, "Could not confirm loading"),
variant: "destructive",
}),
}),
);
const unload = useMutation(
api.trainScheduling.unloadScheduleBooking.mutationOptions({
onSuccess: (result) => {
void invalidate();
toast({
title:
result.status === "COMPLETED"
? "Cargo unloaded — booking completed"
: "Cargo unloaded — booking arrived",
});
},
onError: (err) =>
toast({
title: "Unload failed",
description: parseError(err, "Could not confirm unloading"),
variant: "destructive",
}),
}),
);
const data = yardWorkQuery.data;
const yards: YardWorkYard[] = data?.yards ?? [];
const trainAtYardId = data?.trainAtYardId ?? null;
const pendingLoadId = load.isPending ? (load.variables?.bookingId ?? null) : null;
const pendingUnloadId = unload.isPending ? (unload.variables?.bookingId ?? null) : null;
return (
<Paper withBorder radius="lg" p="lg" mt="md">
<Stack gap="md">
<Group gap="xs">
<MapPin size={18} />
<Text fw={700}>Yard load / unload</Text>
</Group>
{yardWorkQuery.isLoading ? (
<Group gap="xs">
<Loader size="xs" />
<Text size="sm" c="dimmed">
Loading yard worklists
</Text>
</Group>
) : yardWorkQuery.isError ? (
<Alert color="red" variant="light" icon={<AlertCircle size={16} />}>
{parseError(yardWorkQuery.error, "Could not load the yard worklist")}
</Alert>
) : yards.length === 0 ? (
<Text size="sm" c="dimmed">
No bookings are assigned to this schedule yet.
</Text>
) : (
<>
<Text size="sm" c="dimmed">
What boards and alights at each stop. Confirm loading at a booking's
origin and unloading at its destination while the train is at that
yard — unloading stamps the booking's own arrival, even before the
train's final stop.
</Text>
{yards.map((yard, index) => {
const trainHere = trainAtYardId === yard.yardId;
return (
<Stack key={yard.yardId} gap="sm">
{index > 0 && <Divider />}
<Group gap="xs">
<Text fw={600}>{yard.yard}</Text>
{trainHere && (
<Badge
size="sm"
variant="light"
color="edr-green"
leftSection={<TrainFront size={12} />}
>
Train here
</Badge>
)}
</Group>
<Stack gap={6}>
<Text size="sm" fw={600} c="dimmed">
Board here
</Text>
<WorkTable
rows={yard.toLoad}
side="load"
trainHere={trainHere}
onLoad={(bookingId) => load.mutate({ scheduleId, bookingId })}
onUnload={(bookingId) => unload.mutate({ scheduleId, bookingId })}
pendingBookingId={pendingLoadId}
/>
</Stack>
<Stack gap={6}>
<Text size="sm" fw={600} c="dimmed">
Alight here
</Text>
<WorkTable
rows={yard.toUnload}
side="unload"
trainHere={trainHere}
onLoad={(bookingId) => load.mutate({ scheduleId, bookingId })}
onUnload={(bookingId) => unload.mutate({ scheduleId, bookingId })}
pendingBookingId={pendingUnloadId}
/>
</Stack>
</Stack>
);
})}
</>
)}
</Stack>
</Paper>
);
}

View File

@@ -53,7 +53,6 @@ import { FleetAvailabilitySummary } from "@/components/trainScheduling/FleetAvai
import { IntercityRideAlongPanel } from "@/components/trainScheduling/IntercityRideAlongPanel";
import { LegCapacityPanel } from "@/components/trainScheduling/LegCapacityPanel";
import ScheduleHistoryPanel from "@/components/trainScheduling/ScheduleHistoryPanel";
import { YardWorkPanel } from "@/components/trainScheduling/YardWorkPanel";
// import { ImportLoadingConfirmationPanel } from "@/components/trainScheduling/ImportLoadingConfirmationPanel";
import { RescheduleTrainDialog } from "@/components/trainScheduling/RescheduleTrainDialog";
import BookingWindowSettingsModal from "@/components/trainScheduling/BookingWindowSettingsModal";
@@ -1248,7 +1247,6 @@ export default function TrainScheduleV2DetailPage() {
void detailQuery.refetch();
}}
/>
{scheduleId ? <YardWorkPanel scheduleId={scheduleId} /> : null}
{scheduleId ? (
<IntercityRideAlongPanel
scheduleId={scheduleId}