fix issues

This commit is contained in:
Marshal
2026-08-28 10:34:02 +00:00
parent ba56974e32
commit 3015de7508
14 changed files with 435 additions and 49 deletions

View File

@@ -1159,6 +1159,18 @@ export default function TrainScheduleV2DetailPage() {
}
/>
{schedule.status === "CANCELLED" && schedule.cancellationReason ? (
<Alert
color="red"
variant="light"
radius="md"
icon={<AlertTriangle size={18} />}
title="This schedule was cancelled"
>
{schedule.cancellationReason}
</Alert>
) : null}
{/* Ops signage: Train No. / Voyage No. / Direction read at a glance from
across the room, so these stay large rather than folding into the
numeric KpiStrip below. */}

View File

@@ -17,6 +17,7 @@ import {
Stack,
Switch,
Text,
Textarea,
TextInput,
ThemeIcon,
} from "@mantine/core";
@@ -150,6 +151,9 @@ export default function TrainScheduleV2ListPage() {
const [dispatchAt, setDispatchAt] = useState<Date | null>(null);
// Cancelling is likewise irreversible — confirmed before the mutation fires.
const [cancelTarget, setCancelTarget] = useState<TrainScheduleListItem | null>(null);
// Required: the reason is stored on the schedule and shown wherever the
// cancelled train appears, so staff downstream know why it died.
const [cancelReason, setCancelReason] = useState("");
const [editDateSchedule, setEditDateSchedule] = useState<TrainScheduleListItem | null>(null);
const [routeId, setRouteId] = useState("");
const [scheduleDate, setScheduleDate] = useState("");
@@ -823,7 +827,10 @@ export default function TrainScheduleV2ListPage() {
confirmed here rather than firing straight from the row menu. */}
<Modal
opened={cancelTarget != null}
onClose={() => setCancelTarget(null)}
onClose={() => {
setCancelTarget(null);
setCancelReason("");
}}
title="Cancel this schedule?"
centered
radius="md"
@@ -842,23 +849,43 @@ export default function TrainScheduleV2ListPage() {
another schedule.
</Text>
) : null}
<Textarea
label="Reason for cancelling"
placeholder="e.g. locomotive failure, track closure, insufficient cargo"
value={cancelReason}
onChange={(e) => setCancelReason(e.currentTarget.value)}
autosize
minRows={2}
maxLength={500}
required
data-autofocus
/>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={() => setCancelTarget(null)}>
<Button
variant="default"
onClick={() => {
setCancelTarget(null);
setCancelReason("");
}}
>
Keep schedule
</Button>
<Button
color="red"
leftSection={<Ban size={16} />}
loading={cancel.isPending}
disabled={!cancelReason.trim()}
onClick={async () => {
if (!cancelTarget) return;
if (!cancelTarget || !cancelReason.trim()) return;
try {
await cancel.mutateAsync({
id: cancelTarget.id,
freightType: cancelTarget.freightType ?? "CONTAINER",
reason: cancelReason.trim(),
});
toast({ title: "Schedule cancelled" });
setCancelTarget(null);
setCancelReason("");
void schedulesQuery.refetch();
} catch (err) {
toast({
@@ -929,6 +956,11 @@ function TrainIdentityCell({ schedule }: { schedule: TrainScheduleListItem }) {
</Text>
<StatusPill status={schedule.status} />
</Group>
{schedule.status === "CANCELLED" && schedule.cancellationReason ? (
<Text size="xs" c="red.7" lh={1.2} truncate title={schedule.cancellationReason}>
{schedule.cancellationReason}
</Text>
) : null}
</Stack>
);
}
@@ -1041,6 +1073,15 @@ function ScheduleCard({
<MetricChip value={schedule.bookingsCount} label="bkg" />
</Group>
{schedule.status === "CANCELLED" && schedule.cancellationReason ? (
<Text size="xs" c="red.7" lineClamp={2}>
<Text span fw={600}>
Cancelled:
</Text>{" "}
{schedule.cancellationReason}
</Text>
) : null}
<Group gap="xs" wrap="nowrap">
<Button
variant="light"

View File

@@ -1043,13 +1043,17 @@ export const api = {
),
cancelSchedule: endpoint<
{ id: string; freightType?: FreightType },
{ id: string; freightType?: FreightType; reason: string },
TrainScheduleDetail
>(
"train-scheduling",
"cancel-schedule",
({ id, freightType }) =>
trainSchedulingService.cancelSchedule(id, freightType ?? "CONTAINER"),
({ id, freightType, reason }) =>
trainSchedulingService.cancelSchedule(
id,
freightType ?? "CONTAINER",
reason,
),
undefined,
() => TRAIN_SCHEDULING_INVALIDATIONS,
),

View File

@@ -801,12 +801,13 @@ export const trainSchedulingService = {
cancelSchedule: async (
id: string,
freightType: FreightType = "CONTAINER",
reason?: string,
): Promise<TrainScheduleDetail> => {
const response = await client.post<TrainScheduleDetail>(
pathsFor(
freightType === "MIXED" ? undefined : freightType,
).CANCEL_SCHEDULE(id),
{},
{ reason },
);
return unwrap(response.data);
},

View File

@@ -287,6 +287,9 @@ export interface BookableSchedule {
freightType?: FreightType | null;
status: TrainScheduleStatus | string;
bookingWindowStatus: "OPEN" | "FULL" | "CLOSED" | string;
/** Why the schedule was cancelled — null unless status is CANCELLED. */
cancellationReason?: string | null;
cancelledAt?: string | null;
maxWagons: number;
remainingWagons: number;
locomotive: { id: string; code: string; name?: string | null } | null;
@@ -637,6 +640,9 @@ export interface TrainScheduleDetail {
id: string;
reference?: string | null;
status: TrainScheduleStatus | string;
/** Why the schedule was cancelled — null unless status is CANCELLED. */
cancellationReason?: string | null;
cancelledAt?: string | null;
deferredBookings?: DeferredBookingRow[];
freightType?: FreightType | null;
trainNumber?: string | null;