mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
- Implemented pagination in ScheduleHistoryPanel to manage large history entries. - Updated API to support pagination parameters for schedule history. - Enhanced ConsolidationApprovalsPage with tabbed navigation and pagination for approval rows. - Introduced new types for paginated responses in bookings and train scheduling services. - Added a database migration to create an index on wagon_booking_allocations for performance improvements.
154 lines
5.1 KiB
TypeScript
154 lines
5.1 KiB
TypeScript
import {
|
|
Badge,
|
|
Group,
|
|
Pagination,
|
|
Paper,
|
|
Stack,
|
|
Text,
|
|
ThemeIcon,
|
|
Timeline,
|
|
} from "@mantine/core";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useState } from "react";
|
|
import {
|
|
ArrowLeftRight,
|
|
History,
|
|
MapPin,
|
|
Minus,
|
|
PackageCheck,
|
|
PackageMinus,
|
|
PackageOpen,
|
|
Plus,
|
|
User,
|
|
} from "lucide-react";
|
|
|
|
import { api } from "@/services/api";
|
|
import type { ScheduleHistoryEntry } from "@/services/trainBuilder.service";
|
|
|
|
const ACTION_META: Record<
|
|
ScheduleHistoryEntry["action"],
|
|
{ label: string; color: string; icon: typeof Plus }
|
|
> = {
|
|
ADD: { label: "Wagon coupled", color: "edr-green", icon: Plus },
|
|
REMOVE: { label: "Wagon trimmed", color: "red", icon: Minus },
|
|
SWITCH: { label: "Wagon switched", color: "blue", icon: ArrowLeftRight },
|
|
BOOKING_REMOVED: { label: "Booking removed", color: "orange", icon: PackageMinus },
|
|
BOOKING_LOADED: { label: "Booking loaded", color: "edr-green", icon: PackageCheck },
|
|
BOOKING_UNLOADED: { label: "Booking unloaded", color: "blue", icon: PackageOpen },
|
|
};
|
|
|
|
/**
|
|
* "History" tab: every change made to the train after it was scheduled —
|
|
* wagons coupled/trimmed/switched (with the stop where it happened) and
|
|
* bookings removed from the composition — newest first.
|
|
*/
|
|
export default function ScheduleHistoryPanel({ scheduleId }: { scheduleId: string }) {
|
|
const [page, setPage] = useState(1);
|
|
const historyQuery = useQuery(
|
|
api.trainScheduling.scheduleHistory.queryOptions({
|
|
input: { scheduleId, page, pageSize: 20 },
|
|
enabled: Boolean(scheduleId),
|
|
// 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">
|
|
<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">
|
|
Change history
|
|
</Text>
|
|
<Text size="sm" c="dimmed">
|
|
Wagons coupled, trimmed or switched, bookings loaded/unloaded per
|
|
yard, and bookings removed — after this train was scheduled,
|
|
newest first.
|
|
</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 changes recorded yet — the consist and composition are as
|
|
scheduled.
|
|
</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.kind}-${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}
|
|
</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.note ? (
|
|
<Text size="xs" c="dimmed" mt={2} fs="italic">
|
|
{entry.note}
|
|
</Text>
|
|
) : 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>
|
|
);
|
|
}
|