Healthcare

Claims Status

An insurance-claims table where each row carries a four-stage pipeline from submitted to paid, with denial states and patient-responsibility amounts in tabular figures.

Install

npx shadcn@latest add @paragon/claims-status

claims-status.tsx

"use client";

import * as React from "react";
import { cn } from "@/lib/utils";

const STAGES = ["Submitted", "Received", "Adjudicated", "Paid"] as const;
export type ClaimStage = (typeof STAGES)[number];

export interface Claim {
  id: string;
  /** e.g. "Office visit — 99213". */
  service: string;
  provider: string;
  date: string;
  billed: number;
  /** Patient responsibility, in dollars. */
  responsibility?: number;
  /** Current stage; earlier stages render complete. */
  stage: ClaimStage;
  /** Marks the claim as denied at its current stage. */
  denied?: boolean;
}

export interface ClaimsStatusProps extends React.ComponentProps<"div"> {
  claims?: Claim[];
}

const usd = (n: number) =>
  n.toLocaleString("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 });

const DEFAULT_CLAIMS: Claim[] = [
  { id: "CLM-90412", service: "Office visit — 99214", provider: "Dr. Priya Nair", date: "Jun 12", billed: 245, responsibility: 35, stage: "Paid" },
  { id: "CLM-90455", service: "Lab panel — 80053", provider: "Quest Diagnostics", date: "Jun 14", billed: 118, responsibility: 0, stage: "Adjudicated" },
  { id: "CLM-90478", service: "X-ray, chest — 71046", provider: "Metro Imaging", date: "Jun 16", billed: 320, stage: "Received" },
  { id: "CLM-90501", service: "MRI, knee — 73721", provider: "Metro Imaging", date: "Jun 18", billed: 1240, stage: "Submitted", denied: true },
];

function Pipeline({ stage, denied }: { stage: ClaimStage; denied?: boolean }) {
  const activeIdx = STAGES.indexOf(stage);
  return (
    <div className="flex items-center gap-1" aria-hidden>
      {STAGES.map((s, i) => {
        const complete = i < activeIdx || (i === activeIdx && stage === "Paid" && !denied);
        const isCurrent = i === activeIdx;
        const failed = denied && isCurrent;
        return (
          <React.Fragment key={s}>
            <span
              className={cn(
                "size-2.5 shrink-0 rounded-full transition-colors duration-150",
                failed
                  ? "bg-destructive"
                  : complete
                    ? "bg-success"
                    : isCurrent
                      ? "bg-warning"
                      : "bg-secondary",
              )}
            />
            {i < STAGES.length - 1 && (
              <span
                className={cn(
                  "h-0.5 w-4 shrink-0 rounded-full transition-colors duration-150 sm:w-6",
                  i < activeIdx ? "bg-success" : "bg-secondary",
                )}
              />
            )}
          </React.Fragment>
        );
      })}
    </div>
  );
}

/**
 * A claims table where each row carries a four-stage pipeline
 * (submitted → received → adjudicated → paid). Completed stages read green,
 * the current stage amber, a denial red. Amounts use tabular figures. Static
 * composition — a ledger is read, not animated.
 */
export function ClaimsStatus({
  claims = DEFAULT_CLAIMS,
  className,
  ...props
}: ClaimsStatusProps) {
  return (
    <div
      data-slot="claims-status"
      className={cn(
        "w-full max-w-2xl overflow-hidden rounded-xl bg-card text-card-foreground shadow-border",
        className,
      )}
      {...props}
    >
      <ul className="divide-y divide-border">
        {claims.map((c) => {
          const stageLabel = c.denied ? "Denied" : c.stage;
          return (
            <li
              key={c.id}
              className="flex flex-col gap-3 p-3.5 sm:flex-row sm:items-center sm:gap-4"
            >
              <div className="min-w-0 flex-1">
                <div className="flex items-baseline gap-2">
                  <p className="truncate text-sm font-medium">{c.service}</p>
                  <span className="shrink-0 font-mono text-[11px] text-muted-foreground tabular-nums">
                    {c.id}
                  </span>
                </div>
                <p className="mt-0.5 text-xs text-muted-foreground tabular-nums">
                  {c.provider} · {c.date}
                </p>
              </div>

              <div className="flex flex-col gap-1.5 sm:items-center">
                <Pipeline stage={c.stage} denied={c.denied} />
                <span
                  className={cn(
                    "text-[11px] font-medium",
                    c.denied
                      ? "text-destructive"
                      : c.stage === "Paid"
                        ? "text-success"
                        : "text-muted-foreground",
                  )}
                >
                  {stageLabel}
                </span>
              </div>

              <div className="shrink-0 text-right sm:w-24">
                <p className="text-sm font-semibold tabular-nums">{usd(c.billed)}</p>
                {c.responsibility !== undefined && (
                  <p className="text-[11px] text-muted-foreground tabular-nums">
                    You owe {usd(c.responsibility)}
                  </p>
                )}
              </div>
            </li>
          );
        })}
      </ul>
    </div>
  );
}