Files
edr-platform/apps/edr-freight-web/backoffice/src/components/trainScheduling/ScheduleHistoryPanel.tsx
Marshal e2189040fa feat: add pagination to schedule history and consolidation approvals
- 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.
2026-08-23 04:49:58 +00:00

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>
);
}