mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 04:15:43 +00:00
Merge pull request #523 from Tria-plc/freight_feature/usermanagement
resolve conflict
This commit is contained in:
@@ -12,23 +12,28 @@ export class AddEmailToOtpVerifications1900000000000
|
|||||||
name = "AddEmailToOtpVerifications1900000000000";
|
name = "AddEmailToOtpVerifications1900000000000";
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
// The table lives in the `freight` schema (the OtpVerification entity pins
|
||||||
|
// schema: "freight"). An earlier version of this migration targeted
|
||||||
|
// `public.otp_verifications`, which does not exist there — leaving the real
|
||||||
|
// freight table without an `email` column and OTP send failing with
|
||||||
|
// `column OtpVerification.email does not exist`. Target `freight` explicitly.
|
||||||
await queryRunner.query(`
|
await queryRunner.query(`
|
||||||
ALTER TABLE public.otp_verifications
|
ALTER TABLE freight.otp_verifications
|
||||||
ALTER COLUMN phone DROP NOT NULL
|
ALTER COLUMN phone DROP NOT NULL
|
||||||
`);
|
`);
|
||||||
await queryRunner.query(`
|
await queryRunner.query(`
|
||||||
ALTER TABLE public.otp_verifications
|
ALTER TABLE freight.otp_verifications
|
||||||
ADD COLUMN IF NOT EXISTS email varchar UNIQUE
|
ADD COLUMN IF NOT EXISTS email varchar UNIQUE
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`
|
await queryRunner.query(`
|
||||||
ALTER TABLE public.otp_verifications
|
ALTER TABLE freight.otp_verifications
|
||||||
DROP COLUMN IF EXISTS email
|
DROP COLUMN IF EXISTS email
|
||||||
`);
|
`);
|
||||||
await queryRunner.query(`
|
await queryRunner.query(`
|
||||||
ALTER TABLE public.otp_verifications
|
ALTER TABLE freight.otp_verifications
|
||||||
ALTER COLUMN phone SET NOT NULL
|
ALTER COLUMN phone SET NOT NULL
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repair: AddEmailToOtpVerifications1900000000000 originally altered
|
||||||
|
* `public.otp_verifications`, but the OtpVerification entity pins
|
||||||
|
* schema: "freight". On any DB where that migration already ran (and is recorded
|
||||||
|
* as executed, so it won't run again), the real `freight.otp_verifications` table
|
||||||
|
* never got the `email` column and `phone` was never made nullable — so OTP send
|
||||||
|
* dies with `column OtpVerification.email does not exist`.
|
||||||
|
*
|
||||||
|
* This migration re-applies the change against the correct schema. Idempotent
|
||||||
|
* (IF NOT EXISTS / no-op DROP NOT NULL), and guarded so it's a no-op when the
|
||||||
|
* freight table is absent.
|
||||||
|
*/
|
||||||
|
export class RepairOtpEmailSchema2020000000000 implements MigrationInterface {
|
||||||
|
name = "RepairOtpEmailSchema2020000000000";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const exists = await queryRunner.hasTable("freight.otp_verifications");
|
||||||
|
if (!exists) return;
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE freight.otp_verifications
|
||||||
|
ALTER COLUMN phone DROP NOT NULL
|
||||||
|
`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE freight.otp_verifications
|
||||||
|
ADD COLUMN IF NOT EXISTS email varchar UNIQUE
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const exists = await queryRunner.hasTable("freight.otp_verifications");
|
||||||
|
if (!exists) return;
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE freight.otp_verifications
|
||||||
|
DROP COLUMN IF EXISTS email
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,8 +72,14 @@ export class OtpService {
|
|||||||
message: "OTP sent successfully",
|
message: "OTP sent successfully",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
// Log the real cause (DB/SMS/email failure) with its stack so a deployed
|
||||||
|
// "Failed to send OTP" 400 is diagnosable from the API logs, not opaque.
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to send OTP to ${target.email ?? target.phone}: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`,
|
||||||
|
error instanceof Error ? error.stack : undefined,
|
||||||
|
);
|
||||||
throw new BadRequestException("Failed to send OTP");
|
throw new BadRequestException("Failed to send OTP");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
Paper,
|
Paper,
|
||||||
Progress,
|
RingProgress,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
@@ -26,7 +26,11 @@ import {
|
|||||||
|
|
||||||
import { PageContainer } from "@/components/page";
|
import { PageContainer } from "@/components/page";
|
||||||
import { RouteCorridorTrack } from "@/components/trainScheduling/RouteCorridorTrack";
|
import { RouteCorridorTrack } from "@/components/trainScheduling/RouteCorridorTrack";
|
||||||
import { RouteCorridor, StatusPill } from "@/components/trainScheduling/scheduleVisuals";
|
import {
|
||||||
|
RouteCorridor,
|
||||||
|
StatusPill,
|
||||||
|
scheduleBrand,
|
||||||
|
} from "@/components/trainScheduling/scheduleVisuals";
|
||||||
import { freightBrand } from "@/theme/freight-brand";
|
import { freightBrand } from "@/theme/freight-brand";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
@@ -52,8 +56,11 @@ function formatDateTime(iso?: string | null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Compact icon + label + value cell used in the header meta strip. */
|
/**
|
||||||
function MetaStat({
|
* A single fact in the hero's glass meta strip — icon chip + uppercase label +
|
||||||
|
* value, laid on the translucent panel over the gradient.
|
||||||
|
*/
|
||||||
|
function HeroStat({
|
||||||
icon,
|
icon,
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
@@ -63,15 +70,33 @@ function MetaStat({
|
|||||||
value: string;
|
value: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
|
<Group gap={10} wrap="nowrap" style={{ minWidth: 0 }}>
|
||||||
<ThemeIcon size={32} radius="md" variant="light" color="edr-green">
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
borderRadius: 10,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: "rgba(255,255,255,0.16)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.24)",
|
||||||
|
color: "white",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</ThemeIcon>
|
</Box>
|
||||||
<Stack gap={0} style={{ minWidth: 0 }}>
|
<Stack gap={1} style={{ minWidth: 0 }}>
|
||||||
<Text size="10px" fw={700} tt="uppercase" c="dimmed" style={{ letterSpacing: 0.5 }}>
|
<Text
|
||||||
|
size="10px"
|
||||||
|
fw={700}
|
||||||
|
tt="uppercase"
|
||||||
|
style={{ letterSpacing: 0.6, color: "rgba(255,255,255,0.72)" }}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" fw={700} c="dark.5" truncate>
|
<Text size="sm" fw={700} c="white" truncate>
|
||||||
{value}
|
{value}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -79,6 +104,38 @@ function MetaStat({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Section header — icon chip + title + one-line hint. Shared by the cards. */
|
||||||
|
function SectionHead({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
hint,
|
||||||
|
}: {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
hint: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Group gap="sm" align="center" wrap="nowrap">
|
||||||
|
<ThemeIcon size={36} radius="md" variant="light" color="edr-green">
|
||||||
|
{icon}
|
||||||
|
</ThemeIcon>
|
||||||
|
<Stack gap={0} style={{ minWidth: 0 }}>
|
||||||
|
<Text fw={800} size="sm">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{hint}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CARD_STYLE = {
|
||||||
|
borderColor: scheduleBrand.mutedBorder,
|
||||||
|
boxShadow: scheduleBrand.shadowSm,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export default function TrainScheduleTrackPage() {
|
export default function TrainScheduleTrackPage() {
|
||||||
const { scheduleId } = useParams<{ scheduleId: string }>();
|
const { scheduleId } = useParams<{ scheduleId: string }>();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -116,9 +173,13 @@ 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 progressPct = totalStations > 1 ? (track.currentSequenceNo / (totalStations - 1)) * 100 : 0;
|
const progressPct =
|
||||||
|
totalStations > 1 ? (track.currentSequenceNo / (totalStations - 1)) * 100 : 0;
|
||||||
const clampedPct = Math.min(100, Math.max(0, progressPct));
|
const clampedPct = Math.min(100, Math.max(0, progressPct));
|
||||||
const currentStation = track.stations[Math.max(0, track.currentSequenceNo)]?.label ?? "—";
|
const currentStation =
|
||||||
|
track.stations[Math.max(0, track.currentSequenceNo)]?.label ?? "—";
|
||||||
|
const inTransit = track.status === "DISPATCHED";
|
||||||
|
const arrived = track.status === "ARRIVED";
|
||||||
|
|
||||||
const handleLog = (sequenceNo: number) => {
|
const handleLog = (sequenceNo: number) => {
|
||||||
const isFinal = sequenceNo === track.stations[totalStations - 1]?.sequenceNo;
|
const isFinal = sequenceNo === track.stations[totalStations - 1]?.sequenceNo;
|
||||||
@@ -156,160 +217,259 @@ export default function TrainScheduleTrackPage() {
|
|||||||
Back to schedule
|
Back to schedule
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Header */}
|
{/* ── Hero: gradient wash, route + a bold progress ring woven together ── */}
|
||||||
<Paper radius="lg" withBorder p="lg">
|
<Paper
|
||||||
<Stack gap="lg">
|
radius="lg"
|
||||||
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
p={0}
|
||||||
<Group gap="md" align="flex-start" wrap="nowrap" style={{ minWidth: 0 }}>
|
style={{ overflow: "hidden", boxShadow: scheduleBrand.shadow }}
|
||||||
<Box
|
>
|
||||||
style={{
|
<Box
|
||||||
width: 48,
|
style={{
|
||||||
height: 48,
|
background: scheduleBrand.heroGradient,
|
||||||
borderRadius: 12,
|
padding: "26px 28px",
|
||||||
display: "flex",
|
position: "relative",
|
||||||
alignItems: "center",
|
}}
|
||||||
justifyContent: "center",
|
>
|
||||||
background: freightBrand.gradient,
|
{/* soft decorative glow, purely artistic */}
|
||||||
color: "white",
|
<Box
|
||||||
flexShrink: 0,
|
style={{
|
||||||
}}
|
position: "absolute",
|
||||||
>
|
top: -80,
|
||||||
<Navigation size={24} />
|
right: -60,
|
||||||
</Box>
|
width: 260,
|
||||||
<Stack gap={6} style={{ minWidth: 0 }}>
|
height: 260,
|
||||||
<Group gap="sm" align="center" wrap="wrap">
|
borderRadius: "50%",
|
||||||
<Title order={3} fw={800}>
|
background: "rgba(255,255,255,0.10)",
|
||||||
Train tracking
|
pointerEvents: "none",
|
||||||
</Title>
|
}}
|
||||||
{track.trainNumber ? (
|
/>
|
||||||
<Badge variant="light" color="edr-green" radius="sm">
|
|
||||||
{track.trainNumber}
|
<Group
|
||||||
</Badge>
|
justify="space-between"
|
||||||
) : null}
|
align="flex-start"
|
||||||
{track.direction ? (
|
wrap="wrap"
|
||||||
<Badge variant="light" color="gray" radius="sm">
|
gap="xl"
|
||||||
{track.direction}
|
style={{ position: "relative" }}
|
||||||
</Badge>
|
>
|
||||||
) : null}
|
{/* left — identity + route */}
|
||||||
</Group>
|
<Stack gap={14} style={{ minWidth: 260, flex: 1 }}>
|
||||||
<Box maw={360}>
|
<Group gap="md" align="center" wrap="nowrap">
|
||||||
<RouteCorridor origin={track.origin} destination={track.destination} variant="compact" />
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 52,
|
||||||
|
height: 52,
|
||||||
|
borderRadius: 14,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: "rgba(255,255,255,0.16)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.26)",
|
||||||
|
color: "white",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Navigation size={26} />
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
<Stack gap={6} style={{ minWidth: 0 }}>
|
||||||
</Group>
|
<Group gap="sm" align="center" wrap="wrap">
|
||||||
<StatusPill status={track.status} />
|
<Title order={3} fw={800} c="white">
|
||||||
</Group>
|
Train tracking
|
||||||
|
</Title>
|
||||||
|
{track.trainNumber ? (
|
||||||
|
<Badge
|
||||||
|
variant="white"
|
||||||
|
color="dark"
|
||||||
|
radius="sm"
|
||||||
|
styles={{ root: { color: freightBrand.primaryDark } }}
|
||||||
|
>
|
||||||
|
{track.trainNumber}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
{track.direction ? (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
radius="sm"
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
color: "white",
|
||||||
|
borderColor: "rgba(255,255,255,0.5)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{track.direction}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
<Box maw={380}>
|
||||||
|
<RouteCorridor
|
||||||
|
origin={track.origin}
|
||||||
|
destination={track.destination}
|
||||||
|
variant="compact"
|
||||||
|
onDark
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
|
||||||
{/* Journey progress */}
|
<Group gap="sm">
|
||||||
<Box>
|
<StatusPill status={track.status} size="md" />
|
||||||
<Group justify="space-between" mb={6}>
|
<Box
|
||||||
<Text size="xs" fw={700} c="gray.7" tt="uppercase" style={{ letterSpacing: 0.4 }}>
|
px={12}
|
||||||
Journey progress
|
py={5}
|
||||||
</Text>
|
style={{
|
||||||
<Text size="xs" fw={700} c="edr-green.8">
|
borderRadius: 999,
|
||||||
{reached} / {totalStations} stations · {Math.round(clampedPct)}%
|
background: "rgba(255,255,255,0.16)",
|
||||||
</Text>
|
border: "1px solid rgba(255,255,255,0.24)",
|
||||||
</Group>
|
}}
|
||||||
<Progress
|
>
|
||||||
value={clampedPct}
|
<Text size="xs" fw={700} c="white" style={{ letterSpacing: 0.2 }}>
|
||||||
size="lg"
|
{arrived
|
||||||
radius="xl"
|
? "Journey complete"
|
||||||
color="edr-green"
|
: inTransit
|
||||||
striped={track.status === "DISPATCHED"}
|
? `En route · ${currentStation}`
|
||||||
animated={track.status === "DISPATCHED"}
|
: "Awaiting dispatch"}
|
||||||
/>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
{/* Meta strip */}
|
{/* right — progress ring, the artistic focal point */}
|
||||||
<Group justify="space-between" wrap="wrap" gap="lg">
|
<RingProgress
|
||||||
<MetaStat icon={<MapPin size={16} />} label="Current" value={currentStation} />
|
size={132}
|
||||||
<MetaStat
|
thickness={11}
|
||||||
icon={<CalendarClock size={16} />}
|
roundCaps
|
||||||
label="Departed"
|
sections={[{ value: clampedPct, color: "white" }]}
|
||||||
value={formatDateTime(track.actualDepartureAt)}
|
rootColor="rgba(255,255,255,0.22)"
|
||||||
/>
|
label={
|
||||||
<MetaStat
|
<Stack gap={0} align="center">
|
||||||
icon={<Flag size={16} />}
|
<Text fw={800} fz={26} lh={1} c="white">
|
||||||
label="Arrived"
|
{Math.round(clampedPct)}%
|
||||||
value={formatDateTime(track.actualArrivalAt)}
|
</Text>
|
||||||
/>
|
<Text
|
||||||
<MetaStat
|
size="10px"
|
||||||
icon={<Train size={16} />}
|
fw={700}
|
||||||
label="Stations"
|
tt="uppercase"
|
||||||
value={`${reached} of ${totalStations}`}
|
style={{ letterSpacing: 0.6, color: "rgba(255,255,255,0.8)" }}
|
||||||
|
>
|
||||||
|
{reached}/{totalStations} stops
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Box>
|
||||||
|
|
||||||
|
{/* glass meta strip below the wash */}
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
wrap="wrap"
|
||||||
|
gap="lg"
|
||||||
|
px={28}
|
||||||
|
py="md"
|
||||||
|
style={{
|
||||||
|
background: freightBrand.primaryDark,
|
||||||
|
borderTop: "1px solid rgba(255,255,255,0.12)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HeroStat icon={<MapPin size={16} />} label="Current" value={currentStation} />
|
||||||
|
<HeroStat
|
||||||
|
icon={<CalendarClock size={16} />}
|
||||||
|
label="Departed"
|
||||||
|
value={formatDateTime(track.actualDepartureAt)}
|
||||||
|
/>
|
||||||
|
<HeroStat
|
||||||
|
icon={<Flag size={16} />}
|
||||||
|
label="Arrived"
|
||||||
|
value={formatDateTime(track.actualArrivalAt)}
|
||||||
|
/>
|
||||||
|
<HeroStat
|
||||||
|
icon={<Train size={16} />}
|
||||||
|
label="Stations"
|
||||||
|
value={`${reached} of ${totalStations}`}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Corridor */}
|
{/* ── Route corridor ── */}
|
||||||
<Paper radius="lg" p="lg" withBorder>
|
<Paper radius="lg" p="lg" withBorder style={CARD_STYLE}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group gap="sm" align="center" wrap="nowrap">
|
<SectionHead
|
||||||
<ThemeIcon size={34} radius="md" variant="light" color="edr-green">
|
icon={<Navigation size={17} />}
|
||||||
<Navigation size={17} />
|
title="Route corridor"
|
||||||
</ThemeIcon>
|
hint={
|
||||||
<Stack gap={0}>
|
canLog
|
||||||
<Text fw={800} size="sm">
|
? "Log the train passing each station; the final station marks arrival."
|
||||||
Route corridor
|
: arrived
|
||||||
</Text>
|
? "This train has arrived at its destination."
|
||||||
<Text size="xs" c="dimmed">
|
: "Tracking becomes available once the train is dispatched."
|
||||||
{canLog
|
}
|
||||||
? "Log the train passing each station; the final station marks arrival."
|
/>
|
||||||
: track.status === "ARRIVED"
|
|
||||||
? "This train has arrived at its destination."
|
|
||||||
: "Tracking becomes available once the train is dispatched."}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<RouteCorridorTrack
|
<RouteCorridorTrack
|
||||||
stations={track.stations}
|
stations={track.stations}
|
||||||
currentSequenceNo={track.currentSequenceNo}
|
currentSequenceNo={track.currentSequenceNo}
|
||||||
checkpoints={track.checkpoints}
|
checkpoints={track.checkpoints}
|
||||||
canLog={canLog}
|
canLog={canLog}
|
||||||
loggingSeq={
|
loggingSeq={
|
||||||
recordCheckpoint.isPending ? recordCheckpoint.variables?.payload.sequenceNo : null
|
recordCheckpoint.isPending
|
||||||
|
? recordCheckpoint.variables?.payload.sequenceNo
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
onLogCheckpoint={handleLog}
|
onLogCheckpoint={handleLog}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Checkpoint log */}
|
{/* ── Checkpoint log ── */}
|
||||||
<Paper radius="lg" p="lg" withBorder>
|
<Paper radius="lg" p="lg" withBorder style={CARD_STYLE}>
|
||||||
<Group gap="sm" align="center" wrap="nowrap" mb="md">
|
<Group justify="space-between" wrap="nowrap" mb="md">
|
||||||
<ThemeIcon size={34} radius="md" variant="light" color="edr-green">
|
<SectionHead
|
||||||
<CheckCircle2 size={17} />
|
icon={<CheckCircle2 size={17} />}
|
||||||
</ThemeIcon>
|
title="Checkpoint log"
|
||||||
<Stack gap={0}>
|
hint={`${track.checkpoints.length} event${
|
||||||
<Text fw={800} size="sm">
|
track.checkpoints.length === 1 ? "" : "s"
|
||||||
Checkpoint log
|
} recorded`}
|
||||||
</Text>
|
/>
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
{track.checkpoints.length} event{track.checkpoints.length === 1 ? "" : "s"} recorded
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{track.checkpoints.length === 0 ? (
|
{track.checkpoints.length === 0 ? (
|
||||||
<Stack align="center" gap="xs" py="xl">
|
<Stack
|
||||||
<ThemeIcon size={44} radius="xl" variant="light" color="gray">
|
align="center"
|
||||||
<MapPin size={20} />
|
gap="xs"
|
||||||
|
py={40}
|
||||||
|
style={{
|
||||||
|
borderRadius: 14,
|
||||||
|
border: `1px dashed ${scheduleBrand.mutedBorder}`,
|
||||||
|
background: scheduleBrand.softSurface,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThemeIcon size={48} radius="xl" variant="light" color="edr-green">
|
||||||
|
<MapPin size={22} />
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Text size="sm" fw={600} c="gray.7">
|
<Text size="sm" fw={700} c="gray.7">
|
||||||
No checkpoints yet
|
No checkpoints yet
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" c="dimmed" ta="center" maw={300}>
|
<Text size="xs" c="dimmed" ta="center" maw={320}>
|
||||||
Each station the train passes will be logged here with its timestamp.
|
Each station the train passes will be logged here with its
|
||||||
|
timestamp.
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Timeline active={track.checkpoints.length} bulletSize={22} lineWidth={2} color="edr-green">
|
<Timeline
|
||||||
|
active={track.checkpoints.length}
|
||||||
|
bulletSize={24}
|
||||||
|
lineWidth={2}
|
||||||
|
color="edr-green"
|
||||||
|
>
|
||||||
{track.checkpoints.map((cp) => (
|
{track.checkpoints.map((cp) => (
|
||||||
<Timeline.Item
|
<Timeline.Item
|
||||||
key={cp.id}
|
key={cp.id}
|
||||||
bullet={cp.kind === "ARRIVED" ? <CheckCircle2 size={13} /> : <MapPin size={12} />}
|
bullet={
|
||||||
|
cp.kind === "ARRIVED" ? (
|
||||||
|
<CheckCircle2 size={13} />
|
||||||
|
) : (
|
||||||
|
<MapPin size={12} />
|
||||||
|
)
|
||||||
|
}
|
||||||
title={
|
title={
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Text fw={700} size="sm">
|
<Text fw={700} size="sm">
|
||||||
@@ -319,7 +479,13 @@ export default function TrainScheduleTrackPage() {
|
|||||||
size="xs"
|
size="xs"
|
||||||
radius="sm"
|
radius="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
color={cp.kind === "ARRIVED" ? "teal" : cp.kind === "DEPARTED" ? "blue" : "edr-green"}
|
color={
|
||||||
|
cp.kind === "ARRIVED"
|
||||||
|
? "teal"
|
||||||
|
: cp.kind === "DEPARTED"
|
||||||
|
? "blue"
|
||||||
|
: "edr-green"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{cp.kind}
|
{cp.kind}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
Reference in New Issue
Block a user