mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 01:18:18 +00:00
style: WIP ui updates
This commit is contained in:
@@ -55,7 +55,7 @@ export const detailStyles = {
|
|||||||
export function approvalStatusColor(status: string): string {
|
export function approvalStatusColor(status: string): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "APPROVED":
|
case "APPROVED":
|
||||||
return "green";
|
return "edr-green";
|
||||||
case "PENDING":
|
case "PENDING":
|
||||||
return "yellow";
|
return "yellow";
|
||||||
case "REJECTED":
|
case "REJECTED":
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ReactNode } from "react";
|
|
||||||
import { LayoutGrid, Plus, Search, Table2 } from "lucide-react";
|
|
||||||
import { Box, Button, Group, SegmentedControl, TextInput } from "@mantine/core";
|
import { Box, Button, Group, SegmentedControl, TextInput } from "@mantine/core";
|
||||||
|
import { LayoutGrid, Plus, Search, Table2 } from "lucide-react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import type { FleetViewMode } from "./useFleetViewMode";
|
import type { FleetViewMode } from "./useFleetViewMode";
|
||||||
|
|
||||||
@@ -67,7 +67,6 @@ const FleetToolbar = ({
|
|||||||
onChange={(value) => onViewModeChange(value as FleetViewMode)}
|
onChange={(value) => onViewModeChange(value as FleetViewMode)}
|
||||||
size="sm"
|
size="sm"
|
||||||
radius="lg"
|
radius="lg"
|
||||||
color="green"
|
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
value: "table",
|
value: "table",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ArrowRight, Package, Train } from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -8,6 +7,7 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
Text,
|
Text,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
import { ArrowRight, Package, Train } from "lucide-react";
|
||||||
|
|
||||||
import type { EligibleContainerBooking, FreightType } from "@/types/trainScheduling";
|
import type { EligibleContainerBooking, FreightType } from "@/types/trainScheduling";
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Badge } from "@mantine/core";
|
|||||||
const STATUS_COLORS: Record<string, string> = {
|
const STATUS_COLORS: Record<string, string> = {
|
||||||
DRAFT: "gray",
|
DRAFT: "gray",
|
||||||
SCHEDULED: "blue",
|
SCHEDULED: "blue",
|
||||||
DISPATCHED: "green",
|
DISPATCHED: "edr-green",
|
||||||
ARRIVED: "teal",
|
ARRIVED: "teal",
|
||||||
CANCELLED: "red",
|
CANCELLED: "red",
|
||||||
};
|
};
|
||||||
@@ -34,7 +34,7 @@ export function SchedulingStatusBadge({ status }: { status?: string | null }) {
|
|||||||
HOLDING: "yellow",
|
HOLDING: "yellow",
|
||||||
ELIGIBLE: "blue",
|
ELIGIBLE: "blue",
|
||||||
SCHEDULED: "indigo",
|
SCHEDULED: "indigo",
|
||||||
DISPATCHED: "green",
|
DISPATCHED: "edr-green",
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Badge variant="light" color={colors[status] ?? "gray"} size="sm">
|
<Badge variant="light" color={colors[status] ?? "gray"} size="sm">
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const STATUS_META: Record<
|
|||||||
{ color: string; dot: string; label?: string }
|
{ color: string; dot: string; label?: string }
|
||||||
> = {
|
> = {
|
||||||
DRAFT: { color: "gray", dot: "var(--mantine-color-gray-5)" },
|
DRAFT: { color: "gray", dot: "var(--mantine-color-gray-5)" },
|
||||||
SCHEDULED: { color: "green", dot: freightBrand.primary },
|
SCHEDULED: { color: "edr-green", dot: freightBrand.primary },
|
||||||
DISPATCHED: { color: "teal", dot: "var(--mantine-color-teal-6)" },
|
DISPATCHED: { color: "teal", dot: "var(--mantine-color-teal-6)" },
|
||||||
ARRIVED: { color: "blue", dot: "var(--mantine-color-blue-6)" },
|
ARRIVED: { color: "blue", dot: "var(--mantine-color-blue-6)" },
|
||||||
CANCELLED: { color: "red", dot: "var(--mantine-color-red-6)" },
|
CANCELLED: { color: "red", dot: "var(--mantine-color-red-6)" },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Fragment } from "react";
|
import { Anchor, Breadcrumbs as MantineBreadcrumbs, Text } from "@mantine/core";
|
||||||
|
import { ChevronRight } from "lucide-react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { ChevronRight, Home } from "lucide-react";
|
|
||||||
|
|
||||||
export interface BreadcrumbItem {
|
export interface BreadcrumbItem {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -12,45 +12,48 @@ export interface BreadcrumbsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Breadcrumbs({ items }: BreadcrumbsProps) {
|
export default function Breadcrumbs({ items }: BreadcrumbsProps) {
|
||||||
return (
|
// The dashboard root is always the first crumb; callers pass only the trail
|
||||||
<nav
|
// beyond it.
|
||||||
aria-label="Breadcrumb"
|
const crumbs: BreadcrumbItem[] = [{ label: "Dashboard", href: "/" }, ...items];
|
||||||
className="flex items-center text-sm text-slate-500"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
aria-label="Home"
|
|
||||||
className="flex items-center transition hover:text-[var(--freight-brand)]"
|
|
||||||
>
|
|
||||||
{/* <Home className="h-4 w-4" /> */}
|
|
||||||
Dashboard
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{items.map((item, i) => {
|
return (
|
||||||
const isLast = i === items.length - 1;
|
<MantineBreadcrumbs
|
||||||
|
separator={<ChevronRight size={14} aria-hidden />}
|
||||||
|
separatorMargin="xs"
|
||||||
|
styles={{
|
||||||
|
root: { flexWrap: "wrap", rowGap: 4 },
|
||||||
|
separator: { color: "var(--mantine-color-edr-muted-5)" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{crumbs.map((item, index) => {
|
||||||
|
const isLast = index === crumbs.length - 1;
|
||||||
|
|
||||||
|
if (item.href && !isLast) {
|
||||||
|
return (
|
||||||
|
<Anchor
|
||||||
|
key={`${item.label}-${index}`}
|
||||||
|
component={Link}
|
||||||
|
to={item.href}
|
||||||
|
size="sm"
|
||||||
|
c="dimmed"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Anchor>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={`${item.label}-${i}`}>
|
<Text
|
||||||
<ChevronRight className="mx-2 h-4 w-4 text-slate-300" />
|
key={`${item.label}-${index}`}
|
||||||
|
size="sm"
|
||||||
{item.href && !isLast ? (
|
fw={isLast ? 600 : 400}
|
||||||
<Link
|
c={isLast ? "edr-text" : "dimmed"}
|
||||||
to={item.href}
|
aria-current={isLast ? "page" : undefined}
|
||||||
className="transition hover:text-[var(--freight-brand)]"
|
>
|
||||||
>
|
{item.label}
|
||||||
{item.label}
|
</Text>
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
aria-current={isLast ? "page" : undefined}
|
|
||||||
className="font-medium text-slate-900"
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</MantineBreadcrumbs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
|
||||||
|
import { PageContainer } from "@/components/page";
|
||||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||||
import { ApprovalStepsCard } from "@/components/bookings/ApprovalStepsCard";
|
import { ApprovalStepsCard } from "@/components/bookings/ApprovalStepsCard";
|
||||||
import { BookingActionsToolbar } from "@/components/bookings/BookingActionsToolbar";
|
import { BookingActionsToolbar } from "@/components/bookings/BookingActionsToolbar";
|
||||||
@@ -61,7 +62,7 @@ export default function BookingRequestDetailPage() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Box style={detailStyles.page}>
|
<PageContainer>
|
||||||
<Center mih="60vh">
|
<Center mih="60vh">
|
||||||
<Stack align="center" gap="md">
|
<Stack align="center" gap="md">
|
||||||
<Loader color="gray" />
|
<Loader color="gray" />
|
||||||
@@ -70,13 +71,13 @@ export default function BookingRequestDetailPage() {
|
|||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !booking) {
|
if (isError || !booking) {
|
||||||
return (
|
return (
|
||||||
<Box style={detailStyles.page}>
|
<PageContainer>
|
||||||
<Container size="sm" py="xl">
|
<Container size="sm" py="xl">
|
||||||
<Paper radius="md" withBorder p="xl" ta="center" style={detailStyles.card}>
|
<Paper radius="md" withBorder p="xl" ta="center" style={detailStyles.card}>
|
||||||
<Center>
|
<Center>
|
||||||
@@ -111,7 +112,7 @@ export default function BookingRequestDetailPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,16 +128,15 @@ export default function BookingRequestDetailPage() {
|
|||||||
booking.status === "APPROVED_PENDING_SIGNATURE";
|
booking.status === "APPROVED_PENDING_SIGNATURE";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box style={detailStyles.page}>
|
<PageContainer>
|
||||||
<Container size="xxl" py="lg">
|
<Breadcrumbs
|
||||||
<Breadcrumbs
|
items={[
|
||||||
items={[
|
{ label: "Booking requests", href: "/dashboard/booking-requests" },
|
||||||
{ label: "Booking requests", href: "/dashboard/booking-requests" },
|
{ label: booking.reference },
|
||||||
{ label: booking.reference },
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<Stack gap="lg" mt="sm">
|
<Stack gap="lg">
|
||||||
<BookingRequestHero
|
<BookingRequestHero
|
||||||
booking={booking}
|
booking={booking}
|
||||||
customerLabel={row.customerLabel}
|
customerLabel={row.customerLabel}
|
||||||
@@ -190,7 +190,7 @@ export default function BookingRequestDetailPage() {
|
|||||||
{showContractButton && (
|
{showContractButton && (
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
color="green"
|
color="edr-green"
|
||||||
leftSection={<FileSignature size={16} />}
|
leftSection={<FileSignature size={16} />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(`/dashboard/booking-requests/${booking.id}/contract`)
|
navigate(`/dashboard/booking-requests/${booking.id}/contract`)
|
||||||
@@ -206,8 +206,7 @@ export default function BookingRequestDetailPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</PageContainer>
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useCallback, useMemo, useRef, useState } from "react";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { ArrowRight, Calendar, Package, Search, User, X } from "lucide-react";
|
import { ArrowRight, Calendar, Package, Search, User, X } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Container,
|
|
||||||
Stack,
|
Stack,
|
||||||
Group,
|
Group,
|
||||||
Text,
|
Text,
|
||||||
@@ -12,6 +11,7 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
|
||||||
|
import { PageContainer } from "@/components/page";
|
||||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||||
import { BookingPriorityBadge } from "@/components/bookings/BookingPriorityBadge";
|
import { BookingPriorityBadge } from "@/components/bookings/BookingPriorityBadge";
|
||||||
import {
|
import {
|
||||||
@@ -297,11 +297,10 @@ export default function BookingRequestsPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ background: "var(--mantine-color-gray-0)", minHeight: "100vh" }}>
|
<PageContainer>
|
||||||
<Container size="xxl" py="xl">
|
<Breadcrumbs items={[{ label: "Operations" }, { label: "Booking requests" }]} />
|
||||||
<Breadcrumbs items={[{ label: "Operations" }, { label: "Booking requests" }]} />
|
|
||||||
|
|
||||||
<Stack gap="lg" mt="md">
|
<Stack gap="lg">
|
||||||
<BookingRequestsHeader
|
<BookingRequestsHeader
|
||||||
metrics={metrics}
|
metrics={metrics}
|
||||||
tabs={tabCounts}
|
tabs={tabCounts}
|
||||||
@@ -320,15 +319,7 @@ export default function BookingRequestsPage() {
|
|||||||
counts={tabCounts}
|
counts={tabCounts}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card
|
<Card p="md" radius="lg" withBorder>
|
||||||
p="md"
|
|
||||||
radius="lg"
|
|
||||||
withBorder
|
|
||||||
style={{
|
|
||||||
background: "white",
|
|
||||||
border: "1px solid var(--mantine-color-gray-2)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group justify="space-between" gap="md" wrap="wrap">
|
<Group justify="space-between" gap="md" wrap="wrap">
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -441,7 +432,6 @@ export default function BookingRequestsPage() {
|
|||||||
initialBookingIds={allocateIds}
|
initialBookingIds={allocateIds}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Container>
|
</PageContainer>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Navigate, useLocation } from "react-router-dom";
|
|||||||
import type { ColumnDef } from "@edr/ui-common";
|
import type { ColumnDef } from "@edr/ui-common";
|
||||||
import { Box, Button, Card, Group, Modal, Select, Stack, Text } from "@mantine/core";
|
import { Box, Button, Card, Group, Modal, Select, Stack, Text } from "@mantine/core";
|
||||||
|
|
||||||
|
import { PageContainer } from "@/components/page";
|
||||||
import FleetCardGrid from "@/components/fleet/FleetCardGrid";
|
import FleetCardGrid from "@/components/fleet/FleetCardGrid";
|
||||||
import FleetFormDialog from "@/components/fleet/FleetFormDialog";
|
import FleetFormDialog from "@/components/fleet/FleetFormDialog";
|
||||||
import FleetRecordActions from "@/components/fleet/FleetRecordActions";
|
import FleetRecordActions from "@/components/fleet/FleetRecordActions";
|
||||||
@@ -291,8 +292,8 @@ const FleetResourcePage = () => {
|
|||||||
const itemLabel = config.label.toLowerCase();
|
const itemLabel = config.label.toLowerCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<PageContainer>
|
||||||
<Card radius="lg" padding={0} withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
<Card radius="lg" padding={0} withBorder>
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<Box px="md" pt="md" pb="sm" w="100%">
|
<Box px="md" pt="md" pb="sm" w="100%">
|
||||||
<FleetToolbar
|
<FleetToolbar
|
||||||
@@ -435,7 +436,7 @@ const FleetResourcePage = () => {
|
|||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Stack>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
|
||||||
|
import { PageContainer } from "@/components/page";
|
||||||
import FleetToolbar from "@/components/fleet/FleetToolbar";
|
import FleetToolbar from "@/components/fleet/FleetToolbar";
|
||||||
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode";
|
import { useFleetViewMode } from "@/components/fleet/useFleetViewMode";
|
||||||
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
||||||
@@ -241,7 +242,7 @@ export default function RoutesPage() {
|
|||||||
header: "Status",
|
header: "Status",
|
||||||
meta: { headerClassName, cellClassName },
|
meta: { headerClassName, cellClassName },
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Badge color={row.original.isActive ? "green" : "gray"} variant="light" size="sm">
|
<Badge color={row.original.isActive ? "edr-green" : "gray"} variant="light" size="sm">
|
||||||
{row.original.isActive ? "Active" : "Inactive"}
|
{row.original.isActive ? "Active" : "Inactive"}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
@@ -279,8 +280,8 @@ export default function RoutesPage() {
|
|||||||
}, [deactivateMutation.isPending]);
|
}, [deactivateMutation.isPending]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<PageContainer>
|
||||||
<Card radius="lg" padding={0} withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
<Card radius="lg" padding={0} withBorder>
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<Box px="md" pt="md" pb="sm" w="100%">
|
<Box px="md" pt="md" pb="sm" w="100%">
|
||||||
<FleetToolbar
|
<FleetToolbar
|
||||||
@@ -338,7 +339,7 @@ export default function RoutesPage() {
|
|||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Text fw={600}>{route.name}</Text>
|
<Text fw={600}>{route.name}</Text>
|
||||||
<Badge color={route.isActive ? "green" : "gray"} variant="light" size="sm">
|
<Badge color={route.isActive ? "edr-green" : "gray"} variant="light" size="sm">
|
||||||
{route.isActive ? "Active" : "Inactive"}
|
{route.isActive ? "Active" : "Inactive"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -431,7 +432,7 @@ export default function RoutesPage() {
|
|||||||
<Button variant="default" type="button" onClick={resetForm}>
|
<Button variant="default" type="button" onClick={resetForm}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="green" type="submit" loading={isSaving}>
|
<Button color="edr-green" type="submit" loading={isSaving}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -484,6 +485,6 @@ export default function RoutesPage() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
) : null}
|
) : null}
|
||||||
</Modal>
|
</Modal>
|
||||||
</Stack>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
@@ -8,13 +7,10 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Container,
|
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
Paper,
|
Paper,
|
||||||
SimpleGrid,
|
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
|
||||||
Tabs,
|
Tabs,
|
||||||
Text,
|
Text,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
@@ -41,8 +37,11 @@ import {
|
|||||||
Weight,
|
Weight,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
|
|
||||||
|
import { DataTable, type ColumnDef } from "@edr/ui-common";
|
||||||
|
|
||||||
|
import { KpiStrip, PageContainer } from "@/components/page";
|
||||||
|
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
||||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||||
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
|
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
|
||||||
import { TrainConsistView, CompositionBookingTabs } from "@/components/trainScheduling/compositionEditor";
|
import { TrainConsistView, CompositionBookingTabs } from "@/components/trainScheduling/compositionEditor";
|
||||||
@@ -52,8 +51,6 @@ import {
|
|||||||
totalBookingCount,
|
totalBookingCount,
|
||||||
WindowStatusPill,
|
WindowStatusPill,
|
||||||
} from "@/components/trainScheduling/batchVisuals";
|
} from "@/components/trainScheduling/batchVisuals";
|
||||||
import { MiniRing, MiniSparkline } from "@/components/common/MiniGraph";
|
|
||||||
import type { OverviewAccent } from "@/components/overview/overview.styles";
|
|
||||||
import { RouteCorridor } from "@/components/trainScheduling/scheduleVisuals";
|
import { RouteCorridor } from "@/components/trainScheduling/scheduleVisuals";
|
||||||
import {
|
import {
|
||||||
useBatchBoardDetail,
|
useBatchBoardDetail,
|
||||||
@@ -72,7 +69,7 @@ const STATE_META: Record<
|
|||||||
BatchBoardBookingState,
|
BatchBoardBookingState,
|
||||||
{ label: string; color: string; icon: typeof CheckCircle2 }
|
{ label: string; color: string; icon: typeof CheckCircle2 }
|
||||||
> = {
|
> = {
|
||||||
ALLOCATED: { label: "Allocated", color: "green", icon: CheckCircle2 },
|
ALLOCATED: { label: "Allocated", color: "edr-green", icon: CheckCircle2 },
|
||||||
SELECTED_FOR_BATCH: { label: "Selected for batch", color: "orange", icon: Clock },
|
SELECTED_FOR_BATCH: { label: "Selected for batch", color: "orange", icon: Clock },
|
||||||
READY: { label: "Ready for batch", color: "teal", icon: Hourglass },
|
READY: { label: "Ready for batch", color: "teal", icon: Hourglass },
|
||||||
WAITING: { label: "Paid · waiting", color: "blue", icon: Hourglass },
|
WAITING: { label: "Paid · waiting", color: "blue", icon: Hourglass },
|
||||||
@@ -84,7 +81,7 @@ const ALLOC_META: Record<
|
|||||||
BookingAllocationStatus,
|
BookingAllocationStatus,
|
||||||
{ label: string; color: string }
|
{ label: string; color: string }
|
||||||
> = {
|
> = {
|
||||||
ASSIGNED: { label: "Wagons assigned", color: "green" },
|
ASSIGNED: { label: "Wagons assigned", color: "edr-green" },
|
||||||
NOT_ATTEMPTED: { label: "Not allocated", color: "gray" },
|
NOT_ATTEMPTED: { label: "Not allocated", color: "gray" },
|
||||||
DEFERRED: { label: "Deferred", color: "orange" },
|
DEFERRED: { label: "Deferred", color: "orange" },
|
||||||
FAILED: { label: "Allocation failed", color: "red" },
|
FAILED: { label: "Allocation failed", color: "red" },
|
||||||
@@ -151,140 +148,153 @@ function AllocationBadge({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BookingTable({ bookings }: { bookings: BatchBoardBookingDetail[] }) {
|
const bookingCellMeta = {
|
||||||
if (!bookings.length) {
|
headerClassName: ruleEngineTable.headerCell,
|
||||||
return (
|
cellClassName: ruleEngineTable.bodyCell,
|
||||||
<Text size="sm" c="dimmed" py="sm" ta="center">
|
};
|
||||||
No bookings in this batch window.
|
|
||||||
|
const BOOKING_COLUMNS: ColumnDef<BatchBoardBookingDetail>[] = [
|
||||||
|
{
|
||||||
|
id: "reference",
|
||||||
|
header: "Reference",
|
||||||
|
meta: bookingCellMeta,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const b = row.original;
|
||||||
|
return (
|
||||||
|
<Group gap={6} wrap="nowrap">
|
||||||
|
<Text size="sm" fw={700} c="dark.5">
|
||||||
|
{b.reference}
|
||||||
|
</Text>
|
||||||
|
{b.isGovernment ? (
|
||||||
|
<Badge size="xs" variant="light" color="grape">
|
||||||
|
Gov
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "customer",
|
||||||
|
header: "Customer",
|
||||||
|
meta: bookingCellMeta,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const b = row.original;
|
||||||
|
return (
|
||||||
|
<Group gap={8} wrap="nowrap">
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
borderRadius: 8,
|
||||||
|
flexShrink: 0,
|
||||||
|
background: "#FEF1D5",
|
||||||
|
border: "1px solid #FBD171",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="10px" fw={800} style={{ color: "#B26C09" }}>
|
||||||
|
{initials(b.company)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Text size="sm" c="gray.7" truncate>
|
||||||
|
{b.company}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "contractSigned",
|
||||||
|
header: "Contract signed",
|
||||||
|
meta: bookingCellMeta,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
||||||
|
{fmtDateTime(row.original.fullyExecutedAt)} EAT
|
||||||
</Text>
|
</Text>
|
||||||
);
|
),
|
||||||
}
|
},
|
||||||
|
{
|
||||||
const th = (label: string) => (
|
id: "selectedForBatch",
|
||||||
<Table.Th
|
header: "Selected for batch",
|
||||||
style={{
|
meta: bookingCellMeta,
|
||||||
fontSize: 11,
|
cell: ({ row }) => {
|
||||||
fontWeight: 700,
|
const b = row.original;
|
||||||
textTransform: "uppercase",
|
if (!b.selectedForBatchAt) {
|
||||||
letterSpacing: 0.5,
|
return (
|
||||||
color: "var(--mantine-color-gray-6)",
|
<Text size="sm" c="dimmed">
|
||||||
background: "var(--mantine-color-gray-0)",
|
—
|
||||||
whiteSpace: "nowrap",
|
</Text>
|
||||||
}}
|
);
|
||||||
>
|
}
|
||||||
{label}
|
return (
|
||||||
</Table.Th>
|
<>
|
||||||
);
|
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
||||||
|
{fmtDateTime(b.selectedForBatchAt)} EAT
|
||||||
|
</Text>
|
||||||
|
{b.paymentDeadline ? (
|
||||||
|
<Text size="xs" c="orange.7" fw={600} style={{ whiteSpace: "nowrap" }}>
|
||||||
|
Pay by {fmtDateTime(b.paymentDeadline)} EAT
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "capacity",
|
||||||
|
header: "Capacity",
|
||||||
|
meta: bookingCellMeta,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const b = row.original;
|
||||||
|
return (
|
||||||
|
<Group gap={4} wrap="nowrap">
|
||||||
|
<Badge variant="default" radius="sm" size="sm">
|
||||||
|
{b.wagons}w
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="default" radius="sm" size="sm">
|
||||||
|
{fmtTons(b.weightTons)}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "state",
|
||||||
|
header: "Batch state",
|
||||||
|
meta: bookingCellMeta,
|
||||||
|
cell: ({ row }) => <StateBadge state={row.original.state} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "allocation",
|
||||||
|
header: "Wagon allocation",
|
||||||
|
meta: bookingCellMeta,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<AllocationBadge
|
||||||
|
status={row.original.allocationStatus}
|
||||||
|
issue={row.original.allocationIssue}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function BookingTable({ bookings }: { bookings: BatchBoardBookingDetail[] }) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<DataTable
|
||||||
style={{
|
columns={BOOKING_COLUMNS}
|
||||||
border: "1px solid var(--mantine-color-gray-2)",
|
data={bookings}
|
||||||
borderRadius: 12,
|
status="success"
|
||||||
overflow: "auto",
|
emptyMessage="No bookings in this batch window."
|
||||||
}}
|
containerClassName="overflow-x-auto rounded-lg border border-edr-border"
|
||||||
>
|
/>
|
||||||
<Table highlightOnHover verticalSpacing="sm" horizontalSpacing="md" miw={760}>
|
|
||||||
<Table.Thead>
|
|
||||||
<Table.Tr>
|
|
||||||
{th("Reference")}
|
|
||||||
{th("Customer")}
|
|
||||||
{th("Contract signed")}
|
|
||||||
{th("Selected for batch")}
|
|
||||||
{th("Capacity")}
|
|
||||||
{th("Batch state")}
|
|
||||||
{th("Wagon allocation")}
|
|
||||||
</Table.Tr>
|
|
||||||
</Table.Thead>
|
|
||||||
<Table.Tbody>
|
|
||||||
{bookings.map((b) => (
|
|
||||||
<Table.Tr key={b.id}>
|
|
||||||
<Table.Td>
|
|
||||||
<Group gap={6} wrap="nowrap">
|
|
||||||
<Text size="sm" fw={700} c="dark.5">
|
|
||||||
{b.reference}
|
|
||||||
</Text>
|
|
||||||
{b.isGovernment ? (
|
|
||||||
<Badge size="xs" variant="light" color="grape">
|
|
||||||
Gov
|
|
||||||
</Badge>
|
|
||||||
) : null}
|
|
||||||
</Group>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Group gap={8} wrap="nowrap">
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
width: 26,
|
|
||||||
height: 26,
|
|
||||||
borderRadius: 8,
|
|
||||||
flexShrink: 0,
|
|
||||||
background: "#FEF1D5",
|
|
||||||
border: "1px solid #FBD171",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text size="10px" fw={800} style={{ color: "#B26C09" }}>
|
|
||||||
{initials(b.company)}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Text size="sm" c="gray.7" truncate>
|
|
||||||
{b.company}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
|
||||||
{fmtDateTime(b.fullyExecutedAt)} EAT
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
{b.selectedForBatchAt ? (
|
|
||||||
<>
|
|
||||||
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
|
||||||
{fmtDateTime(b.selectedForBatchAt)} EAT
|
|
||||||
</Text>
|
|
||||||
{b.paymentDeadline ? (
|
|
||||||
<Text size="xs" c="orange.7" fw={600} style={{ whiteSpace: "nowrap" }}>
|
|
||||||
Pay by {fmtDateTime(b.paymentDeadline)} EAT
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
—
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Group gap={4} wrap="nowrap">
|
|
||||||
<Badge variant="default" radius="sm" size="sm">
|
|
||||||
{b.wagons}w
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="default" radius="sm" size="sm">
|
|
||||||
{fmtTons(b.weightTons)}
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<StateBadge state={b.state} />
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<AllocationBadge status={b.allocationStatus} issue={b.allocationIssue} />
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
))}
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WindowCountChips({ counts }: { counts: BatchWindowGroup["counts"] }) {
|
function WindowCountChips({ counts }: { counts: BatchWindowGroup["counts"] }) {
|
||||||
const chips: Array<{ value: number; color: string; label: string }> = [
|
const chips: Array<{ value: number; color: string; label: string }> = [
|
||||||
{ value: counts.allocated, color: "green", label: "allocated" },
|
{ value: counts.allocated, color: "edr-green", label: "allocated" },
|
||||||
{ value: counts.selectedForBatch, color: "orange", label: "selected" },
|
{ value: counts.selectedForBatch, color: "orange", label: "selected" },
|
||||||
{ value: counts.ready, color: "teal", label: "ready" },
|
{ value: counts.ready, color: "teal", label: "ready" },
|
||||||
{ value: counts.waiting, color: "blue", label: "waiting" },
|
{ value: counts.waiting, color: "blue", label: "waiting" },
|
||||||
@@ -415,66 +425,6 @@ function WindowAccordionItem({ window }: { window: BatchWindowGroup }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Light stat card matching the overview look — keeps the explicit numbers.
|
|
||||||
* Capacity cards (pct set) show a ring; count cards show an area/line trend.
|
|
||||||
*/
|
|
||||||
function StatCard({
|
|
||||||
icon: Icon,
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
sub,
|
|
||||||
pct,
|
|
||||||
variant = "area",
|
|
||||||
}: {
|
|
||||||
icon: LucideIcon;
|
|
||||||
label: string;
|
|
||||||
value: ReactNode;
|
|
||||||
sub?: string;
|
|
||||||
pct?: number | null;
|
|
||||||
variant?: "area" | "line";
|
|
||||||
}) {
|
|
||||||
const ringAccent: OverviewAccent =
|
|
||||||
pct == null ? "gold" : pct >= 100 ? "rose" : pct >= 85 ? "orange" : "gold";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper
|
|
||||||
p="md"
|
|
||||||
radius="lg"
|
|
||||||
withBorder
|
|
||||||
style={{ borderColor: "var(--mantine-color-gray-2)", background: "white" }}
|
|
||||||
>
|
|
||||||
<Stack gap={8}>
|
|
||||||
<Group gap="sm" wrap="nowrap" align="center">
|
|
||||||
<ThemeIcon size={38} radius="md" variant="light" color="#F2A516">
|
|
||||||
<Icon size={19} />
|
|
||||||
</ThemeIcon>
|
|
||||||
<Stack gap={1} style={{ minWidth: 0, flex: 1 }}>
|
|
||||||
<Text fw={800} size="lg" lh={1.05} c="dark.5" style={{ whiteSpace: "nowrap" }}>
|
|
||||||
{value}
|
|
||||||
</Text>
|
|
||||||
<Text size="xs" fw={600} c="dimmed" truncate>
|
|
||||||
{label}
|
|
||||||
{sub ? ` · ${sub}` : ""}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
{pct != null ? (
|
|
||||||
<MiniRing pct={pct} accent={ringAccent} size={42} stroke={5}>
|
|
||||||
<Text size="9px" fw={800} c="dark.4">
|
|
||||||
{Math.round(pct)}%
|
|
||||||
</Text>
|
|
||||||
</MiniRing>
|
|
||||||
) : null}
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{pct == null ? (
|
|
||||||
<MiniSparkline variant={variant} accent="gold" baseline={0.5} seed={label} height={20} />
|
|
||||||
) : null}
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BatchScheduleDetailPage() {
|
export default function BatchScheduleDetailPage() {
|
||||||
const { scheduleId } = useParams<{ scheduleId: string }>();
|
const { scheduleId } = useParams<{ scheduleId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -621,11 +571,11 @@ export default function BatchScheduleDetailPage() {
|
|||||||
|
|
||||||
if (isLoading || !data) {
|
if (isLoading || !data) {
|
||||||
return (
|
return (
|
||||||
<Container fluid py="lg" px="xl">
|
<PageContainer fluid>
|
||||||
<Group justify="center" py="xl">
|
<Group justify="center" py="xl">
|
||||||
<Loader color="green" />
|
<Loader color="edr-green" />
|
||||||
</Group>
|
</Group>
|
||||||
</Container>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,7 +591,7 @@ export default function BatchScheduleDetailPage() {
|
|||||||
const totalBookings = totalBookingCount(data.counts);
|
const totalBookings = totalBookingCount(data.counts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container fluid py="lg" px="xl">
|
<PageContainer fluid>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ label: "Operations" },
|
{ label: "Operations" },
|
||||||
@@ -650,7 +600,7 @@ export default function BatchScheduleDetailPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs value={activeTab} onChange={setActiveTab} mt="md">
|
<Tabs value={activeTab} onChange={setActiveTab}>
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Tab value="overview">Overview</Tabs.Tab>
|
<Tabs.Tab value="overview">Overview</Tabs.Tab>
|
||||||
<Tabs.Tab value="composition">
|
<Tabs.Tab value="composition">
|
||||||
@@ -673,7 +623,7 @@ export default function BatchScheduleDetailPage() {
|
|||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Title order={2} fw={800} style={{ color: "#0f172a", letterSpacing: 0.2 }}>
|
<Title order={2} fw={800}>
|
||||||
{data.trainNumber ?? data.routeName ?? "Schedule"}
|
{data.trainNumber ?? data.routeName ?? "Schedule"}
|
||||||
</Title>
|
</Title>
|
||||||
<WindowStatusPill status={data.bookingWindowStatus} />
|
<WindowStatusPill status={data.bookingWindowStatus} />
|
||||||
@@ -715,7 +665,7 @@ export default function BatchScheduleDetailPage() {
|
|||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="edr-green"
|
||||||
radius="md"
|
radius="md"
|
||||||
leftSection={<PlayCircle size={16} />}
|
leftSection={<PlayCircle size={16} />}
|
||||||
loading={runAllocation.isPending}
|
loading={runAllocation.isPending}
|
||||||
@@ -725,7 +675,7 @@ export default function BatchScheduleDetailPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="green"
|
color="edr-green"
|
||||||
radius="md"
|
radius="md"
|
||||||
leftSection={<Layers size={16} />}
|
leftSection={<Layers size={16} />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -743,41 +693,36 @@ export default function BatchScheduleDetailPage() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 1, xs: 2, md: 4 }} spacing="md">
|
<KpiStrip
|
||||||
<StatCard
|
items={[
|
||||||
icon={Boxes}
|
{
|
||||||
label="Allocated wagons"
|
label: "Allocated wagons",
|
||||||
value={data.capacity.allocatedWagons}
|
value: data.capacity.allocatedWagons,
|
||||||
sub="on this train"
|
hint: "on this train",
|
||||||
/>
|
icon: Boxes,
|
||||||
<StatCard
|
},
|
||||||
icon={Ruler}
|
{
|
||||||
label="Train length"
|
label: "Train length",
|
||||||
value={
|
value: data.capacity.maxLengthMeters
|
||||||
data.capacity.maxLengthMeters
|
|
||||||
? `${fmtMeters(data.capacity.allocatedLengthMeters)} / ${fmtMeters(data.capacity.maxLengthMeters)}`
|
? `${fmtMeters(data.capacity.allocatedLengthMeters)} / ${fmtMeters(data.capacity.maxLengthMeters)}`
|
||||||
: fmtMeters(data.capacity.allocatedLengthMeters)
|
: fmtMeters(data.capacity.allocatedLengthMeters),
|
||||||
}
|
icon: Ruler,
|
||||||
pct={lengthPct}
|
},
|
||||||
/>
|
{
|
||||||
<StatCard
|
label: "Weight",
|
||||||
icon={Weight}
|
value: data.capacity.maxWeightTons
|
||||||
label="Weight"
|
|
||||||
value={
|
|
||||||
data.capacity.maxWeightTons
|
|
||||||
? `${fmtTons(data.capacity.usedWeightTons)} / ${fmtTons(data.capacity.maxWeightTons)}`
|
? `${fmtTons(data.capacity.usedWeightTons)} / ${fmtTons(data.capacity.maxWeightTons)}`
|
||||||
: fmtTons(data.capacity.usedWeightTons)
|
: fmtTons(data.capacity.usedWeightTons),
|
||||||
}
|
icon: Weight,
|
||||||
pct={weightPct}
|
},
|
||||||
/>
|
{
|
||||||
<StatCard
|
label: "Bookings",
|
||||||
icon={Package}
|
value: totalBookings,
|
||||||
label="Bookings"
|
hint: `${data.counts.allocated} allocated · ${data.counts.expired} expired`,
|
||||||
value={totalBookings}
|
icon: Package,
|
||||||
sub={`${data.counts.allocated} allocated · ${data.counts.expired} expired`}
|
},
|
||||||
variant="line"
|
]}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
|
||||||
|
|
||||||
{/* Booking pipeline */}
|
{/* Booking pipeline */}
|
||||||
<Paper
|
<Paper
|
||||||
@@ -828,21 +773,9 @@ export default function BatchScheduleDetailPage() {
|
|||||||
style={{ borderColor: "var(--mantine-color-gray-2)" }}
|
style={{ borderColor: "var(--mantine-color-gray-2)" }}
|
||||||
>
|
>
|
||||||
<Group gap="sm" mb={4} wrap="nowrap" align="flex-start">
|
<Group gap="sm" mb={4} wrap="nowrap" align="flex-start">
|
||||||
<Box
|
<ThemeIcon size={38} radius="md" variant="light" color="#F2A516">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
width: 38,
|
|
||||||
height: 38,
|
|
||||||
borderRadius: 10,
|
|
||||||
background: "linear-gradient(135deg, #FBD171, #F2A516)",
|
|
||||||
color: "white",
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Clock size={19} />
|
<Clock size={19} />
|
||||||
</Box>
|
</ThemeIcon>
|
||||||
<Box>
|
<Box>
|
||||||
<Title order={4}>Batch windows (EAT)</Title>
|
<Title order={4}>Batch windows (EAT)</Title>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
@@ -1054,7 +987,7 @@ export default function BatchScheduleDetailPage() {
|
|||||||
style={{ borderColor: "var(--mantine-color-gray-2)" }}
|
style={{ borderColor: "var(--mantine-color-gray-2)" }}
|
||||||
>
|
>
|
||||||
<Group justify="center">
|
<Group justify="center">
|
||||||
<Loader color="green" size="sm" />
|
<Loader color="edr-green" size="sm" />
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
Loading train composition…
|
Loading train composition…
|
||||||
</Text>
|
</Text>
|
||||||
@@ -1063,6 +996,6 @@ export default function BatchScheduleDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Container>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
|
||||||
|
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 } from "@/components/trainScheduling/scheduleVisuals";
|
||||||
import { freightBrand } from "@/theme/freight-brand";
|
import { freightBrand } from "@/theme/freight-brand";
|
||||||
@@ -62,7 +63,7 @@ function MetaStat({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
|
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
|
||||||
<ThemeIcon size={32} radius="md" variant="light" color="green">
|
<ThemeIcon size={32} radius="md" variant="light" color="edr-green">
|
||||||
{icon}
|
{icon}
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Stack gap={0} style={{ minWidth: 0 }}>
|
<Stack gap={0} style={{ minWidth: 0 }}>
|
||||||
@@ -85,18 +86,22 @@ export default function TrainScheduleTrackPage() {
|
|||||||
|
|
||||||
if (trackQuery.isLoading) {
|
if (trackQuery.isLoading) {
|
||||||
return (
|
return (
|
||||||
<Group justify="center" py="xl">
|
<PageContainer>
|
||||||
<Loader size="sm" color="green" />
|
<Group justify="center" py="xl">
|
||||||
</Group>
|
<Loader size="sm" color="edr-green" />
|
||||||
|
</Group>
|
||||||
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const track = trackQuery.data;
|
const track = trackQuery.data;
|
||||||
if (!track || !scheduleId) {
|
if (!track || !scheduleId) {
|
||||||
return (
|
return (
|
||||||
<Text c="dimmed" py="xl">
|
<PageContainer>
|
||||||
Tracking data not found.
|
<Text c="dimmed" py="xl">
|
||||||
</Text>
|
Tracking data not found.
|
||||||
|
</Text>
|
||||||
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +135,7 @@ export default function TrainScheduleTrackPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md" px={{ base: "xs", sm: 0 }} py="md" maw={1080} mx="auto" w="100%">
|
<PageContainer>
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/dashboard/operations/train-scheduling-v2/${scheduleId}`}
|
to={`/dashboard/operations/train-scheduling-v2/${scheduleId}`}
|
||||||
@@ -144,7 +149,7 @@ export default function TrainScheduleTrackPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Paper radius="lg" withBorder p="lg" style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
<Paper radius="lg" withBorder p="lg">
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
||||||
<Group gap="md" align="flex-start" wrap="nowrap" style={{ minWidth: 0 }}>
|
<Group gap="md" align="flex-start" wrap="nowrap" style={{ minWidth: 0 }}>
|
||||||
@@ -169,7 +174,7 @@ export default function TrainScheduleTrackPage() {
|
|||||||
Train tracking
|
Train tracking
|
||||||
</Title>
|
</Title>
|
||||||
{track.trainNumber ? (
|
{track.trainNumber ? (
|
||||||
<Badge variant="light" color="green" radius="sm">
|
<Badge variant="light" color="edr-green" radius="sm">
|
||||||
{track.trainNumber}
|
{track.trainNumber}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -193,7 +198,7 @@ export default function TrainScheduleTrackPage() {
|
|||||||
<Text size="xs" fw={700} c="gray.7" tt="uppercase" style={{ letterSpacing: 0.4 }}>
|
<Text size="xs" fw={700} c="gray.7" tt="uppercase" style={{ letterSpacing: 0.4 }}>
|
||||||
Journey progress
|
Journey progress
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" fw={700} c="green.8">
|
<Text size="xs" fw={700} c="edr-green.8">
|
||||||
{reached} / {totalStations} stations · {Math.round(clampedPct)}%
|
{reached} / {totalStations} stations · {Math.round(clampedPct)}%
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -201,7 +206,7 @@ export default function TrainScheduleTrackPage() {
|
|||||||
value={clampedPct}
|
value={clampedPct}
|
||||||
size="lg"
|
size="lg"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
color="green"
|
color="edr-green"
|
||||||
striped={track.status === "DISPATCHED"}
|
striped={track.status === "DISPATCHED"}
|
||||||
animated={track.status === "DISPATCHED"}
|
animated={track.status === "DISPATCHED"}
|
||||||
/>
|
/>
|
||||||
@@ -230,10 +235,10 @@ export default function TrainScheduleTrackPage() {
|
|||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Corridor */}
|
{/* Corridor */}
|
||||||
<Paper radius="lg" p="lg" withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
<Paper radius="lg" p="lg" withBorder>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group gap="sm" align="center" wrap="nowrap">
|
<Group gap="sm" align="center" wrap="nowrap">
|
||||||
<ThemeIcon size={34} radius="md" variant="light" color="green">
|
<ThemeIcon size={34} radius="md" variant="light" color="edr-green">
|
||||||
<Navigation size={17} />
|
<Navigation size={17} />
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
@@ -264,9 +269,9 @@ export default function TrainScheduleTrackPage() {
|
|||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Checkpoint log */}
|
{/* Checkpoint log */}
|
||||||
<Paper radius="lg" p="lg" withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
<Paper radius="lg" p="lg" withBorder>
|
||||||
<Group gap="sm" align="center" wrap="nowrap" mb="md">
|
<Group gap="sm" align="center" wrap="nowrap" mb="md">
|
||||||
<ThemeIcon size={34} radius="md" variant="light" color="green">
|
<ThemeIcon size={34} radius="md" variant="light" color="edr-green">
|
||||||
<CheckCircle2 size={17} />
|
<CheckCircle2 size={17} />
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
@@ -292,7 +297,7 @@ export default function TrainScheduleTrackPage() {
|
|||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Timeline active={track.checkpoints.length} bulletSize={22} lineWidth={2} color="green">
|
<Timeline active={track.checkpoints.length} bulletSize={22} lineWidth={2} color="edr-green">
|
||||||
{track.checkpoints.map((cp) => (
|
{track.checkpoints.map((cp) => (
|
||||||
<Timeline.Item
|
<Timeline.Item
|
||||||
key={cp.id}
|
key={cp.id}
|
||||||
@@ -306,7 +311,7 @@ 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" : "green"}
|
color={cp.kind === "ARRIVED" ? "teal" : cp.kind === "DEPARTED" ? "blue" : "edr-green"}
|
||||||
>
|
>
|
||||||
{cp.kind}
|
{cp.kind}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -326,6 +331,6 @@ export default function TrainScheduleTrackPage() {
|
|||||||
</Timeline>
|
</Timeline>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import {
|
||||||
import { Link, useParams } from "react-router-dom";
|
Badge,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Group,
|
||||||
|
Loader,
|
||||||
|
Paper,
|
||||||
|
RingProgress,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
import { isAxiosError } from "axios";
|
import { isAxiosError } from "axios";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
@@ -15,48 +27,35 @@ import {
|
|||||||
Train,
|
Train,
|
||||||
Weight,
|
Weight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
Badge,
|
import { Link, useParams } from "react-router-dom";
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Group,
|
|
||||||
Loader,
|
|
||||||
Paper,
|
|
||||||
RingProgress,
|
|
||||||
SimpleGrid,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
ThemeIcon,
|
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
|
||||||
|
|
||||||
import { ContainerPlacementGrid } from "@/components/trainScheduling/ContainerPlacementGrid";
|
import { KpiStrip, PageContainer } from "@/components/page";
|
||||||
import {
|
import {
|
||||||
autoFillPlacements,
|
autoFillPlacements,
|
||||||
mergePlacementsWithSaved,
|
mergePlacementsWithSaved,
|
||||||
placementsFromScheduleWagons,
|
placementsFromScheduleWagons,
|
||||||
validateLocalPlacements,
|
validateLocalPlacements,
|
||||||
} from "@/components/trainScheduling/containerPlacement.util";
|
} from "@/components/trainScheduling/containerPlacement.util";
|
||||||
|
import { ContainerPlacementGrid } from "@/components/trainScheduling/ContainerPlacementGrid";
|
||||||
import { FleetAvailabilitySummary } from "@/components/trainScheduling/FleetAvailabilitySummary";
|
import { FleetAvailabilitySummary } from "@/components/trainScheduling/FleetAvailabilitySummary";
|
||||||
|
import { RescheduleTrainDialog } from "@/components/trainScheduling/RescheduleTrainDialog";
|
||||||
|
import { ScheduleBatchPanel } from "@/components/trainScheduling/ScheduleBatchPanel";
|
||||||
import { ScheduleBookingsStep } from "@/components/trainScheduling/ScheduleBookingsStep";
|
import { ScheduleBookingsStep } from "@/components/trainScheduling/ScheduleBookingsStep";
|
||||||
|
import { FreightTypeBadge } from "@/components/trainScheduling/ScheduleStatusBadge";
|
||||||
|
import {
|
||||||
|
RouteCorridor,
|
||||||
|
StatusPill,
|
||||||
|
scheduleBrand,
|
||||||
|
} from "@/components/trainScheduling/scheduleVisuals";
|
||||||
import {
|
import {
|
||||||
PreviewSummary,
|
PreviewSummary,
|
||||||
ScheduleWarningsAlert,
|
ScheduleWarningsAlert,
|
||||||
} from "@/components/trainScheduling/ScheduleWarningsAlert";
|
} from "@/components/trainScheduling/ScheduleWarningsAlert";
|
||||||
import { FreightTypeBadge } from "@/components/trainScheduling/ScheduleStatusBadge";
|
|
||||||
import {
|
|
||||||
RouteCorridor,
|
|
||||||
StatTile,
|
|
||||||
StatusPill,
|
|
||||||
scheduleBrand,
|
|
||||||
} from "@/components/trainScheduling/scheduleVisuals";
|
|
||||||
import { RescheduleTrainDialog } from "@/components/trainScheduling/RescheduleTrainDialog";
|
|
||||||
import { ScheduleBatchPanel } from "@/components/trainScheduling/ScheduleBatchPanel";
|
|
||||||
import { WorkflowRail, WorkflowStep } from "@/components/trainScheduling/WorkflowStep";
|
|
||||||
import { shouldShowContainerPlacementStep } from "@/components/trainScheduling/schedulingContainerStep.util";
|
import { shouldShowContainerPlacementStep } from "@/components/trainScheduling/schedulingContainerStep.util";
|
||||||
import { WagonPlanGrid } from "@/components/trainScheduling/WagonPlanGrid";
|
|
||||||
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
|
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
|
||||||
|
import { WagonPlanGrid } from "@/components/trainScheduling/WagonPlanGrid";
|
||||||
|
import { WorkflowRail, WorkflowStep } from "@/components/trainScheduling/WorkflowStep";
|
||||||
import {
|
import {
|
||||||
useEligibleBookings,
|
useEligibleBookings,
|
||||||
useScheduleDetail,
|
useScheduleDetail,
|
||||||
@@ -267,17 +266,21 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
|
|
||||||
if (detailQuery.isLoading) {
|
if (detailQuery.isLoading) {
|
||||||
return (
|
return (
|
||||||
<Group justify="center" py="xl">
|
<PageContainer>
|
||||||
<Loader size="sm" />
|
<Group justify="center" py="xl">
|
||||||
</Group>
|
<Loader size="sm" />
|
||||||
|
</Group>
|
||||||
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!schedule || !scheduleId) {
|
if (!schedule || !scheduleId) {
|
||||||
return (
|
return (
|
||||||
<Text c="dimmed" py="xl">
|
<PageContainer>
|
||||||
Schedule not found
|
<Text c="dimmed" py="xl">
|
||||||
</Text>
|
Schedule not found
|
||||||
|
</Text>
|
||||||
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +408,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
variant="light"
|
variant="light"
|
||||||
color={previewResult.valid ? "green" : "red"}
|
color={previewResult.valid ? "edr-green" : "red"}
|
||||||
radius="sm"
|
radius="sm"
|
||||||
>
|
>
|
||||||
{previewResult.valid ? "Plan valid" : "Has issues"}
|
{previewResult.valid ? "Plan valid" : "Has issues"}
|
||||||
@@ -413,14 +416,14 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return allSelectedIds.length ? (
|
return allSelectedIds.length ? (
|
||||||
<Badge variant="light" color="green" radius="sm">
|
<Badge variant="light" color="edr-green" radius="sm">
|
||||||
{allSelectedIds.length} selected
|
{allSelectedIds.length} selected
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
if (key === "wagon" && displayWagonPlan.length) {
|
if (key === "wagon" && displayWagonPlan.length) {
|
||||||
return (
|
return (
|
||||||
<Badge variant="light" color="green" radius="sm">
|
<Badge variant="light" color="edr-green" radius="sm">
|
||||||
{displayWagonPlan.length} wagons
|
{displayWagonPlan.length} wagons
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
@@ -429,7 +432,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
variant="light"
|
variant="light"
|
||||||
color={containerComplete ? "green" : "yellow"}
|
color={containerComplete ? "edr-green" : "yellow"}
|
||||||
radius="sm"
|
radius="sm"
|
||||||
>
|
>
|
||||||
{containerUnits.length} units
|
{containerUnits.length} units
|
||||||
@@ -485,7 +488,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="edr-green"
|
||||||
radius="md"
|
radius="md"
|
||||||
leftSection={<Eye size={16} />}
|
leftSection={<Eye size={16} />}
|
||||||
loading={preview.isPending}
|
loading={preview.isPending}
|
||||||
@@ -532,7 +535,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
<Group>
|
<Group>
|
||||||
{!hasContainerStep ? (
|
{!hasContainerStep ? (
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="edr-green"
|
||||||
radius="md"
|
radius="md"
|
||||||
loading={assign.isPending}
|
loading={assign.isPending}
|
||||||
onClick={handleAssign}
|
onClick={handleAssign}
|
||||||
@@ -541,7 +544,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="edr-green"
|
||||||
radius="md"
|
radius="md"
|
||||||
rightSection={<ContainerIcon size={16} />}
|
rightSection={<ContainerIcon size={16} />}
|
||||||
onClick={() => setActiveStep(2)}
|
onClick={() => setActiveStep(2)}
|
||||||
@@ -578,7 +581,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
{canEditBookings ? (
|
{canEditBookings ? (
|
||||||
<Group>
|
<Group>
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="edr-green"
|
||||||
radius="md"
|
radius="md"
|
||||||
loading={assign.isPending}
|
loading={assign.isPending}
|
||||||
onClick={handleAssign}
|
onClick={handleAssign}
|
||||||
@@ -629,7 +632,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
<Text fw={600}>Ready to depart</Text>
|
<Text fw={600}>Ready to depart</Text>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
Finalizing locks the plan and moves the schedule to{" "}
|
Finalizing locks the plan and moves the schedule to{" "}
|
||||||
<Text span fw={600} c="green.7">
|
<Text span fw={600} c="edr-green.7">
|
||||||
SCHEDULED
|
SCHEDULED
|
||||||
</Text>
|
</Text>
|
||||||
. Dispatch then begins rail movement and notifies the yard.
|
. Dispatch then begins rail movement and notifies the yard.
|
||||||
@@ -640,7 +643,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
<Group>
|
<Group>
|
||||||
{canFinalize ? (
|
{canFinalize ? (
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="edr-green"
|
||||||
size="md"
|
size="md"
|
||||||
radius="md"
|
radius="md"
|
||||||
leftSection={<CheckCircle2 size={18} />}
|
leftSection={<CheckCircle2 size={18} />}
|
||||||
@@ -663,7 +666,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
{canDispatch ? (
|
{canDispatch ? (
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="edr-green"
|
||||||
size="md"
|
size="md"
|
||||||
radius="md"
|
radius="md"
|
||||||
leftSection={<Send size={18} />}
|
leftSection={<Send size={18} />}
|
||||||
@@ -695,7 +698,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<PageContainer>
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/dashboard/operations/train-scheduling-v2"
|
to="/dashboard/operations/train-scheduling-v2"
|
||||||
@@ -711,13 +714,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
<Paper
|
<Paper
|
||||||
radius="xl"
|
radius="xl"
|
||||||
p="xl"
|
p="xl"
|
||||||
style={{
|
style={{ position: "relative", overflow: "hidden" }}
|
||||||
position: "relative",
|
|
||||||
overflow: "hidden",
|
|
||||||
background: "#ffffff",
|
|
||||||
border: "1px solid var(--mantine-color-gray-2)",
|
|
||||||
boxShadow: "0 1px 3px rgba(15,23,42,0.04)",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Stack gap="lg" style={{ position: "relative" }}>
|
<Stack gap="lg" style={{ position: "relative" }}>
|
||||||
<Group justify="space-between" align="flex-start" wrap="wrap">
|
<Group justify="space-between" align="flex-start" wrap="wrap">
|
||||||
@@ -758,7 +755,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/dashboard/operations/train-scheduling-v2/${scheduleId}/track`}
|
to={`/dashboard/operations/train-scheduling-v2/${scheduleId}/track`}
|
||||||
color="green"
|
color="edr-green"
|
||||||
radius="lg"
|
radius="lg"
|
||||||
size="sm"
|
size="sm"
|
||||||
leftSection={<Navigation size={16} />}
|
leftSection={<Navigation size={16} />}
|
||||||
@@ -779,63 +776,12 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="md">
|
|
||||||
<StatTile
|
|
||||||
icon={Train}
|
|
||||||
label="Locomotive"
|
|
||||||
value={schedule.trainSet?.locomotive?.code ?? "—"}
|
|
||||||
hint={
|
|
||||||
schedule.trainSet?.locomotive?.currentYardId
|
|
||||||
? schedule.trainSet.locomotive.currentYardId === schedule.originStation?.id
|
|
||||||
? `At ${schedule.originStation?.label ?? schedule.originStation?.code ?? "origin yard"}`
|
|
||||||
: "Not at schedule origin yard"
|
|
||||||
: "No current yard set"
|
|
||||||
}
|
|
||||||
accent="#F2A516"
|
|
||||||
graph="area"
|
|
||||||
graphAccent="gold"
|
|
||||||
/>
|
|
||||||
<StatTile
|
|
||||||
icon={Package}
|
|
||||||
label="Bookings"
|
|
||||||
value={schedule.bookings?.length ?? 0}
|
|
||||||
accent="#FB8C2E"
|
|
||||||
graph="line"
|
|
||||||
graphAccent="orange"
|
|
||||||
/>
|
|
||||||
<StatTile
|
|
||||||
icon={Weight}
|
|
||||||
label="Wagons / load"
|
|
||||||
value={`${schedule.trainSet?.wagonCount ?? displayWagonPlan.length} · ${
|
|
||||||
schedule.trainSet?.totalWeightTons ?? 0
|
|
||||||
}T`}
|
|
||||||
accent="#F2A516"
|
|
||||||
graph="area"
|
|
||||||
graphAccent="gold"
|
|
||||||
/>
|
|
||||||
<StatTile
|
|
||||||
icon={CalendarClock}
|
|
||||||
label="Departure"
|
|
||||||
value={new Date(schedule.scheduledDepartureDate).toLocaleDateString(
|
|
||||||
"en",
|
|
||||||
{ month: "short", day: "2-digit" },
|
|
||||||
)}
|
|
||||||
hint={new Date(schedule.scheduledDepartureDate).toLocaleTimeString("en", {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
})}
|
|
||||||
accent="#FB8C2E"
|
|
||||||
graph="line"
|
|
||||||
graphAccent="orange"
|
|
||||||
/>
|
|
||||||
</SimpleGrid>
|
|
||||||
|
|
||||||
{previewResult ? (
|
{previewResult ? (
|
||||||
<Badge
|
<Badge
|
||||||
size="lg"
|
size="lg"
|
||||||
radius="sm"
|
radius="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
color={previewResult.valid ? "green" : "red"}
|
color={previewResult.valid ? "edr-green" : "red"}
|
||||||
leftSection={
|
leftSection={
|
||||||
<Box
|
<Box
|
||||||
w={8}
|
w={8}
|
||||||
@@ -843,7 +789,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
style={{
|
style={{
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
background: previewResult.valid
|
background: previewResult.valid
|
||||||
? "var(--mantine-color-green-6)"
|
? "var(--mantine-color-edr-green-6)"
|
||||||
: "var(--mantine-color-red-6)",
|
: "var(--mantine-color-red-6)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -855,17 +801,51 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Paper radius="xl" p="lg" withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
<KpiStrip
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: "Locomotive",
|
||||||
|
value: schedule.trainSet?.locomotive?.code ?? "—",
|
||||||
|
hint: schedule.trainSet?.locomotive?.currentYardId
|
||||||
|
? schedule.trainSet.locomotive.currentYardId === schedule.originStation?.id
|
||||||
|
? `At ${schedule.originStation?.label ?? schedule.originStation?.code ?? "origin yard"}`
|
||||||
|
: "Not at schedule origin yard"
|
||||||
|
: "No current yard set",
|
||||||
|
icon: Train,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Bookings",
|
||||||
|
value: schedule.bookings?.length ?? 0,
|
||||||
|
icon: Package,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Wagons / load",
|
||||||
|
value: `${schedule.trainSet?.wagonCount ?? displayWagonPlan.length} · ${
|
||||||
|
schedule.trainSet?.totalWeightTons ?? 0
|
||||||
|
}T`,
|
||||||
|
icon: Weight,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Departure",
|
||||||
|
value: new Date(schedule.scheduledDepartureDate).toLocaleDateString("en", {
|
||||||
|
month: "short",
|
||||||
|
day: "2-digit",
|
||||||
|
}),
|
||||||
|
hint: new Date(schedule.scheduledDepartureDate).toLocaleTimeString("en", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}),
|
||||||
|
icon: CalendarClock,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Paper radius="xl" p="lg">
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
{/* Workflow header with ring progress */}
|
{/* Workflow header with ring progress */}
|
||||||
<Group justify="space-between" align="center" wrap="nowrap">
|
<Group justify="space-between" align="center" wrap="nowrap">
|
||||||
<Group gap="md" align="center" wrap="nowrap">
|
<Group gap="md" align="center" wrap="nowrap">
|
||||||
<ThemeIcon
|
<ThemeIcon size={44} radius="md" variant="light" color="edr-green">
|
||||||
size={44}
|
|
||||||
radius="md"
|
|
||||||
variant="gradient"
|
|
||||||
gradient={{ from: "green", to: "teal", deg: 135 }}
|
|
||||||
>
|
|
||||||
<RouteIcon size={22} />
|
<RouteIcon size={22} />
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Stack gap={2}>
|
<Stack gap={2}>
|
||||||
@@ -882,9 +862,9 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
size={64}
|
size={64}
|
||||||
thickness={6}
|
thickness={6}
|
||||||
roundCaps
|
roundCaps
|
||||||
sections={[{ value: progressPct, color: "green" }]}
|
sections={[{ value: progressPct, color: "edr-green" }]}
|
||||||
label={
|
label={
|
||||||
<Text ta="center" size="xs" fw={700} c="green.7">
|
<Text ta="center" size="xs" fw={700} c="edr-green.7">
|
||||||
{progressPct}%
|
{progressPct}%
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
@@ -928,6 +908,6 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
onComplete={() => void detailQuery.refetch()}
|
onComplete={() => void detailQuery.refetch()}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Stack>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import {
|
|||||||
Loader,
|
Loader,
|
||||||
Stack,
|
Stack,
|
||||||
Select,
|
Select,
|
||||||
Table,
|
|
||||||
Tabs,
|
Tabs,
|
||||||
Text,
|
Text,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { ArrowLeft, Boxes, LayoutGrid, Package, Pencil, Plus } from 'lucide-react';
|
import { ArrowLeft, Boxes, LayoutGrid, Package, Pencil, Plus } from 'lucide-react';
|
||||||
|
|
||||||
|
import { DataTable, type ColumnDef } from '@edr/ui-common';
|
||||||
|
|
||||||
import { KpiStrip, PageContainer, PageHeader } from '@/components/page';
|
import { KpiStrip, PageContainer, PageHeader } from '@/components/page';
|
||||||
import {
|
import {
|
||||||
CreateYardModal,
|
CreateYardModal,
|
||||||
@@ -58,6 +59,90 @@ export default function WarehouseDetailPage() {
|
|||||||
[yards],
|
[yards],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const yardColumns = useMemo<ColumnDef<WarehouseYard>[]>(
|
||||||
|
() => [
|
||||||
|
{ id: 'name', header: 'Name', cell: ({ row }) => row.original.name },
|
||||||
|
{ id: 'code', header: 'Code', cell: ({ row }) => row.original.code },
|
||||||
|
{ id: 'type', header: 'Type', cell: ({ row }) => humanizeEnum(row.original.type) },
|
||||||
|
{
|
||||||
|
id: 'weight',
|
||||||
|
header: 'Weight (cur / cap)',
|
||||||
|
cell: ({ row }) => formatCapacity(row.original.currentWeight, row.original.capacityWeight),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'containers',
|
||||||
|
header: 'Containers (cur / cap)',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
formatCapacity(row.original.currentContainers, row.original.capacityContainers),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
cell: ({ row }) => <WarehouseStatusBadge status={row.original.status} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: 'Actions',
|
||||||
|
meta: { headerClassName: 'text-right', cellClassName: 'text-right' },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingYard(row.original);
|
||||||
|
setYardModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const zoneColumns = useMemo<ColumnDef<WarehouseZone>[]>(
|
||||||
|
() => [
|
||||||
|
{ id: 'name', header: 'Name', cell: ({ row }) => row.original.name },
|
||||||
|
{ id: 'code', header: 'Code', cell: ({ row }) => row.original.code },
|
||||||
|
{ id: 'type', header: 'Type', cell: ({ row }) => humanizeEnum(row.original.type) },
|
||||||
|
{
|
||||||
|
id: 'weight',
|
||||||
|
header: 'Weight (cur / cap)',
|
||||||
|
cell: ({ row }) => formatCapacity(row.original.currentWeight, row.original.capacityWeight),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'containers',
|
||||||
|
header: 'Containers (cur / cap)',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
formatCapacity(row.original.currentContainers, row.original.capacityContainers),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
cell: ({ row }) => <WarehouseStatusBadge status={row.original.status} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: 'Actions',
|
||||||
|
meta: { headerClassName: 'text-right', cellClassName: 'text-right' },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingZone(row.original);
|
||||||
|
setZoneModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Center mih="60vh">
|
<Center mih="60vh">
|
||||||
@@ -154,51 +239,27 @@ export default function WarehouseDetailPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{yards.length === 0 ? (
|
<DataTable
|
||||||
<Text c="dimmed" ta="center" py="lg">
|
columns={yardColumns}
|
||||||
No yards yet.
|
data={yards}
|
||||||
</Text>
|
status={
|
||||||
) : (
|
yardsQuery.isLoading
|
||||||
<Table highlightOnHover verticalSpacing="sm" striped>
|
? 'loading'
|
||||||
<Table.Thead>
|
: yardsQuery.isError
|
||||||
<Table.Tr>
|
? 'error'
|
||||||
<Table.Th>Name</Table.Th>
|
: 'success'
|
||||||
<Table.Th>Code</Table.Th>
|
}
|
||||||
<Table.Th>Type</Table.Th>
|
emptyMessage="No yards yet."
|
||||||
<Table.Th>Weight (cur / cap)</Table.Th>
|
containerClassName="border-0 shadow-none"
|
||||||
<Table.Th>Containers (cur / cap)</Table.Th>
|
error={
|
||||||
<Table.Th>Status</Table.Th>
|
yardsQuery.isError
|
||||||
<Table.Th ta="right">Actions</Table.Th>
|
? {
|
||||||
</Table.Tr>
|
message: 'Failed to load yards.',
|
||||||
</Table.Thead>
|
onRetry: () => void yardsQuery.refetch(),
|
||||||
<Table.Tbody>
|
}
|
||||||
{yards.map((yard) => (
|
: undefined
|
||||||
<Table.Tr key={yard.id}>
|
}
|
||||||
<Table.Td>{yard.name}</Table.Td>
|
/>
|
||||||
<Table.Td>{yard.code}</Table.Td>
|
|
||||||
<Table.Td>{humanizeEnum(yard.type)}</Table.Td>
|
|
||||||
<Table.Td>{formatCapacity(yard.currentWeight, yard.capacityWeight)}</Table.Td>
|
|
||||||
<Table.Td>{formatCapacity(yard.currentContainers, yard.capacityContainers)}</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<WarehouseStatusBadge status={yard.status} />
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td ta="right">
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
color="gray"
|
|
||||||
onClick={() => {
|
|
||||||
setEditingYard(yard);
|
|
||||||
setYardModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Pencil size={16} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
))}
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
@@ -234,50 +295,28 @@ export default function WarehouseDetailPage() {
|
|||||||
<Text c="dimmed" ta="center" py="lg">
|
<Text c="dimmed" ta="center" py="lg">
|
||||||
Select a yard to view its zones.
|
Select a yard to view its zones.
|
||||||
</Text>
|
</Text>
|
||||||
) : (zonesQuery.data ?? []).length === 0 ? (
|
|
||||||
<Text c="dimmed" ta="center" py="lg">
|
|
||||||
No zones in this yard yet.
|
|
||||||
</Text>
|
|
||||||
) : (
|
) : (
|
||||||
<Table highlightOnHover verticalSpacing="sm" striped>
|
<DataTable
|
||||||
<Table.Thead>
|
columns={zoneColumns}
|
||||||
<Table.Tr>
|
data={zonesQuery.data ?? []}
|
||||||
<Table.Th>Name</Table.Th>
|
status={
|
||||||
<Table.Th>Code</Table.Th>
|
zonesQuery.isLoading
|
||||||
<Table.Th>Type</Table.Th>
|
? 'loading'
|
||||||
<Table.Th>Weight (cur / cap)</Table.Th>
|
: zonesQuery.isError
|
||||||
<Table.Th>Containers (cur / cap)</Table.Th>
|
? 'error'
|
||||||
<Table.Th>Status</Table.Th>
|
: 'success'
|
||||||
<Table.Th ta="right">Actions</Table.Th>
|
}
|
||||||
</Table.Tr>
|
emptyMessage="No zones in this yard yet."
|
||||||
</Table.Thead>
|
containerClassName="border-0 shadow-none"
|
||||||
<Table.Tbody>
|
error={
|
||||||
{(zonesQuery.data ?? []).map((zone) => (
|
zonesQuery.isError
|
||||||
<Table.Tr key={zone.id}>
|
? {
|
||||||
<Table.Td>{zone.name}</Table.Td>
|
message: 'Failed to load zones.',
|
||||||
<Table.Td>{zone.code}</Table.Td>
|
onRetry: () => void zonesQuery.refetch(),
|
||||||
<Table.Td>{humanizeEnum(zone.type)}</Table.Td>
|
}
|
||||||
<Table.Td>{formatCapacity(zone.currentWeight, zone.capacityWeight)}</Table.Td>
|
: undefined
|
||||||
<Table.Td>{formatCapacity(zone.currentContainers, zone.capacityContainers)}</Table.Td>
|
}
|
||||||
<Table.Td>
|
/>
|
||||||
<WarehouseStatusBadge status={zone.status} />
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td ta="right">
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
color="gray"
|
|
||||||
onClick={() => {
|
|
||||||
setEditingZone(zone);
|
|
||||||
setZoneModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Pencil size={16} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
))}
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user