mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 03:40:56 +00:00
- Added a reason field to train history entries for detach/maintenance actions. - Updated TrainHistoryPanel to display the reason for wagon detachments. - Introduced per-wagon load/unload functionality in ScheduleWorkspacePanel with a modal for managing individual wagons. - Implemented API endpoints for loading and unloading specific wagons, including the ability to cancel remaining wagons with a reason. - Refactored detach request handling in TrainBuilderDetailPage to streamline the process and remove the approval flow, requiring a reason for detachments. - Updated types and services to support new wagon loading/unloading features and booking wagon retrieval.
162 lines
5.7 KiB
TypeScript
162 lines
5.7 KiB
TypeScript
import { Badge, Group, Pagination, Paper, Stack, Text, ThemeIcon, Timeline } from "@mantine/core";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import {
|
|
ArrowLeftRight,
|
|
History,
|
|
MapPin,
|
|
MessageSquare,
|
|
Minus,
|
|
Plus,
|
|
TrainFront,
|
|
User,
|
|
} from "lucide-react";
|
|
import { useState } from "react";
|
|
|
|
import { api } from "@/services/api";
|
|
import type { TrainHistoryEntry } from "@/services/trainBuilder.service";
|
|
|
|
const PAGE_SIZE = 20;
|
|
|
|
const ACTION_META: Record<
|
|
TrainHistoryEntry["action"],
|
|
{ label: string; color: string; icon: typeof Plus }
|
|
> = {
|
|
ADD: { label: "Wagon attached", color: "edr-green", icon: Plus },
|
|
REMOVE: { label: "Wagon detached", color: "red", icon: Minus },
|
|
SWITCH: { label: "Wagon switched", color: "blue", icon: ArrowLeftRight },
|
|
};
|
|
|
|
/**
|
|
* "History" tab of the train-builder detail page: every wagon ever attached,
|
|
* detached or switched on this built train — builder edits and trip events
|
|
* (real cuts, mid-route couples, consist adjustments) alike, newest first.
|
|
*/
|
|
export default function TrainHistoryPanel({ trainId }: { trainId: string }) {
|
|
const [page, setPage] = useState(1);
|
|
const historyQuery = useQuery(
|
|
api.trainBuilder.history.queryOptions({
|
|
input: { id: trainId, page, pageSize: PAGE_SIZE },
|
|
enabled: Boolean(trainId),
|
|
// Keep the previous page on screen while the next one loads.
|
|
placeholderData: (prev) => prev,
|
|
}),
|
|
);
|
|
const entries = historyQuery.data?.items ?? [];
|
|
const totalPages = Math.max(1, historyQuery.data?.meta.totalPages ?? 1);
|
|
const total = historyQuery.data?.meta.total ?? 0;
|
|
|
|
return (
|
|
<Paper radius="xl" p="lg" withBorder>
|
|
<Stack gap="lg">
|
|
<Group gap="md" align="center" wrap="nowrap">
|
|
<ThemeIcon size={44} radius="md" variant="light" color="edr-green">
|
|
<History size={22} />
|
|
</ThemeIcon>
|
|
<Stack gap={2}>
|
|
<Text fw={700} fz="lg">
|
|
Wagon history
|
|
</Text>
|
|
<Text size="sm" c="dimmed">
|
|
Who attached, detached or switched which wagon on this train — from
|
|
the builder and from its trips — newest first, with the reason
|
|
given for detaching off a scheduled run.
|
|
</Text>
|
|
</Stack>
|
|
</Group>
|
|
|
|
{historyQuery.isLoading ? (
|
|
<Text size="sm" c="dimmed" ta="center" py="md">
|
|
Loading history…
|
|
</Text>
|
|
) : entries.length === 0 ? (
|
|
<Text size="sm" c="dimmed" ta="center" py="md">
|
|
No wagon changes recorded yet for this train.
|
|
</Text>
|
|
) : (
|
|
<Timeline bulletSize={26} lineWidth={2} color="edr-green">
|
|
{entries.map((entry) => {
|
|
const meta = ACTION_META[entry.action] ?? ACTION_META.ADD;
|
|
const Icon = meta.icon;
|
|
return (
|
|
<Timeline.Item
|
|
key={entry.id}
|
|
bullet={<Icon size={13} />}
|
|
color={meta.color}
|
|
title={
|
|
<Group gap="xs" wrap="nowrap">
|
|
<Badge size="sm" variant="light" color={meta.color}>
|
|
{meta.label}
|
|
</Badge>
|
|
{entry.subject ? (
|
|
<Text size="sm" fw={600} ff="monospace">
|
|
{entry.subject}
|
|
</Text>
|
|
) : null}
|
|
{entry.scheduleReference ? (
|
|
<Badge
|
|
size="sm"
|
|
variant="light"
|
|
color="blue"
|
|
leftSection={<TrainFront size={10} />}
|
|
>
|
|
{entry.scheduleReference}
|
|
</Badge>
|
|
) : (
|
|
<Badge size="sm" variant="light" color="gray">
|
|
Builder
|
|
</Badge>
|
|
)}
|
|
</Group>
|
|
}
|
|
>
|
|
<Group gap="md" mt={2}>
|
|
<Text size="xs" c="dimmed">
|
|
{new Date(entry.occurredAt).toLocaleString()}
|
|
</Text>
|
|
{entry.yardLabel ? (
|
|
<Group gap={4} wrap="nowrap">
|
|
<MapPin size={12} />
|
|
<Text size="xs" c="dimmed">
|
|
at {entry.yardLabel}
|
|
</Text>
|
|
</Group>
|
|
) : null}
|
|
{entry.actor ? (
|
|
<Group gap={4} wrap="nowrap">
|
|
<User size={12} />
|
|
<Text size="xs" c="dimmed">
|
|
{entry.actor}
|
|
</Text>
|
|
</Group>
|
|
) : null}
|
|
</Group>
|
|
{entry.reason ? (
|
|
<Group gap={4} wrap="nowrap" align="flex-start" mt={4}>
|
|
<MessageSquare
|
|
size={12}
|
|
style={{ flexShrink: 0, marginTop: 3 }}
|
|
/>
|
|
<Text size="xs" c="dimmed" style={{ fontStyle: "italic" }}>
|
|
{entry.reason}
|
|
</Text>
|
|
</Group>
|
|
) : null}
|
|
</Timeline.Item>
|
|
);
|
|
})}
|
|
</Timeline>
|
|
)}
|
|
|
|
{totalPages > 1 ? (
|
|
<Group justify="space-between">
|
|
<Text size="xs" c="dimmed">
|
|
{total} change(s)
|
|
</Text>
|
|
<Pagination size="sm" value={page} onChange={setPage} total={totalPages} />
|
|
</Group>
|
|
) : null}
|
|
</Stack>
|
|
</Paper>
|
|
);
|
|
}
|