mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
style: WIP BatchBoard and train schedule
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
Alert,
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Container,
|
|
||||||
Group,
|
Group,
|
||||||
|
Menu,
|
||||||
Paper,
|
Paper,
|
||||||
RingProgress,
|
RingProgress,
|
||||||
Select,
|
Select,
|
||||||
@@ -21,7 +22,9 @@ import {
|
|||||||
ArrowRight,
|
ArrowRight,
|
||||||
CalendarClock,
|
CalendarClock,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
|
Eye,
|
||||||
Inbox,
|
Inbox,
|
||||||
|
MoreHorizontal,
|
||||||
Package,
|
Package,
|
||||||
Ruler,
|
Ruler,
|
||||||
Train,
|
Train,
|
||||||
@@ -31,7 +34,7 @@ import {
|
|||||||
import type { ColumnDef } from "@edr/ui-common";
|
import type { ColumnDef } from "@edr/ui-common";
|
||||||
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
|
import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common";
|
||||||
|
|
||||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
import { KpiStrip, PageContainer, PageHeader } 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 {
|
import {
|
||||||
@@ -40,7 +43,7 @@ import {
|
|||||||
totalBookingCount,
|
totalBookingCount,
|
||||||
WindowStatusPill,
|
WindowStatusPill,
|
||||||
} from "@/components/trainScheduling/batchVisuals";
|
} from "@/components/trainScheduling/batchVisuals";
|
||||||
import { RouteCorridor, StatTile } from "@/components/trainScheduling/scheduleVisuals";
|
import { RouteCorridor } from "@/components/trainScheduling/scheduleVisuals";
|
||||||
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
||||||
import { FREIGHT_BRAND, FREIGHT_BRAND_DARK } from "@/theme/freight-brand";
|
import { FREIGHT_BRAND, FREIGHT_BRAND_DARK } from "@/theme/freight-brand";
|
||||||
import { useBatchBoard } from "@/hooks/trainScheduling/useTrainScheduling";
|
import { useBatchBoard } from "@/hooks/trainScheduling/useTrainScheduling";
|
||||||
@@ -365,7 +368,7 @@ function CardSkeleton() {
|
|||||||
|
|
||||||
export default function BatchBoardPage() {
|
export default function BatchBoardPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data, isLoading, isFetching, refetch } = useBatchBoard();
|
const { data, isLoading, isError, isFetching, refetch } = useBatchBoard();
|
||||||
const { viewMode, setViewMode } = useFleetViewMode("batch-board");
|
const { viewMode, setViewMode } = useFleetViewMode("batch-board");
|
||||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -549,72 +552,69 @@ export default function BatchBoardPage() {
|
|||||||
header: "",
|
header: "",
|
||||||
meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` },
|
meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` },
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Group justify="flex-end" wrap="nowrap">
|
<Group justify="flex-end" wrap="nowrap" onClick={(e) => e.stopPropagation()}>
|
||||||
<Button
|
<Menu position="bottom-end" withinPortal shadow="md" width={180}>
|
||||||
variant="light"
|
<Menu.Target>
|
||||||
color="green"
|
<ActionIcon variant="subtle" color="gray" aria-label="Row actions">
|
||||||
size="compact-sm"
|
<MoreHorizontal size={16} />
|
||||||
rightSection={<ArrowRight size={14} />}
|
</ActionIcon>
|
||||||
onClick={() =>
|
</Menu.Target>
|
||||||
navigate(`/dashboard/operations/batch-board/${row.original.scheduleId}`)
|
<Menu.Dropdown>
|
||||||
}
|
<Menu.Item
|
||||||
>
|
leftSection={<Eye size={15} />}
|
||||||
View windows
|
onClick={() =>
|
||||||
</Button>
|
navigate(`/dashboard/operations/batch-board/${row.original.scheduleId}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
View windows
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
</Group>
|
</Group>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
const tableStatus = isLoading ? "loading" : "success";
|
const tableStatus = isLoading ? "loading" : isError ? "error" : "success";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container fluid py="lg" px="xl">
|
<PageContainer>
|
||||||
<Breadcrumbs items={[{ label: "Operations" }, { label: "Batch board" }]} />
|
<PageHeader
|
||||||
|
title="Batch Board"
|
||||||
|
subtitle="Active schedules and their booking windows."
|
||||||
|
action={
|
||||||
|
<Button variant="default" loading={isFetching} onClick={() => void refetch()}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 1, xs: 3 }} spacing="md" mt="md">
|
<KpiStrip
|
||||||
<StatTile
|
loading={isLoading}
|
||||||
icon={Train}
|
items={[
|
||||||
label="Active schedules"
|
{
|
||||||
value={isLoading ? "…" : schedules.length}
|
label: "Active schedules",
|
||||||
hint="on the board right now"
|
value: schedules.length,
|
||||||
accent="#F2A516"
|
hint: "on the board right now",
|
||||||
graph="area"
|
icon: Train,
|
||||||
graphAccent="gold"
|
},
|
||||||
/>
|
{
|
||||||
<StatTile
|
label: "Open windows",
|
||||||
icon={CalendarDays}
|
value: summary.openWindows,
|
||||||
label="Open windows"
|
hint: "accepting bookings",
|
||||||
value={isLoading ? "…" : summary.openWindows}
|
icon: CalendarDays,
|
||||||
hint="accepting bookings"
|
},
|
||||||
accent="#FB8C2E"
|
{
|
||||||
graph="line"
|
label: "Bookings in play",
|
||||||
graphAccent="orange"
|
value: summary.totalBookings,
|
||||||
/>
|
hint: `${summary.totalWagons} wagons allocated`,
|
||||||
<StatTile
|
icon: Package,
|
||||||
icon={Package}
|
},
|
||||||
label="Bookings in play"
|
]}
|
||||||
value={isLoading ? "…" : summary.totalBookings}
|
/>
|
||||||
hint={`${summary.totalWagons} wagons allocated`}
|
|
||||||
accent="#F2A516"
|
|
||||||
graph="ring"
|
|
||||||
graphAccent="gold"
|
|
||||||
graphPct={
|
|
||||||
schedules.length
|
|
||||||
? Math.min(100, Math.round((summary.openWindows / schedules.length) * 100))
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SimpleGrid>
|
|
||||||
|
|
||||||
<Card
|
<Card p={0}>
|
||||||
radius="lg"
|
|
||||||
padding={0}
|
|
||||||
withBorder
|
|
||||||
mt="lg"
|
|
||||||
style={{ borderColor: "var(--mantine-color-gray-2)" }}
|
|
||||||
>
|
|
||||||
<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
|
||||||
@@ -647,6 +647,17 @@ export default function BatchBoardPage() {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={paged}
|
data={paged}
|
||||||
status={tableStatus}
|
status={tableStatus}
|
||||||
|
onRowClick={(schedule) =>
|
||||||
|
navigate(`/dashboard/operations/batch-board/${schedule.scheduleId}`)
|
||||||
|
}
|
||||||
|
error={
|
||||||
|
isError
|
||||||
|
? {
|
||||||
|
message: "Failed to load the batch board.",
|
||||||
|
onRetry: () => void refetch(),
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
emptyMessage="No active schedules"
|
emptyMessage="No active schedules"
|
||||||
pagination={{
|
pagination={{
|
||||||
pageIndex: pagination.pageIndex,
|
pageIndex: pagination.pageIndex,
|
||||||
@@ -688,7 +699,6 @@ export default function BatchBoardPage() {
|
|||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
background: "white",
|
background: "white",
|
||||||
border: "1px solid var(--mantine-color-gray-2)",
|
border: "1px solid var(--mantine-color-gray-2)",
|
||||||
boxShadow: "0 4px 12px rgba(15,23,42,0.06)",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Inbox size={28} color="var(--mantine-color-gray-5)" />
|
<Inbox size={28} color="var(--mantine-color-gray-5)" />
|
||||||
@@ -711,18 +721,6 @@ export default function BatchBoardPage() {
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
</PageContainer>
|
||||||
<Group justify="flex-end" mt="md">
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
color="gray"
|
|
||||||
size="xs"
|
|
||||||
loading={isFetching}
|
|
||||||
onClick={() => void refetch()}
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { isAxiosError } from "axios";
|
import { isAxiosError } from "axios";
|
||||||
import type { ColumnDef } from "@edr/ui-common";
|
import type { ColumnDef } from "@edr/ui-common";
|
||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Group,
|
Group,
|
||||||
|
Menu,
|
||||||
Modal,
|
Modal,
|
||||||
Select,
|
Select,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
@@ -15,16 +17,25 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { ArrowRight, CalendarClock, Navigation, Send, Train, Weight } from "lucide-react";
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
Ban,
|
||||||
|
CalendarClock,
|
||||||
|
Eye,
|
||||||
|
MoreHorizontal,
|
||||||
|
Navigation,
|
||||||
|
Send,
|
||||||
|
Train,
|
||||||
|
Weight,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import { KpiStrip, PageContainer, PageHeader } 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 { FreightTypeBadge } from "@/components/trainScheduling/ScheduleStatusBadge";
|
import { FreightTypeBadge } from "@/components/trainScheduling/ScheduleStatusBadge";
|
||||||
import {
|
import {
|
||||||
RouteCorridor,
|
RouteCorridor,
|
||||||
StatTile,
|
|
||||||
StatusPill,
|
StatusPill,
|
||||||
scheduleBrand,
|
|
||||||
} from "@/components/trainScheduling/scheduleVisuals";
|
} from "@/components/trainScheduling/scheduleVisuals";
|
||||||
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
||||||
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
||||||
@@ -253,61 +264,65 @@ export default function TrainScheduleV2ListPage() {
|
|||||||
id: "actions",
|
id: "actions",
|
||||||
header: "Actions",
|
header: "Actions",
|
||||||
meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` },
|
meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` },
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<Group gap={6} justify="flex-end" wrap="nowrap">
|
const schedule = row.original;
|
||||||
<Button
|
return (
|
||||||
variant="light"
|
<Group justify="flex-end" onClick={(e) => e.stopPropagation()}>
|
||||||
color="green"
|
<Menu position="bottom-end" withinPortal shadow="md" width={190}>
|
||||||
size="compact-sm"
|
<Menu.Target>
|
||||||
rightSection={<ArrowRight size={14} />}
|
<ActionIcon variant="subtle" color="gray" aria-label="Row actions">
|
||||||
onClick={() =>
|
<MoreHorizontal size={16} />
|
||||||
navigate(`/dashboard/operations/train-scheduling-v2/${row.original.id}`)
|
</ActionIcon>
|
||||||
}
|
</Menu.Target>
|
||||||
>
|
<Menu.Dropdown>
|
||||||
Open
|
<Menu.Item
|
||||||
</Button>
|
leftSection={<Eye size={15} />}
|
||||||
{["DISPATCHED", "ARRIVED"].includes(row.original.status) ? (
|
onClick={() =>
|
||||||
<Button
|
navigate(`/dashboard/operations/train-scheduling-v2/${schedule.id}`)
|
||||||
variant="light"
|
}
|
||||||
color="teal"
|
>
|
||||||
size="compact-sm"
|
View detail
|
||||||
leftSection={<Navigation size={14} />}
|
</Menu.Item>
|
||||||
onClick={() =>
|
{["DISPATCHED", "ARRIVED"].includes(schedule.status) ? (
|
||||||
navigate(
|
<Menu.Item
|
||||||
`/dashboard/operations/train-scheduling-v2/${row.original.id}/track`,
|
leftSection={<Navigation size={15} />}
|
||||||
)
|
onClick={() =>
|
||||||
}
|
navigate(
|
||||||
>
|
`/dashboard/operations/train-scheduling-v2/${schedule.id}/track`,
|
||||||
Track
|
)
|
||||||
</Button>
|
}
|
||||||
) : null}
|
>
|
||||||
{["DRAFT", "SCHEDULED"].includes(row.original.status) ? (
|
Track
|
||||||
<Button
|
</Menu.Item>
|
||||||
variant="subtle"
|
) : null}
|
||||||
color="red"
|
{["DRAFT", "SCHEDULED"].includes(schedule.status) ? (
|
||||||
size="compact-sm"
|
<Menu.Item
|
||||||
loading={cancel.isPending}
|
color="red"
|
||||||
onClick={async () => {
|
leftSection={<Ban size={15} />}
|
||||||
try {
|
onClick={async () => {
|
||||||
await cancel.mutateAsync({
|
try {
|
||||||
id: row.original.id,
|
await cancel.mutateAsync({
|
||||||
freightType: row.original.freightType ?? "CONTAINER",
|
id: schedule.id,
|
||||||
});
|
freightType: schedule.freightType ?? "CONTAINER",
|
||||||
toast({ title: "Schedule cancelled" });
|
});
|
||||||
} catch (err) {
|
toast({ title: "Schedule cancelled" });
|
||||||
toast({
|
} catch (err) {
|
||||||
title: "Cancel failed",
|
toast({
|
||||||
description: parseError(err, "Could not cancel"),
|
title: "Cancel failed",
|
||||||
variant: "destructive",
|
description: parseError(err, "Could not cancel"),
|
||||||
});
|
variant: "destructive",
|
||||||
}
|
});
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
Cancel
|
>
|
||||||
</Button>
|
Cancel schedule
|
||||||
) : null}
|
</Menu.Item>
|
||||||
</Group>
|
) : null}
|
||||||
),
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [navigate, cancel.isPending, cancel, toast]);
|
}, [navigate, cancel.isPending, cancel, toast]);
|
||||||
@@ -340,65 +355,33 @@ export default function TrainScheduleV2ListPage() {
|
|||||||
: "success";
|
: "success";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<PageContainer>
|
||||||
<Group justify="flex-end">
|
<PageHeader
|
||||||
<Button
|
title="Train Schedules"
|
||||||
size="md"
|
subtitle="Operational train scheduling with full allocation workflow."
|
||||||
radius="lg"
|
action={
|
||||||
color="green"
|
<Button leftSection={<Train size={18} />} onClick={() => setCreateOpen(true)}>
|
||||||
leftSection={<Train size={18} />}
|
New schedule
|
||||||
onClick={() => setCreateOpen(true)}
|
</Button>
|
||||||
>
|
}
|
||||||
New schedule
|
/>
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="md">
|
<KpiStrip
|
||||||
<StatTile
|
items={[
|
||||||
icon={Train}
|
{ label: "Total trains", value: stats.total, icon: Train },
|
||||||
label="Total trains"
|
{ label: "Scheduled", value: stats.scheduled, icon: CalendarClock },
|
||||||
value={stats.total}
|
{ label: "Dispatched", value: stats.dispatched, icon: Send },
|
||||||
accent="#F2A516"
|
{ label: "Planned load", value: `${Math.round(stats.weight)}T`, icon: Weight },
|
||||||
graph="area"
|
]}
|
||||||
graphAccent="gold"
|
/>
|
||||||
/>
|
|
||||||
<StatTile
|
|
||||||
icon={CalendarClock}
|
|
||||||
label="Scheduled"
|
|
||||||
value={stats.scheduled}
|
|
||||||
accent="#FB8C2E"
|
|
||||||
graph="line"
|
|
||||||
graphAccent="orange"
|
|
||||||
graphPct={stats.total ? Math.round((stats.scheduled / stats.total) * 100) : 0}
|
|
||||||
/>
|
|
||||||
<StatTile
|
|
||||||
icon={Send}
|
|
||||||
label="Dispatched"
|
|
||||||
value={stats.dispatched}
|
|
||||||
accent="#F2A516"
|
|
||||||
graph="ring"
|
|
||||||
graphAccent="gold"
|
|
||||||
graphPct={stats.total ? Math.round((stats.dispatched / stats.total) * 100) : 0}
|
|
||||||
/>
|
|
||||||
<StatTile
|
|
||||||
icon={Weight}
|
|
||||||
label="Planned load"
|
|
||||||
value={`${Math.round(stats.weight)}T`}
|
|
||||||
accent="#FB8C2E"
|
|
||||||
graph="area"
|
|
||||||
graphAccent="orange"
|
|
||||||
/>
|
|
||||||
</SimpleGrid>
|
|
||||||
|
|
||||||
<Card radius="lg" padding={0} withBorder style={{ borderColor: "var(--mantine-color-gray-2)" }}>
|
<Card p={0}>
|
||||||
<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
|
||||||
search={search}
|
search={search}
|
||||||
onSearchChange={setSearch}
|
onSearchChange={setSearch}
|
||||||
searchPlaceholder="Search schedules…"
|
searchPlaceholder="Search schedules…"
|
||||||
addLabel="Create schedule"
|
|
||||||
onAdd={() => setCreateOpen(true)}
|
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
onViewModeChange={setViewMode}
|
onViewModeChange={setViewMode}
|
||||||
filters={
|
filters={
|
||||||
@@ -442,6 +425,17 @@ export default function TrainScheduleV2ListPage() {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={paged}
|
data={paged}
|
||||||
status={tableStatus}
|
status={tableStatus}
|
||||||
|
onRowClick={(schedule) =>
|
||||||
|
navigate(`/dashboard/operations/train-scheduling-v2/${schedule.id}`)
|
||||||
|
}
|
||||||
|
error={
|
||||||
|
schedulesQuery.isError
|
||||||
|
? {
|
||||||
|
message: "Failed to load train schedules.",
|
||||||
|
onRetry: () => void schedulesQuery.refetch(),
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
emptyMessage="No train schedules found"
|
emptyMessage="No train schedules found"
|
||||||
pagination={{
|
pagination={{
|
||||||
pageIndex: pagination.pageIndex,
|
pageIndex: pagination.pageIndex,
|
||||||
@@ -554,13 +548,13 @@ export default function TrainScheduleV2ListPage() {
|
|||||||
<Button variant="default" onClick={() => setCreateOpen(false)}>
|
<Button variant="default" onClick={() => setCreateOpen(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="green" loading={create.isPending} onClick={handleCreate}>
|
<Button loading={create.isPending} onClick={handleCreate}>
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Stack>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,31 +607,15 @@ function ScheduleCard({
|
|||||||
const canTrack = ["DISPATCHED", "ARRIVED"].includes(schedule.status);
|
const canTrack = ["DISPATCHED", "ARRIVED"].includes(schedule.status);
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
radius="lg"
|
|
||||||
padding={0}
|
padding={0}
|
||||||
withBorder
|
withBorder
|
||||||
onClick={onOpen}
|
onClick={onOpen}
|
||||||
style={{
|
className="cursor-pointer overflow-hidden transition-[transform,border-color] duration-150 hover:-translate-y-0.5 hover:border-edr-primary!"
|
||||||
cursor: "pointer",
|
|
||||||
overflow: "hidden",
|
|
||||||
borderColor: "var(--mantine-color-gray-2)",
|
|
||||||
transition: "box-shadow 150ms ease, transform 150ms ease",
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.boxShadow = scheduleBrand.shadowSm;
|
|
||||||
e.currentTarget.style.transform = "translateY(-2px)";
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.boxShadow = "";
|
|
||||||
e.currentTarget.style.transform = "";
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{/* accent strip */}
|
|
||||||
<Box style={{ height: 4, background: "linear-gradient(90deg, #FBD171, #F2A516)" }} />
|
|
||||||
<Stack gap="sm" p="md">
|
<Stack gap="sm" p="md">
|
||||||
<Group justify="space-between" align="flex-start" wrap="nowrap">
|
<Group justify="space-between" align="flex-start" wrap="nowrap">
|
||||||
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
|
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
|
||||||
<ThemeIcon size={38} radius="md" variant="light" color="#F2A516">
|
<ThemeIcon size={38} radius="md" variant="light" color="edr-green">
|
||||||
<Train size={18} />
|
<Train size={18} />
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Stack gap={0} style={{ minWidth: 0 }}>
|
<Stack gap={0} style={{ minWidth: 0 }}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button, Card, Group, NumberInput, Stack, Text, Title } from "@mantine/core";
|
import { Button, Card, Group, NumberInput, Stack } from "@mantine/core";
|
||||||
|
|
||||||
|
import { PageContainer, PageHeader } from "@/components/page";
|
||||||
import { trainSchedulingService } from "@/services/trainScheduling.service";
|
import { trainSchedulingService } from "@/services/trainScheduling.service";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import type { TrainSchedulingGlobalRules } from "@/types/trainScheduling";
|
import type { TrainSchedulingGlobalRules } from "@/types/trainScheduling";
|
||||||
@@ -44,15 +45,13 @@ export default function TrainSchedulingGlobalRulesPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg" maw={720}>
|
<PageContainer>
|
||||||
<Stack gap={4}>
|
<PageHeader
|
||||||
<Title order={3}>Train scheduling rules</Title>
|
title="Train scheduling rules"
|
||||||
<Text size="sm" c="dimmed">
|
subtitle="Global limits applied when previewing and assigning bookings to trains."
|
||||||
Global limits applied when previewing and assigning bookings to trains.
|
/>
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Card radius="xl" padding="lg" withBorder>
|
<Card maw={720}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Max train length (m)"
|
label="Max train length (m)"
|
||||||
@@ -110,12 +109,12 @@ export default function TrainSchedulingGlobalRulesPage() {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Button color="teal" loading={saving} disabled={loading} onClick={() => void handleSave()}>
|
<Button loading={saving} disabled={loading} onClick={() => void handleSave()}>
|
||||||
Save rules
|
Save rules
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
</Stack>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user