import { useMemo } from "react"; import { Badge, Box, Card, Group, ScrollArea, Skeleton, Stack, Text } from "@mantine/core"; import { useQuery } from "@tanstack/react-query"; import { ArrowRight, CalendarClock } from "lucide-react"; import { CountdownTimer } from "@edr/ui-common"; import { api } from "@/services/api"; import type { BatchBoardSchedule } from "@/types/trainScheduling"; /** All window times are communicated in East Africa Time. */ const TZ = "Africa/Addis_Ababa"; function fmtDay(iso: string): string { return new Date(iso).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", timeZone: TZ, }); } function fmtTime(iso: string): string { return new Date(iso).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", hour12: false, timeZone: TZ, }); } function windowLabel(w: BatchBoardSchedule): string { if (w.windowOpensAt && w.windowClosesAt) { return `${fmtDay(w.windowOpensAt)} · ${fmtTime(w.windowOpensAt)} – ${fmtTime( w.windowClosesAt, )} EAT`; } if (w.windowOpensAt) { return `Opens ${fmtDay(w.windowOpensAt)} · ${fmtTime(w.windowOpensAt)} EAT`; } return (w.windowPhase ?? w.bookingWindowStatus).replace(/_/g, " "); } /** * The countdown for whichever phase the window is currently in, mirroring the * customer portal. Phases run pre-window (opens at windowOpensAt) → open (closes * at windowClosesAt) → document review (docReviewEndsAt) → payment * (paymentPhaseEndsAt). `expiredText` names the NEXT step so a deadline that * lapses between the 60s refetches announces what comes next rather than the * bare word "Expired". Returns null when no phase is timing down. */ function phaseCountdown( w: BatchBoardSchedule, ): { label: string; deadline: string; expiredText: string } | null { switch (w.windowPhase) { case "PRE_WINDOW": return w.windowOpensAt ? { label: "Booking opens in", deadline: w.windowOpensAt, expiredText: "Booking opening now…", } : null; case "OPEN": return w.windowClosesAt ? { label: "Window closes in", deadline: w.windowClosesAt, expiredText: "Document review starting…", } : null; case "DOC_REVIEW": return w.docReviewEndsAt ? { label: "Document review ends in", deadline: w.docReviewEndsAt, expiredText: "Payment starting…", } : null; case "PAYMENT": return w.paymentPhaseEndsAt ? { label: "Payment window ends in", deadline: w.paymentPhaseEndsAt, expiredText: "Payment window closing…", } : null; default: return null; } } function isOpenNow(w: BatchBoardSchedule): boolean { return w.windowPhase === "OPEN" && w.bookingWindowStatus === "OPEN"; } /** Drop windows whose booking window (or the train itself) has already passed. */ function isPast(w: BatchBoardSchedule): boolean { const now = Date.now(); const closes = w.windowClosesAt ? new Date(w.windowClosesAt).getTime() : null; const departs = w.scheduleDate ? new Date(w.scheduleDate).getTime() : null; // Still live while in a post-close staff phase (doc review / payment). if (w.windowPhase === "DOC_REVIEW" || w.windowPhase === "PAYMENT") return false; if (departs != null && departs <= now) return true; if (closes != null && closes <= now) return true; return false; } /** * Upcoming / open import booking windows across all train schedules, shown to GL * ET on the clearance queue so they can see which lanes are accepting bookings * (mirrors the customer's portal "Booking Windows" card). Hidden when nothing is * pending. Windows already past close/departure are dropped. */ export function GlUpcomingWindowsSection() { const { data, isLoading } = useQuery( api.trainScheduling.batchBoard.queryOptions({ refetchInterval: 60_000 }), ); const windows = useMemo(() => { const rows = (data ?? []).filter( (w) => w.windowPhase != null && w.windowPhase !== "DONE" && !isPast(w), ); // Open lanes first, then by opening time. return rows.sort((a, b) => { const openDiff = Number(isOpenNow(b)) - Number(isOpenNow(a)); if (openDiff !== 0) return openDiff; const at = a.windowOpensAt ? new Date(a.windowOpensAt).getTime() : Infinity; const bt = b.windowOpensAt ? new Date(b.windowOpensAt).getTime() : Infinity; return at - bt; }); }, [data]); if (!isLoading && windows.length === 0) return null; return ( Booking windows Upcoming and open import booking windows across all lanes (EAT) {isLoading ? ( {[1, 2].map((i) => ( ))} ) : ( {windows.map((w) => { const open = isOpenNow(w); const cd = phaseCountdown(w); return ( {w.origin ?? "—"} {w.destination ?? "—"} {w.trainNumber ? ( · {w.trainNumber} ) : null} {windowLabel(w)} {w.scheduleDate ? ` · Departs ${fmtDay(w.scheduleDate)}` : ""} {cd ? ( ) : null} {w.direction ? ( {w.direction === "IMPORT" ? "Import" : "Export"} ) : null} {open ? "Open now" : w.windowPhase === "PRE_WINDOW" && w.windowOpensAt ? `Opens ${fmtTime(w.windowOpensAt)} EAT` : (w.windowPhase ?? w.bookingWindowStatus).replace( /_/g, " ", )} ); })} )} ); }