feat: enhance station work logging with user display names and improve loading/unloading logic

This commit is contained in:
Marshal
2026-08-27 02:29:50 +00:00
parent 058535d648
commit 6ec3c8c5e0
6 changed files with 271 additions and 33 deletions

View File

@@ -217,6 +217,20 @@ export function StationWorkControls({
{fmtTime(log!.startedAt!)} {ended ? fmtTime(log!.endedAt!) : "…"} (
{fmtElapsed(log!.startedAt!, log?.endedAt)})
</Text>
{log?.startedByName || log?.endedByName ? (
<Tooltip
label={[
log?.startedByName ? `Started by ${log.startedByName}` : null,
log?.endedByName ? `Ended by ${log.endedByName}` : null,
]
.filter(Boolean)
.join(" · ")}
>
<Badge size="xs" variant="light" color="gray" radius="sm">
{log?.endedByName ?? log?.startedByName}
</Badge>
</Tooltip>
) : null}
<EditTimeButton
label={`${phase} start`}
value={log!.startedAt!}

View File

@@ -5,6 +5,7 @@ import {
ArrowLeft,
CalendarClock,
CheckCircle2,
Clock,
FileText,
Flag,
MapPin,
@@ -13,6 +14,7 @@ import {
Pencil,
Train,
} from "lucide-react";
import { StationWorkControls } from "@/components/trainScheduling/StationWorkControls";
import {
Alert,
Badge,
@@ -653,6 +655,78 @@ export default function TrainScheduleTrackPage() {
</Stack>
</Paper>
{/* ── Loading / unloading windows per station ── */}
<Paper radius="lg" p="lg" withBorder style={CARD_STYLE}>
<SectionHead
icon={<Clock size={17} />}
title="Loading & unloading windows"
hint="Start and end each station's work window — times, duration and who recorded them"
/>
<Stack gap="sm" mt="md">
{track.stations.map((s, i) => {
const isFirst = i === 0;
const isLast = i === track.stations.length - 1;
const workLog = track.stationWorkLogs?.[s.yardId];
return (
<Paper
key={s.yardId}
withBorder
radius="md"
p="sm"
style={{
background:
track.currentSequenceNo === s.sequenceNo
? "var(--mantine-color-green-0)"
: undefined,
}}
>
<Group gap={10} mb={6} wrap="nowrap">
<ThemeIcon size={30} radius="xl" variant="light" color="edr-green">
{isLast ? <Flag size={15} /> : <MapPin size={15} />}
</ThemeIcon>
<Text fw={700} size="sm">
{s.label}
</Text>
{isFirst ? (
<Badge size="xs" variant="light" color="edr-green">
origin
</Badge>
) : null}
{isLast ? (
<Badge size="xs" variant="light" color="gray">
destination
</Badge>
) : null}
{track.currentSequenceNo === s.sequenceNo ? (
<Badge size="xs" variant="filled" color="edr-green">
train here
</Badge>
) : null}
</Group>
<Stack gap={6} pl={40}>
{!isLast ? (
<StationWorkControls
scheduleId={scheduleId ?? ""}
yardId={s.yardId}
phase="loading"
log={workLog?.loading}
/>
) : null}
{!isFirst ? (
<StationWorkControls
scheduleId={scheduleId ?? ""}
yardId={s.yardId}
phase="unloading"
log={workLog?.unloading}
/>
) : null}
</Stack>
</Paper>
);
})}
</Stack>
</Paper>
{/* ── Checkpoint log ── */}
<Paper radius="lg" p="lg" withBorder style={CARD_STYLE}>
<Group justify="space-between" wrap="nowrap" mb="md">

View File

@@ -499,10 +499,16 @@ export default function TrainScheduleV2DetailPage() {
? schedule.stationWorkLogs?.[originYardId]?.loading
: undefined;
const originLoadingStarted = Boolean(originLoadingLog?.startedAt);
const originLoadingEnded = Boolean(originLoadingLog?.endedAt);
const dispatchBoardersKept = pendingOriginBoarders.some(
(b) => b.isGovernment || dispatchLoadedIds.has(b.id),
);
const dispatchNeedsLoadingStart = dispatchBoardersKept && !originLoadingStarted;
// A train never departs mid-loading: once the window opened (or cargo is to
// board), it must be ENDED before dispatch — same gate the server enforces.
const dispatchNeedsLoadingEnd =
(dispatchBoardersKept || originLoadingStarted) && !originLoadingEnded;
const dispatchBlockedByLoading = dispatchNeedsLoadingStart || dispatchNeedsLoadingEnd;
const finalizeStep = hasContainerStep ? 3 : 2;
const canModifyBookings = canEditBookings && !["DISPATCHED", "ARRIVED"].includes(schedule.status);
@@ -966,6 +972,11 @@ export default function TrainScheduleV2DetailPage() {
Start loading before dispatching the ticked bookings are marked
loaded at dispatch, which needs an open loading window.
</Text>
) : dispatchNeedsLoadingEnd ? (
<Text size="xs" c="dimmed">
End the loading window before dispatching a train never departs
mid-loading.
</Text>
) : null}
</Stack>
</Paper>
@@ -1699,14 +1710,18 @@ export default function TrainScheduleV2DetailPage() {
Cancel
</Button>
<Tooltip
label="Start loading at the origin station first — dispatch marks the ticked bookings loaded"
disabled={!dispatchNeedsLoadingStart}
label={
dispatchNeedsLoadingStart
? "Start loading at the origin station first — dispatch marks the ticked bookings loaded"
: "End the loading window at the origin station first — a train never departs mid-loading"
}
disabled={!dispatchBlockedByLoading}
>
<Button
color="edr-green"
leftSection={<Send size={16} />}
loading={dispatch.isPending}
disabled={dispatchNeedsLoadingStart}
disabled={dispatchBlockedByLoading}
onClick={() => void runDispatch()}
>
{hasDispatchWarnings ? "Dispatch anyway" : "Dispatch train"}

View File

@@ -902,6 +902,9 @@ export interface StationWorkPhaseLog {
endedAt?: string | null;
startedByUserId?: string | null;
endedByUserId?: string | null;
/** Resolved display names of the recorders (track endpoint only). */
startedByName?: string | null;
endedByName?: string | null;
}
export interface StationWorkLog {