import { Badge, Button, Card, Group, SegmentedControl, Skeleton, Stack, Switch, Text, ThemeIcon, Timeline, Tooltip, } from "@mantine/core"; import { useQuery } from "@tanstack/react-query"; import { ArrowRight, ChevronLeft, ChevronRight, History, Inbox, Truck, } from "lucide-react"; import { useState } from "react"; import { useAuth } from "@/auth/useAuth"; import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions"; import { api } from "@/services/api"; import type { WagonMovementRecord, WagonTransferRequest, } from "@/services/wagon.service"; import { STATUS_META, TransferProgress, TransferStatusBadge, stripHtmlToText, wagonTypeLabel, yardLabel, } from "./wagon-transfer-ui"; const fmtTime = (iso?: string | null) => iso ? new Date(iso).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", hour12: false, }) : "—"; /** "Today" / "Yesterday" / "Mon 12 Jul 2026" — the header of one timeline block. */ const dayLabel = (iso: string) => { const d = new Date(iso); const days = Math.round( (new Date().setHours(0, 0, 0, 0) - new Date(iso).setHours(0, 0, 0, 0)) / 86_400_000, ); if (days === 0) return "Today"; if (days === 1) return "Yesterday"; return d.toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", year: "numeric", }); }; /** Bucket an already-DESC-sorted list into day blocks, order preserved. */ function groupByDay(items: T[], at: (item: T) => string) { const groups: Array<{ key: string; label: string; items: T[] }> = []; for (const item of items) { const iso = at(item); const key = new Date(iso).toDateString(); const last = groups[groups.length - 1]; if (last?.key === key) last.items.push(item); else groups.push({ key, label: dayLabel(iso), items: [item] }); } return groups; } const MOVEMENT_KIND_LABEL: Record = { LOADED: "Carried cargo", EMPTY_REPOSITION: "Repositioned empty", MANUAL: "Manual move", }; function EmptyState({ label }: { label: string }) { return ( {label} ); } function RequestItem({ request }: { request: WagonTransferRequest }) { const meta = STATUS_META[request.status]; return ( } color={meta?.color ?? "gray"} lineVariant="dotted" > {yardLabel(request.fromYard)} {yardLabel(request.toYard)} {wagonTypeLabel(request.wagonType)} {stripHtmlToText(request.reason) ? ( {stripHtmlToText(request.reason)} ) : null} {fmtTime(request.createdAt)} ); } function MovementItem({ movement }: { movement: WagonMovementRecord }) { return ( } color={movement.transferRequestId ? "edr-green" : "gray"} lineVariant="dotted" > {movement.wagon?.wagonNumber ?? "Wagon"} {yardLabel(movement.fromYard)} {yardLabel(movement.toYard)} {movement.transferRequestId ? ( Transfer ) : ( {MOVEMENT_KIND_LABEL[movement.kind] ?? movement.kind} )} {fmtTime(movement.occurredAt)} ); } /** * Who moved what. A staffer sees their own activity; holders of * `transfer_history_all` can widen it to every staffer (the backend enforces * the scope regardless of the toggle). */ export default function TransferHistoryPanel() { const { user } = useAuth(); const canSeeAll = hasPermission(user, FREIGHT_PERMS.wagons.transferHistoryAll); const [allStaff, setAllStaff] = useState(false); const [view, setView] = useState<"requests" | "movements">("requests"); const [page, setPage] = useState(1); const scopeAll = canSeeAll && allStaff; const mine = useQuery({ ...api.wagonTransferRequests.history.queryOptions({ input: { page, pageSize: 20 }, }), enabled: !scopeAll, }); const all = useQuery({ ...api.wagonTransferRequests.historyAll.queryOptions({ input: { page, pageSize: 20 }, }), enabled: scopeAll, }); const source = scopeAll ? all : mine; const requests = source.data?.requests ?? []; const movements = source.data?.movements ?? []; const meta = source.data?.meta; const showingRequests = view === "requests"; const total = showingRequests ? (meta?.requestsTotal ?? 0) : (meta?.movementsTotal ?? 0); // Each list pages independently on the server; the pager follows the one on screen. const pageSize = meta?.pageSize ?? 20; const totalPages = Math.max(1, Math.ceil(total / pageSize)); const groups = showingRequests ? groupByDay(requests, (r) => r.createdAt) : groupByDay(movements, (m) => m.occurredAt); return ( Transfer history {scopeAll ? "Every staffer's requests and wagon moves" : "Requests you filed or fulfilled, and the wagons you moved"} { setView(v as "requests" | "movements"); setPage(1); }} data={[ { value: "requests", label: `Requests ${meta?.requestsTotal ?? 0}`, }, { value: "movements", label: `Wagons moved ${meta?.movementsTotal ?? 0}`, }, ]} /> {canSeeAll ? ( { setAllStaff(e.currentTarget.checked); setPage(1); }} /> ) : null} {source.isLoading ? ( {[0, 1, 2, 3].map((i) => ( ))} ) : groups.length === 0 ? ( ) : ( {groups.map((group) => ( {group.label} · {group.items.length} {showingRequests ? (group.items as WagonTransferRequest[]).map((r) => ( )) : (group.items as WagonMovementRecord[]).map((m) => ( ))} ))} )} {total} {showingRequests ? "request(s)" : "move(s)"} · page{" "} {meta?.page ?? page} of {totalPages} ); }