mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 16:35:42 +00:00
fix
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
import { FormEvent, useMemo, useState } from "react";
|
||||
import { Ban, CircleCheck, Edit, Eye, Plus, Route as RouteIcon, Trash2 } from "lucide-react";
|
||||
import {
|
||||
ArrowRight,
|
||||
Ban,
|
||||
CircleCheck,
|
||||
Edit,
|
||||
Eye,
|
||||
Plus,
|
||||
Route as RouteIcon,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import type { ColumnDef } from "@edr/ui-common";
|
||||
import {
|
||||
ActionIcon,
|
||||
@@ -7,13 +16,16 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Group,
|
||||
Modal,
|
||||
NumberInput,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
|
||||
@@ -26,22 +38,51 @@ import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
||||
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
||||
import { api } from "@/services/api";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import type { RouteRecord, YardRef } from "@/services/routes.service";
|
||||
import {
|
||||
formatRouteLabel,
|
||||
ROUTE_STATUS_OPTIONS,
|
||||
totalRouteDistanceKm,
|
||||
type RouteRecord,
|
||||
type RouteStatus,
|
||||
type YardRef,
|
||||
} from "@/services/routes.service";
|
||||
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
|
||||
|
||||
type MilestoneFormRow = { yardId: string; distanceKm: string };
|
||||
|
||||
type RouteFormState = {
|
||||
name: string;
|
||||
milestones: string[];
|
||||
status: RouteStatus;
|
||||
milestones: MilestoneFormRow[];
|
||||
};
|
||||
|
||||
const emptyForm = (): RouteFormState => ({ name: "", milestones: ["", ""] });
|
||||
const emptyForm = (): RouteFormState => ({
|
||||
status: "AVAILABLE",
|
||||
milestones: [
|
||||
{ yardId: "", distanceKm: "0" },
|
||||
{ yardId: "", distanceKm: "" },
|
||||
],
|
||||
});
|
||||
|
||||
const yardLabel = (yard?: YardRef | null) => (yard ? `${yard.label} (${yard.code})` : "—");
|
||||
const yardLabel = (yard?: YardRef | null) =>
|
||||
yard ? `${yard.label} (${yard.code})` : "—";
|
||||
|
||||
const routeStops = (route: RouteRecord) =>
|
||||
(route.milestones ?? [])
|
||||
.sort((left, right) => left.sequenceNo - right.sequenceNo)
|
||||
.map((milestone) => milestone.yard?.label ?? milestone.yard?.code ?? milestone.yardId);
|
||||
const statusColor = (status: RouteStatus) => {
|
||||
switch (status) {
|
||||
case "AVAILABLE":
|
||||
return "edr-green";
|
||||
case "MAINTENANCE":
|
||||
return "yellow";
|
||||
case "DAMAGED":
|
||||
return "red";
|
||||
case "STOP_WORKING":
|
||||
return "gray";
|
||||
default:
|
||||
return "gray";
|
||||
}
|
||||
};
|
||||
|
||||
const statusLabel = (status: RouteStatus) =>
|
||||
ROUTE_STATUS_OPTIONS.find((o) => o.value === status)?.label ?? status;
|
||||
|
||||
const normalizeRouteError = (error: unknown) => {
|
||||
const responseData = (error as { response?: { data?: unknown } })?.response?.data;
|
||||
@@ -57,6 +98,60 @@ const normalizeRouteError = (error: unknown) => {
|
||||
: "Save failed";
|
||||
};
|
||||
|
||||
function RouteTimeline({ route }: { route: RouteRecord }) {
|
||||
const stops = [...(route.milestones ?? [])].sort(
|
||||
(a, b) => a.sequenceNo - b.sequenceNo,
|
||||
);
|
||||
const total = totalRouteDistanceKm(route);
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
{stops.map((milestone, index) => {
|
||||
const label =
|
||||
milestone.yard?.label ?? milestone.yard?.code ?? milestone.yardId;
|
||||
const role =
|
||||
index === 0
|
||||
? "Origin"
|
||||
: index === stops.length - 1
|
||||
? "Destination"
|
||||
: `Milestone ${index}`;
|
||||
const km = Number(milestone.distanceKm ?? 0);
|
||||
return (
|
||||
<Box key={milestone.id ?? `${milestone.yardId}-${index}`}>
|
||||
{index > 0 && (
|
||||
<Group gap={8} pl={18} py={6}>
|
||||
<ThemeIcon size={22} radius="xl" variant="light" color="gray">
|
||||
<ArrowRight size={12} />
|
||||
</ThemeIcon>
|
||||
<Text size="xs" c="dimmed" fw={600}>
|
||||
{km} km
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<Badge size="sm" variant="light" color={index === 0 ? "teal" : "gray"}>
|
||||
{role}
|
||||
</Badge>
|
||||
<Text size="sm" fw={500}>
|
||||
{label}
|
||||
</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
<Divider />
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" fw={600}>
|
||||
Total distance
|
||||
</Text>
|
||||
<Text size="sm" fw={700}>
|
||||
{total} km
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RoutesPage() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
@@ -78,12 +173,14 @@ export default function RoutesPage() {
|
||||
if (!query) return routesQuery.data ?? [];
|
||||
return (routesQuery.data ?? []).filter((route) => {
|
||||
const searchable = [
|
||||
route.name,
|
||||
formatRouteLabel(route),
|
||||
route.originYard?.label,
|
||||
route.originYard?.code,
|
||||
route.destinationYard?.label,
|
||||
route.destinationYard?.code,
|
||||
...routeStops(route),
|
||||
...(route.milestones ?? []).map(
|
||||
(m) => m.yard?.label ?? m.yard?.code ?? m.yardId,
|
||||
),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
@@ -99,7 +196,7 @@ export default function RoutesPage() {
|
||||
}, [filteredRoutes, pagination.pageIndex, pagination.pageSize]);
|
||||
|
||||
const allRoutes = routesQuery.data ?? [];
|
||||
const activeCount = allRoutes.filter((route) => route.isActive).length;
|
||||
const availableCount = allRoutes.filter((r) => r.status === "AVAILABLE").length;
|
||||
|
||||
const yardOptions = useMemo(
|
||||
() =>
|
||||
@@ -110,6 +207,16 @@ export default function RoutesPage() {
|
||||
[yardsQuery.data],
|
||||
);
|
||||
|
||||
const formTotalKm = useMemo(
|
||||
() =>
|
||||
form.milestones.reduce(
|
||||
(sum, row, index) =>
|
||||
index === 0 ? sum : sum + Number(row.distanceKm || 0),
|
||||
0,
|
||||
),
|
||||
[form.milestones],
|
||||
);
|
||||
|
||||
const resetForm = () => {
|
||||
setFormOpen(false);
|
||||
setEditing(null);
|
||||
@@ -125,41 +232,52 @@ export default function RoutesPage() {
|
||||
const openEdit = (route: RouteRecord) => {
|
||||
setEditing(route);
|
||||
setForm({
|
||||
name: route.name,
|
||||
milestones: (route.milestones ?? [])
|
||||
.sort((left, right) => left.sequenceNo - right.sequenceNo)
|
||||
.map((milestone) => milestone.yardId),
|
||||
status: route.status,
|
||||
milestones: [...(route.milestones ?? [])]
|
||||
.sort((a, b) => a.sequenceNo - b.sequenceNo)
|
||||
.map((m, index) => ({
|
||||
yardId: m.yardId,
|
||||
distanceKm: String(index === 0 ? 0 : (m.distanceKm ?? "")),
|
||||
})),
|
||||
});
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const setMilestone = (index: number, yardId: string) => {
|
||||
const setMilestone = (index: number, patch: Partial<MilestoneFormRow>) => {
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
milestones: current.milestones.map((value, currentIndex) =>
|
||||
currentIndex === index ? yardId : value,
|
||||
milestones: current.milestones.map((row, i) =>
|
||||
i === index ? { ...row, ...patch } : row,
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const addMilestone = () => {
|
||||
setForm((current) => ({ ...current, milestones: [...current.milestones, ""] }));
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
milestones: [...current.milestones, { yardId: "", distanceKm: "" }],
|
||||
}));
|
||||
};
|
||||
|
||||
const removeMilestone = (index: number) => {
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
milestones: current.milestones.filter((_, currentIndex) => currentIndex !== index),
|
||||
milestones: current.milestones.filter((_, i) => i !== index),
|
||||
}));
|
||||
};
|
||||
|
||||
const buildPayload = () => ({
|
||||
status: form.status,
|
||||
milestones: form.milestones.map((row, index) => ({
|
||||
yardId: row.yardId,
|
||||
distanceKm:
|
||||
index === 0 ? 0 : row.distanceKm ? Number(row.distanceKm) : undefined,
|
||||
})),
|
||||
});
|
||||
|
||||
const handleSubmit = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!form.name.trim()) {
|
||||
toast({ title: "Save failed", description: "Route name is required", variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
if (form.milestones.length < 2 || form.milestones.some((yardId) => !yardId)) {
|
||||
if (form.milestones.length < 2 || form.milestones.some((row) => !row.yardId)) {
|
||||
toast({
|
||||
title: "Save failed",
|
||||
description: "Select at least an origin and destination yard",
|
||||
@@ -167,13 +285,20 @@ export default function RoutesPage() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
for (let i = 1; i < form.milestones.length; i++) {
|
||||
const km = Number(form.milestones[i].distanceKm);
|
||||
if (!form.milestones[i].distanceKm || Number.isNaN(km) || km < 0) {
|
||||
toast({
|
||||
title: "Save failed",
|
||||
description: `Enter segment KM for stop ${i + 1}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
name: form.name.trim(),
|
||||
milestones: form.milestones.map((yardId) => ({ yardId })),
|
||||
isActive: editing?.isActive ?? true,
|
||||
};
|
||||
const payload = buildPayload();
|
||||
if (editing) {
|
||||
await updateMutation.mutateAsync({ id: editing.id, data: payload });
|
||||
toast({ title: "Route updated" });
|
||||
@@ -190,9 +315,19 @@ export default function RoutesPage() {
|
||||
const handleDeactivate = async (route: RouteRecord) => {
|
||||
try {
|
||||
await deactivateMutation.mutateAsync(route.id);
|
||||
toast({ title: "Route deactivated" });
|
||||
toast({ title: "Route marked stop working" });
|
||||
} catch {
|
||||
toast({ title: "Deactivate failed", description: "Could not deactivate route", variant: "destructive" });
|
||||
toast({ title: "Update failed", description: "Could not update route status", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = async (route: RouteRecord, status: RouteStatus) => {
|
||||
try {
|
||||
await updateMutation.mutateAsync({ id: route.id, data: { status } });
|
||||
setViewing((current) => (current?.id === route.id ? { ...current, status } : current));
|
||||
toast({ title: "Status updated" });
|
||||
} catch (error) {
|
||||
toast({ title: "Update failed", description: normalizeRouteError(error), variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -200,10 +335,14 @@ export default function RoutesPage() {
|
||||
|
||||
const availableOptionsForIndex = (index: number) => {
|
||||
const selectedByOthers = new Set(
|
||||
form.milestones.filter((value, currentIndex) => currentIndex !== index && value),
|
||||
form.milestones
|
||||
.filter((row, i) => i !== index && row.yardId)
|
||||
.map((row) => row.yardId),
|
||||
);
|
||||
return yardOptions.filter(
|
||||
(option) => option.value === form.milestones[index] || !selectedByOthers.has(option.value),
|
||||
(option) =>
|
||||
option.value === form.milestones[index]?.yardId ||
|
||||
!selectedByOthers.has(option.value),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -217,7 +356,16 @@ export default function RoutesPage() {
|
||||
const headerClassName = ruleEngineTable.headerCell;
|
||||
const cellClassName = ruleEngineTable.bodyCell;
|
||||
return [
|
||||
{ id: "name", header: "Name", meta: { headerClassName, cellClassName }, cell: ({ row }) => row.original.name },
|
||||
{
|
||||
id: "corridor",
|
||||
header: "Corridor",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => (
|
||||
<Text fw={600} size="sm">
|
||||
{formatRouteLabel(row.original)}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "origin",
|
||||
header: "Origin",
|
||||
@@ -231,18 +379,24 @@ export default function RoutesPage() {
|
||||
cell: ({ row }) => yardLabel(row.original.destinationYard),
|
||||
},
|
||||
{
|
||||
id: "milestones",
|
||||
header: "Milestones",
|
||||
id: "distance",
|
||||
header: "Total KM",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => Math.max((row.original.milestones?.length ?? 0) - 2, 0),
|
||||
cell: ({ row }) => `${totalRouteDistanceKm(row.original)} km`,
|
||||
},
|
||||
{
|
||||
id: "milestones",
|
||||
header: "Stops",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => row.original.milestones?.length ?? 0,
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
header: "Status",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => (
|
||||
<Badge color={row.original.isActive ? "edr-green" : "gray"} variant="light" size="sm">
|
||||
{row.original.isActive ? "Active" : "Inactive"}
|
||||
<Badge color={statusColor(row.original.status)} variant="light" size="sm">
|
||||
{statusLabel(row.original.status)}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
@@ -262,11 +416,11 @@ export default function RoutesPage() {
|
||||
<Edit size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Deactivate">
|
||||
<Tooltip label="Mark stop working">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
disabled={!row.original.isActive || deactivateMutation.isPending}
|
||||
disabled={row.original.status === "STOP_WORKING" || deactivateMutation.isPending}
|
||||
onClick={() => handleDeactivate(row.original)}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
@@ -282,7 +436,7 @@ export default function RoutesPage() {
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="Routes"
|
||||
subtitle="Define rail corridors and their ordered yard stops used by train scheduling."
|
||||
subtitle="Define rail corridors, segment distances, and operational status for train scheduling."
|
||||
action={
|
||||
<Button leftSection={<Plus size={18} />} onClick={openCreate}>
|
||||
Add route
|
||||
@@ -294,10 +448,10 @@ export default function RoutesPage() {
|
||||
loading={routesQuery.isLoading}
|
||||
items={[
|
||||
{ label: "Total routes", value: allRoutes.length, icon: RouteIcon },
|
||||
{ label: "Active", value: activeCount, icon: CircleCheck, color: "edr-green" },
|
||||
{ label: "Available", value: availableCount, icon: CircleCheck, color: "edr-green" },
|
||||
{
|
||||
label: "Inactive",
|
||||
value: allRoutes.length - activeCount,
|
||||
label: "Unavailable",
|
||||
value: allRoutes.length - availableCount,
|
||||
icon: Ban,
|
||||
color: "gray",
|
||||
},
|
||||
@@ -310,7 +464,7 @@ export default function RoutesPage() {
|
||||
<FleetToolbar
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
searchPlaceholder="Search routes…"
|
||||
searchPlaceholder="Search corridors…"
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
/>
|
||||
@@ -359,16 +513,13 @@ export default function RoutesPage() {
|
||||
<Card key={route.id} radius="lg" padding="lg" withBorder>
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between">
|
||||
<Text fw={600}>{route.name}</Text>
|
||||
<Badge color={route.isActive ? "edr-green" : "gray"} variant="light" size="sm">
|
||||
{route.isActive ? "Active" : "Inactive"}
|
||||
<Text fw={600}>{formatRouteLabel(route)}</Text>
|
||||
<Badge color={statusColor(route.status)} variant="light" size="sm">
|
||||
{statusLabel(route.status)}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
{yardLabel(route.originYard)} → {yardLabel(route.destinationYard)}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{Math.max((route.milestones?.length ?? 0) - 2, 0)} intermediate milestones
|
||||
{totalRouteDistanceKm(route)} km · {route.milestones?.length ?? 0} stops
|
||||
</Text>
|
||||
<Group gap={6} justify="flex-end">
|
||||
<Button variant="light" size="compact-sm" onClick={() => setViewing(route)}>
|
||||
@@ -405,26 +556,25 @@ export default function RoutesPage() {
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Name"
|
||||
value={form.name}
|
||||
onChange={(e) => {
|
||||
// Capture the value before the state updater runs — React may
|
||||
// recycle the synthetic event, nulling currentTarget by the time
|
||||
// the updater executes ("Cannot read properties of null").
|
||||
const name = e.currentTarget.value;
|
||||
setForm((current) => ({ ...current, name }));
|
||||
}}
|
||||
/>
|
||||
{editing && (
|
||||
<Select
|
||||
label="Status"
|
||||
data={ROUTE_STATUS_OPTIONS}
|
||||
value={form.status}
|
||||
onChange={(value) =>
|
||||
value && setForm((current) => ({ ...current, status: value as RouteStatus }))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" fw={500}>
|
||||
Stops
|
||||
Stops & segment distances
|
||||
</Text>
|
||||
<Button type="button" variant="light" size="compact-sm" onClick={addMilestone}>
|
||||
Add milestone
|
||||
</Button>
|
||||
</Group>
|
||||
{form.milestones.map((yardId, index) => {
|
||||
{form.milestones.map((row, index) => {
|
||||
const role =
|
||||
index === 0
|
||||
? "Origin"
|
||||
@@ -432,18 +582,32 @@ export default function RoutesPage() {
|
||||
? "Destination"
|
||||
: "Milestone";
|
||||
return (
|
||||
<Group key={`${role}-${index}`} align="flex-end" wrap="nowrap">
|
||||
<Text w={100} size="sm" fw={500}>
|
||||
<Group key={`${role}-${index}`} align="flex-end" wrap="nowrap" gap="sm">
|
||||
<Text w={90} size="sm" fw={500}>
|
||||
{role}
|
||||
</Text>
|
||||
<Select
|
||||
style={{ flex: 1 }}
|
||||
data={availableOptionsForIndex(index)}
|
||||
value={yardId || null}
|
||||
onChange={(value) => value && setMilestone(index, value)}
|
||||
value={row.yardId || null}
|
||||
onChange={(value) => value && setMilestone(index, { yardId: value })}
|
||||
placeholder="Select yard"
|
||||
searchable
|
||||
/>
|
||||
{index > 0 ? (
|
||||
<NumberInput
|
||||
w={120}
|
||||
label="KM"
|
||||
min={0}
|
||||
decimalScale={2}
|
||||
value={row.distanceKm ? Number(row.distanceKm) : ""}
|
||||
onChange={(value) =>
|
||||
setMilestone(index, { distanceKm: String(value ?? "") })
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Box w={120} />
|
||||
)}
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
@@ -455,6 +619,9 @@ export default function RoutesPage() {
|
||||
</Group>
|
||||
);
|
||||
})}
|
||||
<Text size="sm" c="dimmed">
|
||||
Total route distance: <strong>{formTotalKm} km</strong>
|
||||
</Text>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" type="button" onClick={resetForm}>
|
||||
Cancel
|
||||
@@ -470,44 +637,37 @@ export default function RoutesPage() {
|
||||
<Modal
|
||||
opened={Boolean(viewing)}
|
||||
onClose={() => setViewing(null)}
|
||||
title={<Text fw={600}>Route details</Text>}
|
||||
title={<Text fw={600}>{viewing ? formatRouteLabel(viewing) : "Route details"}</Text>}
|
||||
radius="lg"
|
||||
centered
|
||||
size="md"
|
||||
>
|
||||
{viewing ? (
|
||||
<Stack gap="sm">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="flex-end">
|
||||
<div>
|
||||
<Text size="sm" fw={500}>
|
||||
Status
|
||||
</Text>
|
||||
<Badge mt={4} color={statusColor(viewing.status)} variant="light">
|
||||
{statusLabel(viewing.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
<Select
|
||||
w={200}
|
||||
label="Update status"
|
||||
data={ROUTE_STATUS_OPTIONS}
|
||||
value={viewing.status}
|
||||
onChange={(value) =>
|
||||
value && handleStatusChange(viewing, value as RouteStatus)
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
<div>
|
||||
<Text size="sm" fw={500}>
|
||||
Name
|
||||
<Text size="sm" fw={500} mb={8}>
|
||||
Road timeline
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{viewing.name}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" fw={500}>
|
||||
Status
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{viewing.isActive ? "Active" : "Inactive"}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" fw={500}>
|
||||
Stops
|
||||
</Text>
|
||||
<Stack gap={6} mt={6}>
|
||||
{routeStops(viewing).map((stop, index, stops) => (
|
||||
<Text key={`${stop}-${index}`} size="sm" c="dimmed">
|
||||
{index === 0
|
||||
? "Origin"
|
||||
: index === stops.length - 1
|
||||
? "Destination"
|
||||
: `Milestone ${index}`}
|
||||
: {stop}
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
<RouteTimeline route={viewing} />
|
||||
</div>
|
||||
</Stack>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user