This commit is contained in:
Marshal
2026-08-19 08:41:51 +00:00
parent f4a654f273
commit 13f66dfb66
15 changed files with 249 additions and 16 deletions

View File

@@ -6,11 +6,13 @@ import {
type DraggableStateSnapshot,
type DropResult,
} from "@hello-pangea/dnd";
import { ActionIcon, Badge, Box, Group, Stack, Text, Tooltip } from "@mantine/core";
import { ActionIcon, Badge, Box, Group, Menu, Stack, Text, Tooltip } from "@mantine/core";
import { useQuery } from "@tanstack/react-query";
import { GripVertical, MapPin, Trash2, Wrench } from "lucide-react";
import { memo, useCallback, useMemo, type ReactNode } from "react";
import { createPortal } from "react-dom";
import { api } from "@/services/api";
import type { TrainCompositionWagon } from "@/services/trainBuilder.service";
import { wagonTypeColor } from "./trainStatus";
@@ -38,6 +40,7 @@ function ConsistWagonList({
onReorder,
onRemove,
onMaintenance,
onChangeYard,
busy = false,
}: ConsistWagonListProps) {
const onDragEnd = useCallback((result: DropResult) => {
@@ -107,6 +110,7 @@ function ConsistWagonList({
busy={busy}
onRemove={onRemove}
onMaintenance={onMaintenance}
onChangeYard={onChangeYard}
/>
)}
</Draggable>
@@ -129,9 +133,68 @@ export interface ConsistWagonListProps {
onRemove: (wagonId: string) => void;
/** Detach the wagon and move it to MAINTENANCE status (page confirms first). */
onMaintenance: (wagon: TrainCompositionWagon) => void;
/** Move one wagon to another yard from its yard badge; absent = read-only badge. */
onChangeYard?: (wagonId: string, currentYardId: string) => void;
busy?: boolean;
}
/** Yard badge that opens a yard picker when `onChange` is provided. */
function WagonYardBadge({
wagon,
busy,
onChange,
}: {
wagon: TrainCompositionWagon;
busy: boolean;
onChange?: (wagonId: string, currentYardId: string) => void;
}) {
const label = wagon.currentYard?.label ?? wagon.currentYard?.code ?? "No yard";
const yardsQuery = useQuery(
api.routes.yards.queryOptions({ staleTime: 5 * 60_000, enabled: Boolean(onChange) }),
);
if (!onChange) {
return wagon.currentYard ? (
<Badge variant="outline" color="gray" size="xs" radius="sm" leftSection={<MapPin size={10} />}>
{label}
</Badge>
) : null;
}
return (
<Menu shadow="md" width={240} withinPortal>
<Menu.Target>
<Badge
component="button"
type="button"
variant="outline"
color="blue"
size="xs"
radius="sm"
leftSection={<MapPin size={10} />}
disabled={busy}
style={{ cursor: busy ? "default" : "pointer" }}
// Stop the drag handle from swallowing the click.
onMouseDown={(e) => e.stopPropagation()}
aria-label={`Change yard of wagon ${wagon.wagonNumber}`}
>
{label}
</Badge>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Move wagon to yard</Menu.Label>
{(yardsQuery.data ?? []).map((y) => (
<Menu.Item
key={y.id}
disabled={y.id === wagon.currentYard?.id}
onClick={() => onChange(wagon.id, y.id)}
>
{y.label ?? y.code}
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
);
}
const WagonRow = memo(function WagonRow({
wagon,
index,
@@ -141,6 +204,7 @@ const WagonRow = memo(function WagonRow({
busy,
onRemove,
onMaintenance,
onChangeYard,
}: {
wagon: TrainCompositionWagon;
index: number;
@@ -150,6 +214,7 @@ const WagonRow = memo(function WagonRow({
busy: boolean;
onRemove: (wagonId: string) => void;
onMaintenance: (wagon: TrainCompositionWagon) => void;
onChangeYard?: (wagonId: string, currentYardId: string) => void;
}) {
const color = wagonTypeColor(wagon.wagonType?.code);
@@ -195,11 +260,7 @@ const WagonRow = memo(function WagonRow({
{wagon.wagonType.code}
</Badge>
) : null}
{wagon.currentYard ? (
<Badge variant="outline" color="gray" size="xs" radius="sm" leftSection={<MapPin size={10} />}>
{wagon.currentYard.label ?? wagon.currentYard.code}
</Badge>
) : null}
<WagonYardBadge wagon={wagon} busy={busy} onChange={onChangeYard} />
</Group>
<Text size="xs" c="dimmed" truncate>
{wagon.wagonType