import { useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { AlertCircle, Banknote, CircleDollarSign, Clock, Inbox, Receipt, TrendingDown, } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { PageHeader } from "@/components/PageHeader"; import { KpiCard } from "@/components/KpiCard"; import { ActivityFeed } from "@/components/ActivityFeed"; import { AnimatedNumber } from "@/components/AnimatedNumber"; import { DrillableCell } from "@/components/drill/DrillableCell"; import { fmt } from "@/lib/format"; import { eventKindToUrl } from "@/lib/event-routing"; import { useAuth } from "@/auth/useAuth"; import { useClaims } from "@/hooks/useClaims"; import { useProviders } from "@/hooks/useProviders"; import { useActivity } from "@/hooks/useActivity"; import type { Claim } from "@/types"; import { toast } from "sonner"; const MONTHS_BACK = 6; function buildMonthly(claims: Claim[]) { const now = new Date(); const months: { key: string; label: string; count: number; billed: number; received: number; denied: number; }[] = []; for (let i = MONTHS_BACK - 1; i >= 0; i--) { const d = new Date(now.getFullYear(), now.getMonth() - i, 1); months.push({ key: `${d.getFullYear()}-${d.getMonth()}`, label: d.toLocaleString("en-US", { month: "short" }), count: 0, billed: 0, received: 0, denied: 0, }); } const index = new Map(months.map((m, i) => [m.key, i])); for (const c of claims) { const d = new Date(c.submissionDate); const k = `${d.getFullYear()}-${d.getMonth()}`; const i = index.get(k); if (i === undefined) continue; months[i]!.count += 1; months[i]!.billed += c.billedAmount; months[i]!.received += c.receivedAmount; if (c.status === "denied") months[i]!.denied += 1; } let running = 0; const ar: number[] = []; for (const m of months) { running += m.billed - m.received; ar.push(Math.max(0, running)); } return { count: months.map((m) => m.count), billed: months.map((m) => m.billed), received: months.map((m) => m.received), ar, denialRate: months.map((m) => (m.count ? (m.denied / m.count) * 100 : 0)), }; } export function Dashboard() { // Live data: hooks fetch from /api/* when api.isConfigured; otherwise // they fall back to the in-memory store. Pulling from the hooks (not // the store directly) is what wires the Dashboard to the backend. const claimsQuery = useClaims({ limit: 100 }); const providersQuery = useProviders(); const activityQuery = useActivity({ limit: 10 }); const claims = claimsQuery.data?.items ?? []; const providers = providersQuery.data?.items ?? []; const activity = activityQuery.data?.items ?? []; const navigate = useNavigate(); // Time-of-day greeting + live operator name from the auth context. // Falls back to a neutral "there" while /api/auth/me is still in // flight so we never flash "undefined" at the operator. const auth = useAuth(); const greetingPart = (() => { const h = new Date().getHours(); return h < 12 ? "morning" : h < 18 ? "afternoon" : "evening"; })(); const operatorName = auth.user?.username ?? "there"; const kpis = useMemo(() => { const billed = claims.reduce((s, c) => s + c.billedAmount, 0); const received = claims.reduce((s, c) => s + c.receivedAmount, 0); const outstandingAr = billed - received; const denied = claims.filter((c) => c.status === "denied").length; const denialRate = claims.length > 0 ? (denied / claims.length) * 100 : 0; const pending = claims.filter( (c) => c.status === "submitted" || c.status === "pending" ).length; return { count: claims.length, billed, received, outstandingAr, denialRate, pending, }; }, [claims]); const monthly = useMemo(() => buildMonthly(claims), [claims]); const topProviders = useMemo( () => [...providers].sort((a, b) => b.claimCount - a.claimCount).slice(0, 4), [providers] ); const topDenials = useMemo( () => claims.filter((c) => c.status === "denied").slice(0, 5), [claims] ); // Stagger choreography — the hero lands first, then the KPIs in // a left-to-right wave, then the supporting cards. Total // choreography fits under 700ms. const heroDelay = 0; const kpiBase = 120; const kpiStep = 60; const sectionBase = kpiBase + kpiStep * 5 + 80; return (
Newest submissions, denials, and remittances across all providers.
Ranked by claim volume, last 6 months.
Investigate and resubmit where appropriate.