109 lines
4.3 KiB
TypeScript
109 lines
4.3 KiB
TypeScript
import { Building2, MapPin, Phone } from "lucide-react";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { EmptyState } from "@/components/ui/empty-state";
|
|
import { ErrorState } from "@/components/ui/error-state";
|
|
import { PageHeader } from "@/components/PageHeader";
|
|
import { ProviderDrawer } from "@/components/ProviderDrawer";
|
|
import { useProviderDrawerUrlState } from "@/hooks/useProviderDrawerUrlState";
|
|
import { useProviders } from "@/hooks/useProviders";
|
|
import { fmt } from "@/lib/format";
|
|
|
|
export function Providers() {
|
|
const { data, isLoading, isError, error, refetch } = useProviders();
|
|
const items = data?.items ?? [];
|
|
const { providerNpi, open, close } = useProviderDrawerUrlState();
|
|
|
|
return (
|
|
<div className="space-y-6 lg:space-y-8 animate-fade-in">
|
|
<PageHeader
|
|
eyebrow="Providers"
|
|
title={<>Provider <span className="display italic text-muted-foreground">directory</span></>}
|
|
subtitle="NPIs registered with the clearinghouse for 837P submission and 835 remittance retrieval."
|
|
/>
|
|
|
|
{isError ? (
|
|
<ErrorState
|
|
message="Couldn't load providers from the backend."
|
|
detail={error instanceof Error ? error.message : String(error)}
|
|
onRetry={() => refetch()}
|
|
/>
|
|
) : null}
|
|
|
|
{isLoading ? (
|
|
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
<Skeleton key={i} variant="default" height={180} />
|
|
))}
|
|
</div>
|
|
) : items.length === 0 ? (
|
|
<div className="surface rounded-xl">
|
|
<EmptyState
|
|
eyebrow="Providers · directory empty"
|
|
message="Providers appear here as soon as an 837P file is parsed."
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-3.5 md:grid-cols-2 xl:grid-cols-3">
|
|
{items.map((p) => (
|
|
<article
|
|
key={p.npi}
|
|
onClick={() => open(p.npi)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") open(p.npi);
|
|
}}
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-label={`View provider ${p.name}`}
|
|
className="group drillable surface-2 rounded-xl p-5 flex flex-col gap-4 transition-colors hover:bg-muted/20 cursor-pointer"
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div className="h-10 w-10 rounded-md bg-muted/60 ring-1 ring-inset ring-border/40 flex items-center justify-center text-foreground">
|
|
<Building2 className="h-4 w-4" strokeWidth={1.5} />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<h3 className="display text-[15px] text-foreground tracking-tight">
|
|
{p.name}
|
|
</h3>
|
|
<p className="mono text-[10.5px] text-muted-foreground mt-0.5">
|
|
NPI {p.npi} · TIN {p.taxId}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5 text-[12.5px] text-muted-foreground">
|
|
<div className="flex items-start gap-2">
|
|
<MapPin className="h-3.5 w-3.5 mt-0.5 shrink-0" strokeWidth={1.75} />
|
|
<span>
|
|
{p.address}, {p.city}, {p.state} {p.zip}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Phone className="h-3.5 w-3.5" strokeWidth={1.75} />
|
|
<span className="mono">{p.phone}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-border/30">
|
|
<div>
|
|
<div className="eyebrow">Claims</div>
|
|
<div className="display mono text-[20px] leading-none mt-2">
|
|
{fmt.num(p.claimCount)}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="eyebrow">Outstanding AR</div>
|
|
<div className="display mono text-[20px] leading-none mt-2">
|
|
{fmt.usd(p.outstandingAr)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<ProviderDrawer npi={providerNpi} onClose={close} />
|
|
</div>
|
|
);
|
|
}
|