import { Text, Tooltip, UnstyledButton } from "@mantine/core"; import { useQuery } from "@tanstack/react-query"; import { AlertTriangle, ChevronRight } from "lucide-react"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useAuth } from "@/auth/useAuth"; import { canSeeDocReviewAlert } from "@/lib/permissions"; import { api } from "@/services/api"; /** * Booking-request statuses that count as "nobody decided yet". These are the * exact statuses the window engine expires when document review ends * (findUnacceptedForRouteDay), so the deep-linked list shows precisely the * requests the countdown is warning about. */ const UNDECIDED_STATUSES = [ "OPERATION_REQUESTED", "OPERATION_REQUEST_PENDING", "OPERATION_CHANGES_REQUESTED", "OPERATION_PRICE_PENDING_CONFIRM", ].join(","); const pendingRequestsHref = (tradeDirection: string) => `/dashboard/booking-requests?statuses=${UNDECIDED_STATUSES}&tradeDirection=${tradeDirection}`; /** mm:ss (or h:mm:ss past an hour), fixed width so the pill never jitters. */ function formatRemaining(ms: number): string { const total = Math.max(0, Math.floor(ms / 1000)); const hours = Math.floor(total / 3600); const minutes = Math.floor((total % 3600) / 60); const seconds = total % 60; const mm = String(minutes).padStart(2, "0"); const ss = String(seconds).padStart(2, "0"); return hours > 0 ? `${hours}:${mm}:${ss}` : `${mm}:${ss}`; } /** * Header alarm for the document-review deadline. Runs for the whole review * phase — from the moment it opens until the clock runs out — whenever requests * are still undecided. Everything left pending at the deadline is expired * automatically, so staff get the full window to accept or reject rather than * only its back half. Clicking opens the booking requests already filtered to * those undecided requests. */ export default function DocReviewAlertButton() { const navigate = useNavigate(); const { user } = useAuth(); const { data: alert } = useQuery({ ...api.trainScheduling.docReviewAlert.queryOptions(), // Dedicated permission: only the position types granted it are alarmed. enabled: canSeeDocReviewAlert(user), // The window engine ticks every 10s; a minute is close enough for a header // chip — the countdown itself runs locally. refetchInterval: 60_000, }); const deadlineMs = alert ? new Date(alert.docReviewEndsAt).getTime() : 0; const [remaining, setRemaining] = useState(() => deadlineMs - Date.now()); useEffect(() => { if (!deadlineMs) return; const tick = () => setRemaining(deadlineMs - Date.now()); tick(); const id = window.setInterval(tick, 1000); return () => window.clearInterval(id); }, [deadlineMs]); if (!alert) return null; // Alarm for the WHOLE review phase, from the moment it opens: anything still // undecided when the clock runs out is expired automatically, so staff need // the full window to act, not the back half of it. The endpoint only returns // a schedule that is in DOC_REVIEW with undecided requests behind it, so the // remaining check just hides the pill once the deadline passes. if (remaining <= 0) return null; const requestLabel = alert.pendingCount === 1 ? "request" : "requests"; return ( navigate(pendingRequestsHref(alert.tradeDirection))} aria-label={`${alert.pendingCount} import booking ${requestLabel} awaiting a decision — document review ends in ${formatRemaining(remaining)}`} className="group flex h-9 shrink-0 items-center gap-2 rounded-full pl-2.5 pr-2 transition-transform hover:scale-[1.02]" // Inline, not Tailwind: Mantine's UnstyledButton resets the background // in unlayered CSS, which beats a `@layer utilities` class whatever its // specificity — `bg-red-600` alone renders the pill white. style={{ background: "var(--mantine-color-red-6)", border: "1px solid var(--mantine-color-red-7)", color: "#fff", boxShadow: "0 2px 10px rgba(220, 38, 38, 0.35)", }} > {/* Live dot: a ping ring behind a solid core, so the pill reads as active without animating the whole chip. */} {alert.pendingCount} undecided {formatRemaining(remaining)} ); }