export flow and fix intercity issue

This commit is contained in:
Marshal
2026-07-30 13:11:35 +00:00
parent 9208050cb5
commit e80aac877f
19 changed files with 486 additions and 27 deletions

View File

@@ -0,0 +1,131 @@
import { Badge, Box, Group, Loader, Stack, Text, UnstyledButton } from "@mantine/core";
import { CheckCircle2, TrainFront } from "lucide-react";
import type { Freight } from "@edr/types";
const BORDER = "#E6ECF2";
const SELECTED = "#0E7A5F";
function departureLabel(iso: string): string {
const d = new Date(iso);
return d.toLocaleString("en-GB", {
weekday: "short",
day: "2-digit",
month: "short",
hour: "2-digit",
minute: "2-digit",
timeZone: "Africa/Addis_Ababa",
});
}
function closesLabel(iso: string | null): string | null {
if (!iso) return null;
return new Date(iso).toLocaleString("en-GB", {
day: "2-digit",
month: "short",
hour: "2-digit",
minute: "2-digit",
timeZone: "Africa/Addis_Ababa",
});
}
export interface ExportTrainPickerProps {
options: Freight.ExportTrainOption[];
loading: boolean;
value: string;
onChange: (scheduleId: string) => void;
}
/**
* Export shipment-day train picker: one card per export train that day, with
* live free-wagon space per wagon type for THIS booking's cargo. Full or
* not-yet-open trains render disabled — the pick locks the booking onto that
* train when the operation request is submitted.
*/
export function ExportTrainPicker({
options,
loading,
value,
onChange,
}: ExportTrainPickerProps) {
if (loading) {
return (
<Group gap="xs" mt="sm">
<Loader size="xs" />
<Text fz="12px" c="dimmed">
Checking trains for this day
</Text>
</Group>
);
}
if (!options.length) return null;
return (
<Box mt="sm">
<Text fz="13px" fw={700} c="#10202F" mb={6}>
Choose your train
</Text>
<Stack gap="xs">
{options.map((option) => {
const bookable = option.isOpen && option.fits;
const selected = value === option.scheduleId;
const closes = closesLabel(option.bookingClosesAt);
return (
<UnstyledButton
key={option.scheduleId}
onClick={() => bookable && onChange(option.scheduleId)}
disabled={!bookable}
style={{
border: `1.5px solid ${selected ? SELECTED : BORDER}`,
borderRadius: 10,
padding: "10px 12px",
opacity: bookable ? 1 : 0.55,
cursor: bookable ? "pointer" : "not-allowed",
background: selected ? "#F2FAF7" : "#FFFFFF",
}}
>
<Group justify="space-between" align="flex-start" wrap="nowrap">
<Group gap="xs" align="flex-start" wrap="nowrap">
<TrainFront size={16} color={selected ? SELECTED : "#5B6B7A"} />
<Box>
<Text fz="13px" fw={600} c="#10202F">
Departs {departureLabel(option.departure)} EAT
</Text>
<Text fz="12px" c="dimmed">
{option.freeWagons} wagon{option.freeWagons === 1 ? "" : "s"} free
for your cargo · you need {option.neededWagons}
{closes ? ` · booking closes ${closes} EAT` : ""}
</Text>
<Group gap={6} mt={4}>
{option.byWagonType.map((t) => (
<Badge
key={t.wagonTypeId ?? "default"}
size="sm"
variant="light"
color={t.freeWagons > 0 ? "teal" : "gray"}
>
{t.code ?? t.name ?? "Wagon"}: {t.freeWagons} free
</Badge>
))}
</Group>
</Box>
</Group>
{selected ? (
<CheckCircle2 size={18} color={SELECTED} />
) : !option.isOpen ? (
<Badge size="sm" color="gray" variant="light">
Not open
</Badge>
) : !option.fits ? (
<Badge size="sm" color="red" variant="light">
Too little space
</Badge>
) : null}
</Group>
</UnstyledButton>
);
})}
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,2 @@
export { ExportTrainPicker } from "./ExportTrainPicker";
export type { ExportTrainPickerProps } from "./ExportTrainPicker";

View File

@@ -24,6 +24,8 @@ export { useFileViewer } from "./hooks/useFileViewer";
export { OperationDatePicker } from "./components/OperationDatePicker";
export type { OperationDatePickerProps } from "./components/OperationDatePicker";
export { ExportTrainPicker } from "./components/ExportTrainPicker";
export type { ExportTrainPickerProps } from "./components/ExportTrainPicker";
export { CountdownTimer } from "./components/CountdownTimer";
export type { CountdownTimerProps } from "./components/CountdownTimer";