mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 14:48:18 +00:00
Merge branch 'freight_feature/payments' of github.com:Tria-plc/edr-platform into freight_feature/payments
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
Paper,
|
Paper,
|
||||||
SimpleGrid,
|
Progress,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
@@ -25,12 +25,8 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
|
||||||
import { RouteCorridorTrack } from "@/components/trainScheduling/RouteCorridorTrack";
|
import { RouteCorridorTrack } from "@/components/trainScheduling/RouteCorridorTrack";
|
||||||
import {
|
import { RouteCorridor, StatusPill } from "@/components/trainScheduling/scheduleVisuals";
|
||||||
RouteCorridor,
|
import { freightBrand } from "@/theme/freight-brand";
|
||||||
StatTile,
|
|
||||||
StatusPill,
|
|
||||||
scheduleBrand,
|
|
||||||
} from "@/components/trainScheduling/scheduleVisuals";
|
|
||||||
import { useTrainTrack, useScheduleMutations } from "@/hooks/trainScheduling/useTrainScheduling";
|
import { useTrainTrack, useScheduleMutations } from "@/hooks/trainScheduling/useTrainScheduling";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
@@ -54,6 +50,33 @@ function formatDateTime(iso?: string | null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Compact icon + label + value cell used in the header meta strip. */
|
||||||
|
function MetaStat({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
|
||||||
|
<ThemeIcon size={32} radius="md" variant="light" color="green">
|
||||||
|
{icon}
|
||||||
|
</ThemeIcon>
|
||||||
|
<Stack gap={0} style={{ minWidth: 0 }}>
|
||||||
|
<Text size="10px" fw={700} tt="uppercase" c="dimmed" style={{ letterSpacing: 0.5 }}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={700} c="dark.5" truncate>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function TrainScheduleTrackPage() {
|
export default function TrainScheduleTrackPage() {
|
||||||
const { scheduleId } = useParams<{ scheduleId: string }>();
|
const { scheduleId } = useParams<{ scheduleId: string }>();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -63,7 +86,7 @@ export default function TrainScheduleTrackPage() {
|
|||||||
if (trackQuery.isLoading) {
|
if (trackQuery.isLoading) {
|
||||||
return (
|
return (
|
||||||
<Group justify="center" py="xl">
|
<Group justify="center" py="xl">
|
||||||
<Loader size="sm" />
|
<Loader size="sm" color="green" />
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -80,7 +103,9 @@ export default function TrainScheduleTrackPage() {
|
|||||||
const canLog = track.status === "DISPATCHED";
|
const canLog = track.status === "DISPATCHED";
|
||||||
const totalStations = track.stations.length;
|
const totalStations = track.stations.length;
|
||||||
const reached = Math.min(track.currentSequenceNo + 1, totalStations);
|
const reached = Math.min(track.currentSequenceNo + 1, totalStations);
|
||||||
const progressLabel = `${reached} / ${totalStations}`;
|
const progressPct = totalStations > 1 ? (track.currentSequenceNo / (totalStations - 1)) * 100 : 0;
|
||||||
|
const clampedPct = Math.min(100, Math.max(0, progressPct));
|
||||||
|
const currentStation = track.stations[Math.max(0, track.currentSequenceNo)]?.label ?? "—";
|
||||||
|
|
||||||
const handleLog = (sequenceNo: number) => {
|
const handleLog = (sequenceNo: number) => {
|
||||||
const isFinal = sequenceNo === track.stations[totalStations - 1]?.sequenceNo;
|
const isFinal = sequenceNo === track.stations[totalStations - 1]?.sequenceNo;
|
||||||
@@ -105,7 +130,7 @@ export default function TrainScheduleTrackPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="md" px={{ base: "xs", sm: 0 }} py="md" maw={1080} mx="auto" w="100%">
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/dashboard/operations/train-scheduling-v2/${scheduleId}`}
|
to={`/dashboard/operations/train-scheduling-v2/${scheduleId}`}
|
||||||
@@ -118,89 +143,111 @@ export default function TrainScheduleTrackPage() {
|
|||||||
Back to schedule
|
Back to schedule
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Hero */}
|
{/* Header */}
|
||||||
<Paper
|
<Paper radius="lg" withBorder p="lg" style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
||||||
radius="xl"
|
<Stack gap="lg">
|
||||||
p="xl"
|
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
||||||
style={{
|
<Group gap="md" align="flex-start" wrap="nowrap" style={{ minWidth: 0 }}>
|
||||||
position: "relative",
|
<Box
|
||||||
overflow: "hidden",
|
style={{
|
||||||
background: scheduleBrand.heroGradient,
|
width: 48,
|
||||||
boxShadow: scheduleBrand.shadow,
|
height: 48,
|
||||||
}}
|
borderRadius: 12,
|
||||||
>
|
display: "flex",
|
||||||
<Box
|
alignItems: "center",
|
||||||
style={{
|
justifyContent: "center",
|
||||||
position: "absolute",
|
background: freightBrand.gradient,
|
||||||
top: -90,
|
color: "white",
|
||||||
right: -50,
|
flexShrink: 0,
|
||||||
width: 280,
|
}}
|
||||||
height: 280,
|
>
|
||||||
borderRadius: "50%",
|
<Navigation size={24} />
|
||||||
background: "rgba(255,255,255,0.10)",
|
</Box>
|
||||||
pointerEvents: "none",
|
<Stack gap={6} style={{ minWidth: 0 }}>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack gap="lg" style={{ position: "relative" }}>
|
|
||||||
<Group justify="space-between" align="flex-start" wrap="wrap">
|
|
||||||
<Group gap="md" align="flex-start" wrap="nowrap">
|
|
||||||
<ThemeIcon size={56} radius="lg" variant="white" style={{ color: "var(--mantine-color-green-7)" }}>
|
|
||||||
<Navigation size={28} />
|
|
||||||
</ThemeIcon>
|
|
||||||
<Stack gap={6}>
|
|
||||||
<Group gap="sm" align="center" wrap="wrap">
|
<Group gap="sm" align="center" wrap="wrap">
|
||||||
<Title order={2} c="white" fw={700}>
|
<Title order={3} fw={800}>
|
||||||
Track train
|
Train tracking
|
||||||
</Title>
|
</Title>
|
||||||
{track.trainNumber ? (
|
{track.trainNumber ? (
|
||||||
<Badge variant="white" c="green.8" radius="sm" style={{ fontWeight: 600 }}>
|
<Badge variant="light" color="green" radius="sm">
|
||||||
{track.trainNumber}
|
{track.trainNumber}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{track.direction ? (
|
{track.direction ? (
|
||||||
<Badge variant="white" c="green.8" radius="sm">
|
<Badge variant="light" color="gray" radius="sm">
|
||||||
{track.direction}
|
{track.direction}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
</Group>
|
</Group>
|
||||||
<Box maw={340}>
|
<Box maw={360}>
|
||||||
<RouteCorridor onDark origin={track.origin} destination={track.destination} />
|
<RouteCorridor origin={track.origin} destination={track.destination} variant="compact" />
|
||||||
</Box>
|
</Box>
|
||||||
<StatusPill status={track.status} />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
|
<StatusPill status={track.status} />
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="md">
|
{/* Journey progress */}
|
||||||
<StatTile onDark icon={Train} label="Progress" value={progressLabel} hint="stations reached" />
|
<Box>
|
||||||
<StatTile onDark icon={MapPin} label="Current" value={track.stations[Math.max(0, track.currentSequenceNo)]?.label ?? "—"} />
|
<Group justify="space-between" mb={6}>
|
||||||
<StatTile onDark icon={CalendarClock} label="Departed" value={formatDateTime(track.actualDepartureAt)} />
|
<Text size="xs" fw={700} c="gray.7" tt="uppercase" style={{ letterSpacing: 0.4 }}>
|
||||||
<StatTile onDark icon={Flag} label="Arrived" value={formatDateTime(track.actualArrivalAt)} />
|
Journey progress
|
||||||
</SimpleGrid>
|
</Text>
|
||||||
|
<Text size="xs" fw={700} c="green.8">
|
||||||
|
{reached} / {totalStations} stations · {Math.round(clampedPct)}%
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Progress
|
||||||
|
value={clampedPct}
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
color="green"
|
||||||
|
striped={track.status === "DISPATCHED"}
|
||||||
|
animated={track.status === "DISPATCHED"}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Meta strip */}
|
||||||
|
<Group justify="space-between" wrap="wrap" gap="lg">
|
||||||
|
<MetaStat icon={<MapPin size={16} />} label="Current" value={currentStation} />
|
||||||
|
<MetaStat
|
||||||
|
icon={<CalendarClock size={16} />}
|
||||||
|
label="Departed"
|
||||||
|
value={formatDateTime(track.actualDepartureAt)}
|
||||||
|
/>
|
||||||
|
<MetaStat
|
||||||
|
icon={<Flag size={16} />}
|
||||||
|
label="Arrived"
|
||||||
|
value={formatDateTime(track.actualArrivalAt)}
|
||||||
|
/>
|
||||||
|
<MetaStat
|
||||||
|
icon={<Train size={16} />}
|
||||||
|
label="Stations"
|
||||||
|
value={`${reached} of ${totalStations}`}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Corridor */}
|
{/* Corridor */}
|
||||||
<Paper radius="xl" p="lg" withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
<Paper radius="lg" p="lg" withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
||||||
<Stack gap="lg">
|
<Stack gap="md">
|
||||||
<Group justify="space-between" align="center" wrap="nowrap">
|
<Group gap="sm" align="center" wrap="nowrap">
|
||||||
<Group gap="md" align="center" wrap="nowrap">
|
<ThemeIcon size={34} radius="md" variant="light" color="green">
|
||||||
<ThemeIcon size={44} radius="md" variant="gradient" gradient={{ from: "green", to: "teal", deg: 135 }}>
|
<Navigation size={17} />
|
||||||
<Navigation size={22} />
|
</ThemeIcon>
|
||||||
</ThemeIcon>
|
<Stack gap={0}>
|
||||||
<Stack gap={2}>
|
<Text fw={800} size="sm">
|
||||||
<Title order={4} fw={700}>
|
Route corridor
|
||||||
Route corridor
|
</Text>
|
||||||
</Title>
|
<Text size="xs" c="dimmed">
|
||||||
<Text size="sm" c="dimmed">
|
{canLog
|
||||||
{canLog
|
? "Log the train passing each station; the final station marks arrival."
|
||||||
? "Log the train passing each station; the final station marks arrival."
|
: track.status === "ARRIVED"
|
||||||
: track.status === "ARRIVED"
|
? "This train has arrived at its destination."
|
||||||
? "This train has arrived at its destination."
|
: "Tracking becomes available once the train is dispatched."}
|
||||||
: "Tracking becomes available once the train is dispatched."}
|
</Text>
|
||||||
</Text>
|
</Stack>
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<RouteCorridorTrack
|
<RouteCorridorTrack
|
||||||
@@ -216,47 +263,68 @@ export default function TrainScheduleTrackPage() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Timeline */}
|
{/* Checkpoint log */}
|
||||||
<Paper radius="xl" p="lg" withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
<Paper radius="lg" p="lg" withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
||||||
<Stack gap="md">
|
<Group gap="sm" align="center" wrap="nowrap" mb="md">
|
||||||
<Title order={5} fw={700}>
|
<ThemeIcon size={34} radius="md" variant="light" color="green">
|
||||||
Checkpoint log
|
<CheckCircle2 size={17} />
|
||||||
</Title>
|
</ThemeIcon>
|
||||||
{track.checkpoints.length === 0 ? (
|
<Stack gap={0}>
|
||||||
<Text size="sm" c="dimmed">
|
<Text fw={800} size="sm">
|
||||||
No checkpoints logged yet.
|
Checkpoint log
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
<Text size="xs" c="dimmed">
|
||||||
<Timeline active={track.checkpoints.length} bulletSize={22} lineWidth={2} color="green">
|
{track.checkpoints.length} event{track.checkpoints.length === 1 ? "" : "s"} recorded
|
||||||
{track.checkpoints.map((cp) => (
|
</Text>
|
||||||
<Timeline.Item
|
</Stack>
|
||||||
key={cp.id}
|
</Group>
|
||||||
bullet={cp.kind === "ARRIVED" ? <CheckCircle2 size={13} /> : <MapPin size={12} />}
|
|
||||||
title={
|
{track.checkpoints.length === 0 ? (
|
||||||
<Group gap="sm">
|
<Stack align="center" gap="xs" py="xl">
|
||||||
<Text fw={600} size="sm">
|
<ThemeIcon size={44} radius="xl" variant="light" color="gray">
|
||||||
{cp.label ?? `Station ${cp.sequenceNo}`}
|
<MapPin size={20} />
|
||||||
</Text>
|
</ThemeIcon>
|
||||||
<Badge
|
<Text size="sm" fw={600} c="gray.7">
|
||||||
size="xs"
|
No checkpoints yet
|
||||||
radius="sm"
|
</Text>
|
||||||
variant="light"
|
<Text size="xs" c="dimmed" ta="center" maw={300}>
|
||||||
color={cp.kind === "ARRIVED" ? "teal" : cp.kind === "DEPARTED" ? "blue" : "green"}
|
Each station the train passes will be logged here with its timestamp.
|
||||||
>
|
</Text>
|
||||||
{cp.kind}
|
</Stack>
|
||||||
</Badge>
|
) : (
|
||||||
</Group>
|
<Timeline active={track.checkpoints.length} bulletSize={22} lineWidth={2} color="green">
|
||||||
}
|
{track.checkpoints.map((cp) => (
|
||||||
>
|
<Timeline.Item
|
||||||
<Text size="xs" c="dimmed">
|
key={cp.id}
|
||||||
{formatDateTime(cp.occurredAt)}
|
bullet={cp.kind === "ARRIVED" ? <CheckCircle2 size={13} /> : <MapPin size={12} />}
|
||||||
|
title={
|
||||||
|
<Group gap="sm">
|
||||||
|
<Text fw={700} size="sm">
|
||||||
|
{cp.label ?? `Station ${cp.sequenceNo}`}
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
size="xs"
|
||||||
|
radius="sm"
|
||||||
|
variant="light"
|
||||||
|
color={cp.kind === "ARRIVED" ? "teal" : cp.kind === "DEPARTED" ? "blue" : "green"}
|
||||||
|
>
|
||||||
|
{cp.kind}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{formatDateTime(cp.occurredAt)}
|
||||||
|
</Text>
|
||||||
|
{cp.note ? (
|
||||||
|
<Text size="xs" mt={2}>
|
||||||
|
{cp.note}
|
||||||
</Text>
|
</Text>
|
||||||
{cp.note ? <Text size="xs">{cp.note}</Text> : null}
|
) : null}
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
))}
|
))}
|
||||||
</Timeline>
|
</Timeline>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
Submodule apps/edr-freight-web/backoffice/user-management deleted from 7c2e1d9bdf
Reference in New Issue
Block a user