mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 11:21:18 +00:00
Replace wagon/locomotive readiness with yard tracking, assign unassigned bookings from origin-yard fleet, standardize rates on USD with CBE ETB conversion, and update fleet/scheduling UI
This commit is contained in:
@@ -50,30 +50,20 @@ export const CompositionBookingTabs = ({
|
||||
const unassignedQuery = useUnassignedBookings(scheduleId);
|
||||
const removalsQuery = useCompositionRemovals(scheduleId);
|
||||
|
||||
const { assignedCount, freeWagons, freeWeightTons } = useMemo(() => {
|
||||
const { assignedCount } = useMemo(() => {
|
||||
const wagons = scheduleDetail.trainSet?.wagons ?? [];
|
||||
const ids = new Set<string>();
|
||||
let usedWeight = 0;
|
||||
let empty = 0;
|
||||
for (const w of wagons) {
|
||||
const allocs = w.allocations ?? [];
|
||||
if (allocs.length === 0) empty += 1;
|
||||
for (const a of allocs) {
|
||||
for (const a of w.allocations ?? []) {
|
||||
ids.add(a.bookingId);
|
||||
usedWeight += a.allocatedWeightTons ?? 0;
|
||||
}
|
||||
}
|
||||
const maxWeight = scheduleDetail.trainSet?.locomotive?.maxPullWeightTons ?? null;
|
||||
return {
|
||||
assignedCount: ids.size,
|
||||
freeWagons: empty,
|
||||
freeWeightTons: maxWeight != null ? Math.max(0, maxWeight - usedWeight) : null,
|
||||
};
|
||||
}, [scheduleDetail.trainSet?.wagons, scheduleDetail.trainSet?.locomotive?.maxPullWeightTons]);
|
||||
return { assignedCount: ids.size };
|
||||
}, [scheduleDetail.trainSet?.wagons]);
|
||||
|
||||
const counts: Record<TabKey, number> = {
|
||||
assigned: assignedCount,
|
||||
unassigned: unassignedQuery.data?.length ?? 0,
|
||||
unassigned: unassignedQuery.data?.bookings?.length ?? 0,
|
||||
payment: awaitingPayment.length,
|
||||
expired: expired.length,
|
||||
removed: removalsQuery.data?.length ?? 0,
|
||||
@@ -188,8 +178,6 @@ export const CompositionBookingTabs = ({
|
||||
scheduleId={scheduleId}
|
||||
selectedBookingId={selectedBookingId}
|
||||
onSelect={handleSelect}
|
||||
freeWagons={freeWagons}
|
||||
freeWeightTons={freeWeightTons}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import { Badge, Box, Button, Card, Group, Stack, Text, ThemeIcon, Tooltip } from "@mantine/core";
|
||||
import { AlertTriangle, Container as ContainerIcon, Plus, TrainFront, Weight } from "lucide-react";
|
||||
import { AlertTriangle, Container as ContainerIcon, MapPin, Plus, TrainFront } from "lucide-react";
|
||||
import {
|
||||
useUnassignedBookings,
|
||||
useScheduleMutations,
|
||||
} from "@/hooks/trainScheduling/useTrainScheduling";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import type { FleetAvailabilityRow } from "@/types/trainScheduling";
|
||||
import type { BookingDetailData } from "./BookingDetailModal";
|
||||
|
||||
interface UnassignedBookingsPanelProps {
|
||||
scheduleId: string;
|
||||
selectedBookingId?: string | null;
|
||||
onSelect: (booking: BookingDetailData) => void;
|
||||
/** Empty wagon slots currently available on the train. */
|
||||
freeWagons: number;
|
||||
/** Remaining pull-weight headroom in tons, or null when no locomotive limit. */
|
||||
freeWeightTons: number | null;
|
||||
}
|
||||
|
||||
const parseError = (error: unknown): string | null => {
|
||||
if (error && typeof error === "object" && "response" in error) {
|
||||
const resp = (error as { response?: { data?: { message?: unknown } } }).response;
|
||||
const resp = (error as {
|
||||
response?: { data?: { message?: unknown; violations?: string[] } };
|
||||
}).response;
|
||||
const violations = resp?.data?.violations;
|
||||
if (Array.isArray(violations) && violations.length) return violations.join("; ");
|
||||
const msg = resp?.data?.message;
|
||||
if (Array.isArray(msg)) return msg.join(", ");
|
||||
if (typeof msg === "string") return msg;
|
||||
@@ -27,29 +28,65 @@ const parseError = (error: unknown): string | null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const YardFleetBanner = ({ fleetAtOrigin }: { fleetAtOrigin: FleetAvailabilityRow[] }) => {
|
||||
if (!fleetAtOrigin.length) {
|
||||
return (
|
||||
<Group gap={5} wrap="nowrap" px="sm" py={6} style={{ borderRadius: 8, background: "var(--mantine-color-red-0)", border: "1px solid var(--mantine-color-red-2)" }}>
|
||||
<MapPin size={13} color="var(--mantine-color-red-6)" />
|
||||
<Text size="xs" fw={700} c="red.7">
|
||||
No wagons at origin yard
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Group
|
||||
gap="xs"
|
||||
wrap="wrap"
|
||||
px="sm"
|
||||
py={6}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: "var(--mantine-color-green-0)",
|
||||
border: "1px solid var(--mantine-color-green-1)",
|
||||
}}
|
||||
>
|
||||
<Group gap={5} wrap="nowrap">
|
||||
<MapPin size={13} color="var(--mantine-color-green-7)" />
|
||||
<Text size="xs" fw={700} c="green.8">
|
||||
Origin yard
|
||||
</Text>
|
||||
</Group>
|
||||
{fleetAtOrigin.map((row) => (
|
||||
<Badge key={row.wagonTypeId} size="sm" variant="light" color="green">
|
||||
{row.wagonTypeCode}: {row.available}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export const UnassignedBookingsPanel = ({
|
||||
scheduleId,
|
||||
selectedBookingId,
|
||||
onSelect,
|
||||
freeWagons,
|
||||
freeWeightTons,
|
||||
}: UnassignedBookingsPanelProps) => {
|
||||
const { toast } = useToast();
|
||||
const unassignedQuery = useUnassignedBookings(scheduleId);
|
||||
const assignMutation = useScheduleMutations(scheduleId).assign;
|
||||
const assignMutation = useScheduleMutations(scheduleId).assignUnassigned;
|
||||
|
||||
const handleAssign = async (bookingId: string, reference: string | null) => {
|
||||
try {
|
||||
await assignMutation.mutateAsync({
|
||||
id: scheduleId,
|
||||
payload: { bookingIds: [bookingId] },
|
||||
bookingId,
|
||||
});
|
||||
toast({ title: `Assigned ${reference ?? "booking"} to the train` });
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Could not assign booking",
|
||||
description:
|
||||
parseError(err) ?? "No free wagon or not enough space for this booking.",
|
||||
description: parseError(err) ?? "Assignment failed — check yard fleet and train limits.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
@@ -63,7 +100,8 @@ export const UnassignedBookingsPanel = ({
|
||||
);
|
||||
}
|
||||
|
||||
const bookings = unassignedQuery.data ?? [];
|
||||
const bookings = unassignedQuery.data?.bookings ?? [];
|
||||
const fleetAtOrigin = unassignedQuery.data?.fleetAtOrigin ?? [];
|
||||
|
||||
if (bookings.length === 0) {
|
||||
return (
|
||||
@@ -81,53 +119,15 @@ export const UnassignedBookingsPanel = ({
|
||||
);
|
||||
}
|
||||
|
||||
const noFreeWagon = freeWagons <= 0;
|
||||
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
{/* Capacity availability banner */}
|
||||
<Group
|
||||
justify="space-between"
|
||||
wrap="nowrap"
|
||||
px="sm"
|
||||
py={6}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: noFreeWagon ? "var(--mantine-color-red-0)" : "var(--mantine-color-green-0)",
|
||||
border: `1px solid ${
|
||||
noFreeWagon ? "var(--mantine-color-red-2)" : "var(--mantine-color-green-1)"
|
||||
}`,
|
||||
}}
|
||||
>
|
||||
<Group gap={5} wrap="nowrap">
|
||||
<TrainFront
|
||||
size={13}
|
||||
color={noFreeWagon ? "var(--mantine-color-red-6)" : "var(--mantine-color-green-7)"}
|
||||
/>
|
||||
<Text size="xs" fw={700} c={noFreeWagon ? "red.7" : "green.8"}>
|
||||
{freeWagons} free wagon{freeWagons === 1 ? "" : "s"}
|
||||
</Text>
|
||||
</Group>
|
||||
{freeWeightTons != null ? (
|
||||
<Group gap={5} wrap="nowrap">
|
||||
<Weight size={13} color="var(--mantine-color-gray-6)" />
|
||||
<Text size="xs" c="dimmed">
|
||||
{freeWeightTons.toFixed(1)} T headroom
|
||||
</Text>
|
||||
</Group>
|
||||
) : null}
|
||||
</Group>
|
||||
<YardFleetBanner fleetAtOrigin={fleetAtOrigin} />
|
||||
|
||||
{bookings.map((booking) => {
|
||||
const isActive = selectedBookingId === booking.id;
|
||||
const weight = booking.cargoTotalWeightVgm ?? 0;
|
||||
const overWeight = freeWeightTons != null && weight > freeWeightTons;
|
||||
const fits = !noFreeWagon && !overWeight;
|
||||
const blockReason = noFreeWagon
|
||||
? "No free wagon on this train"
|
||||
: overWeight
|
||||
? "Exceeds remaining weight headroom"
|
||||
: null;
|
||||
const weight = Number(booking.cargoTotalWeightVgm ?? 0);
|
||||
const fits = booking.canAssign;
|
||||
const blockReason = booking.blockReason;
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -141,7 +141,7 @@ export const UnassignedBookingsPanel = ({
|
||||
reference: booking.reference,
|
||||
company: null,
|
||||
freightType: booking.freightType,
|
||||
weightTons: booking.cargoTotalWeightVgm ?? null,
|
||||
weightTons: Number.isFinite(weight) ? weight : null,
|
||||
status: booking.status,
|
||||
priorityScore: booking.priorityScore,
|
||||
})
|
||||
@@ -172,9 +172,9 @@ export const UnassignedBookingsPanel = ({
|
||||
{booking.freightType}
|
||||
</Badge>
|
||||
<Group gap={3} wrap="nowrap">
|
||||
<Weight size={11} color="var(--mantine-color-gray-6)" />
|
||||
<TrainFront size={11} color="var(--mantine-color-gray-6)" />
|
||||
<Text size="11px" c="dimmed">
|
||||
{weight.toFixed(1)} T
|
||||
{booking.wagonsRequired}× {booking.requiredWagonTypeCode}
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
@@ -202,7 +202,7 @@ export const UnassignedBookingsPanel = ({
|
||||
}}
|
||||
loading={
|
||||
assignMutation.isPending &&
|
||||
assignMutation.variables?.payload.bookingIds?.[0] === booking.id
|
||||
assignMutation.variables?.bookingId === booking.id
|
||||
}
|
||||
leftSection={<Plus size={12} />}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user