import { useState } from "react";
import {
Alert,
Badge,
Button,
Checkbox,
Group,
Loader,
Paper,
Stack,
Table,
Text,
Tooltip,
} from "@mantine/core";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AlertCircle, ArrowRight, PackageCheck, PackageOpen, TrainFront } from "lucide-react";
import { api } from "@/services/api";
import { useToast } from "@/hooks/use-toast";
import type {
IntercityBookingRow,
IntercityCapacity,
} 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;
};
// A capacity axis can be null when the schedule's locomotive has no limit
// configured for it — render "—" instead of crashing on toFixed.
function fmt(n: number | null | undefined): string {
if (n == null) return "—";
return Number.isInteger(n) ? String(n) : n.toFixed(1);
}
function CapacityBadges({ capacity }: { capacity: IntercityCapacity | null }) {
if (!capacity) {
return (
Capacity unknown — schedule has no locomotive/train set yet.
);
}
return (
0 ? "teal" : "red"}
>
{fmt(capacity.wagons)} wagons free
0 ? "teal" : "red"}
>
{fmt(capacity.weightTons)} t free
0 ? "teal" : "red"}
>
{fmt(capacity.lengthMeters)} m free
);
}
function NeedCells({ need }: { need: IntercityCapacity | null }) {
if (!need) return
—;
return (
<>
{fmt(need.wagons)}
{fmt(need.weightTons)} t
{fmt(need.lengthMeters)} m
>
);
}
function CorridorCell({ row }: { row: IntercityBookingRow }) {
return (
{row.origin}
{row.destination}
);
}
/**
* Intercity ride-along desk for one import/export schedule: waiting intercity
* bookings whose corridor lies on this train's route, checked against the
* remaining wagon/weight/length budget. Accepting opens the customer's pay
* window; after payment the booking is allocated. Loading/unloading is
* confirmed manually when the train is physically at the booking's origin /
* destination yard (the server validates against recorded checkpoints).
*/
/** Plain-language journey states for the accepted ride-along table. */
const INTERCITY_STATUS_META: Record = {
SELECTED_FOR_BATCH: { label: "Awaiting payment", color: "yellow" },
APPROVED: { label: "Ready to load (gov)", color: "edr-green" },
PAID: { label: "Paid — ready to load", color: "edr-green" },
IN_TRANSIT: { label: "Loaded — in transit", color: "indigo" },
COMPLETED: { label: "Delivered", color: "teal" },
};
export function IntercityRideAlongPanel({
scheduleId,
direction,
}: {
scheduleId: string;
direction: string | null | undefined;
}) {
const { toast } = useToast();
const queryClient = useQueryClient();
const [selected, setSelected] = useState([]);
const candidatesQuery = useQuery(
api.trainScheduling.intercityCandidates.queryOptions({
input: { scheduleId },
refetchInterval: 60_000,
}),
);
// Accepting/loading/unloading a ride-along changes the schedule's booking
// list, the yard worklists AND this panel — refresh all three so the
// workspace board and yard-work tables never show a stale picture.
const invalidate = () => {
void queryClient.invalidateQueries({
queryKey: api.trainScheduling.intercityCandidates.queryKey({ scheduleId }),
});
void queryClient.invalidateQueries({
queryKey: api.trainScheduling.yardWork.queryKey({ scheduleId }),
});
void queryClient.invalidateQueries({
queryKey: api.trainScheduling.scheduleDetail.queryKey({ id: scheduleId }),
});
};
const accept = useMutation(
api.trainScheduling.acceptIntercityBookings.mutationOptions({
onSuccess: (result) => {
setSelected([]);
void invalidate();
if (result.accepted.length > 0) {
toast({
title: `${result.accepted.length} intercity booking(s) accepted`,
description: "Customers have been asked to pay.",
});
}
for (const r of result.rejected) {
toast({
title: "Booking skipped",
description: r.reason,
variant: "destructive",
});
}
},
onError: (err) =>
toast({
title: "Accept failed",
description: parseError(err, "Could not accept intercity bookings"),
variant: "destructive",
}),
}),
);
const load = useMutation(
api.trainScheduling.loadIntercityBooking.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.unloadIntercityBooking.mutationOptions({
onSuccess: () => {
void invalidate();
toast({ title: "Cargo unloaded — booking completed" });
},
onError: (err) =>
toast({
title: "Unload failed",
description: parseError(err, "Could not confirm unloading"),
variant: "destructive",
}),
}),
);
// Intercity bookings only ride import/export trains.
if (direction !== "IMPORT" && direction !== "EXPORT") return null;
const data = candidatesQuery.data;
const candidates = data?.candidates ?? [];
const accepted = data?.accepted ?? [];
if (candidatesQuery.isLoading) {
return (
Loading intercity ride-along bookings…
);
}
if (candidates.length === 0 && accepted.length === 0) return null;
return (
Intercity ride-along
{candidates.length > 0 && (
<>
Waiting intercity bookings whose corridor lies on this train's
route. Accepting opens the customer's payment window against the
free capacity above.
Booking
Customer
Corridor
Wagons
Weight
Length
Fits
{candidates.map((row) => (
setSelected((prev) =>
e.currentTarget.checked
? [...prev, row.id]
: prev.filter((id) => id !== row.id),
)
}
/>
{row.reference ?? row.id.slice(0, 8)}
{row.isGovernment && (
GOV
)}
{row.customer}
{row.fits ? (
Fits
) : (
No room
)}
))}
>
)}
{accepted.length > 0 && (
<>
On this train
Booking
Customer
Corridor
Status
{accepted.map((row) => (
{row.reference ?? row.id.slice(0, 8)}
{row.customer}
{INTERCITY_STATUS_META[row.status ?? ""]?.label ?? row.status}
{row.status === "PAID" && (
}
loading={load.isPending}
onClick={() =>
load.mutate({ scheduleId, bookingId: row.id })
}
>
Load
)}
{row.status === "IN_TRANSIT" && (
}
loading={unload.isPending}
onClick={() =>
unload.mutate({ scheduleId, bookingId: row.id })
}
>
Unload
)}
))}
>
)}
{candidatesQuery.isError && (
}>
{parseError(candidatesQuery.error, "Could not load intercity candidates")}
)}
);
}