Components

Invoice Table

A billing history table with status badges, formatted amounts, and per-row download actions.

data

Installation

npx cano-ui add invoice-table

Installs badge, button, table, lucide-react automatically if missing.

Source

import * as React from "react"
import { Download } from "lucide-react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

export type InvoiceStatus = "paid" | "open" | "overdue" | "refunded"

export interface Invoice {
  id: string
  /** Invoice number shown to the user, e.g. "INV-0042". */
  number: string
  date: string
  /** Pre-formatted amount, e.g. "$249.00". */
  amount: string
  status: InvoiceStatus
  description?: string
}

export interface InvoiceTableProps extends React.ComponentProps<"div"> {
  invoices: Invoice[]
  onDownload?: (invoice: Invoice) => void
  emptyMessage?: string
}

const statusStyles: Record<InvoiceStatus, string> = {
  paid: "border-transparent bg-emerald-500/15 text-emerald-700 dark:text-emerald-400",
  open: "border-transparent bg-blue-500/15 text-blue-700 dark:text-blue-400",
  overdue:
    "border-transparent bg-destructive/15 text-destructive dark:text-red-400",
  refunded: "border-transparent bg-muted text-muted-foreground",
}

const statusLabels: Record<InvoiceStatus, string> = {
  paid: "Paid",
  open: "Open",
  overdue: "Overdue",
  refunded: "Refunded",
}

export function InvoiceStatusBadge({ status }: { status: InvoiceStatus }) {
  return (
    <Badge variant="outline" className={cn("capitalize", statusStyles[status])}>
      {statusLabels[status]}
    </Badge>
  )
}

export function InvoiceTable({
  invoices,
  onDownload,
  emptyMessage = "No invoices yet.",
  className,
  ...props
}: InvoiceTableProps) {
  return (
    <div
      data-slot="invoice-table"
      className={cn("w-full overflow-hidden rounded-lg border", className)}
      {...props}
    >
      <Table>
        <TableHeader>
          <TableRow className="hover:bg-transparent">
            <TableHead>Invoice</TableHead>
            <TableHead className="max-sm:hidden">Date</TableHead>
            <TableHead>Status</TableHead>
            <TableHead className="text-right">Amount</TableHead>
            {onDownload ? (
              <TableHead className="w-12">
                <span className="sr-only">Download</span>
              </TableHead>
            ) : null}
          </TableRow>
        </TableHeader>
        <TableBody>
          {invoices.length === 0 ? (
            <TableRow>
              <TableCell
                colSpan={onDownload ? 5 : 4}
                className="h-24 text-center text-muted-foreground"
              >
                {emptyMessage}
              </TableCell>
            </TableRow>
          ) : (
            invoices.map((invoice) => (
              <TableRow key={invoice.id}>
                <TableCell>
                  <div className="flex flex-col">
                    <span className="font-medium">{invoice.number}</span>
                    {invoice.description ? (
                      <span className="text-xs text-muted-foreground max-sm:hidden">
                        {invoice.description}
                      </span>
                    ) : null}
                  </div>
                </TableCell>
                <TableCell className="text-muted-foreground max-sm:hidden">
                  {invoice.date}
                </TableCell>
                <TableCell>
                  <InvoiceStatusBadge status={invoice.status} />
                </TableCell>
                <TableCell className="text-right tabular-nums">
                  {invoice.amount}
                </TableCell>
                {onDownload ? (
                  <TableCell>
                    <Button
                      variant="ghost"
                      size="icon"
                      className="size-8"
                      aria-label={`Download ${invoice.number}`}
                      onClick={() => onDownload(invoice)}
                    >
                      <Download aria-hidden="true" />
                    </Button>
                  </TableCell>
                ) : null}
              </TableRow>
            ))
          )}
        </TableBody>
      </Table>
    </div>
  )
}

This exact file lands in your project at components/ui/invoice-table.tsx. It is yours to edit.