# Invoices table

> The organization's invoices with their status, amount and a link to the PDF.

- Block: [invoices](https://bettersupabase.com/blocks/invoices.md)
- Kind: component
- Data: loaded on the server through the block's client
- Page: https://bettersupabase.com/recipes/invoices-table

## Install the block

```bash
npx better-supabase add invoices
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add alert badge button card empty skeleton table
```

From `apps/examples/nextjs/src/features/billing/components/invoices-table.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/billing/components/invoices-table.tsx):

```tsx
// oxlint-disable-next-line typescript/require-await -- a Server Action must be async, and refresh() returns nothing to await
async function retry() {
  "use server";
  refresh();
}

/**
 * Render inside `<Suspense>`: the organization's invoices with their status,
 * total and what's still owed. Renders nothing without `invoices.read`, so
 * the billing page still shows the plan.
 */
export async function InvoicesTable() {
  const session = await getSession();

  if (!can(session, "invoices.read")) {
    return null;
  }

  const [list, t, format, locale] = await Promise.all([
    getInvoices(),
    getExtracted("billing"),
    getFormatter(),
    getLocale(),
  ]);

  if (list.kind === "no-organization") {
    return null;
  }

  const canWrite = can(session, "invoices.write");

  const status = (value: InvoiceStatus, overdue: boolean) => {
    if (overdue) {
      return <Badge variant="destructive-light">{t("Overdue")}</Badge>;
    }

    switch (value) {
      case "draft": {
        return <Badge variant="secondary">{t("Draft")}</Badge>;
      }

      case "issued": {
        return <Badge variant="info-light">{t("Issued")}</Badge>;
      }

      case "partially_paid": {
        return <Badge variant="warning-light">{t("Partly paid")}</Badge>;
      }

      case "paid": {
        return <Badge variant="success-light">{t("Paid")}</Badge>;
      }

      case "void": {
        return <Badge variant="outline">{t("Void")}</Badge>;
      }

      case "uncollectible": {
        return <Badge variant="destructive-light">{t("Uncollectible")}</Badge>;
      }

      default: {
        const unknown: never = value;

        return unknown;
      }
    }
  };

  const hasInvoices = list.kind === "ok" && list.invoices.length > 0;

  return (
    <Card data-recipe="invoices-table">
      <CardHeader>
        <CardTitle>{t("Invoices")}</CardTitle>
        <CardDescription>
          {t("Every invoice for this organization, newest first.")}
        </CardDescription>
        {hasInvoices && canWrite ? (
          <CardAction>
            <CreateDraftInvoiceDialog variant="outline" />
          </CardAction>
        ) : null}
      </CardHeader>
      <CardContent>
        {list.kind === "error" ? (
          <Alert variant="destructive">
            <TriangleAlertIcon />
            <AlertTitle>{t("The invoices didn't load")}</AlertTitle>
            <AlertDescription>{t("Try again in a moment.")}</AlertDescription>
            <AlertAction>
              <form action={retry}>
                <Button size="sm" variant="outline" type="submit">
                  {t("Retry")}
                </Button>
              </form>
            </AlertAction>
          </Alert>
        ) : list.invoices.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <FileTextIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No invoices yet")}</EmptyTitle>
              <EmptyDescription>
                {t("Invoices show up here once they're created.")}
              </EmptyDescription>
            </EmptyHeader>
            {canWrite ? (
              <EmptyContent>
                <CreateDraftInvoiceDialog />
              </EmptyContent>
            ) : null}
          </Empty>
        ) : (
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>{t("Invoice")}</TableHead>
                <TableHead>{t("Status")}</TableHead>
                <TableHead>{t("Due")}</TableHead>
                <TableHead className="text-right">{t("Total")}</TableHead>
                <TableHead className="text-right">{t("Owed")}</TableHead>
                {canWrite ? (
                  <TableHead>
                    <span className="sr-only">{t("Actions")}</span>
                  </TableHead>
                ) : null}
              </TableRow>
            </TableHeader>
            <TableBody>
              {list.invoices.map((invoice) => (
                <TableRow key={invoice.id}>
                  <TableCell>
                    <div className="flex flex-col gap-1">
                      <span className="text-sm">{invoice.title}</span>
                      <span className="font-mono text-xs text-muted-foreground">
                        {invoice.identifier ?? t("No number yet")}
                      </span>
                    </div>
                  </TableCell>
                  <TableCell>
                    {status(invoice.status, invoice.overdue)}
                  </TableCell>
                  <TableCell className="text-muted-foreground">
                    {invoice.dueOn
                      ? format.dateTime(new Date(invoice.dueOn), {
                          dateStyle: "medium",
                          timeZone: "UTC",
                        })
                      : t("Not issued")}
                  </TableCell>
                  <TableCell className="text-right tabular-nums">
                    {formatMoney(invoice.total, locale)}
                  </TableCell>
                  <TableCell className="text-right tabular-nums">
                    {formatMoney(invoice.balance, locale)}
                  </TableCell>
                  {canWrite ? (
                    <TableCell className="text-right">
                      {invoice.status === "draft" ? (
                        <IssueInvoiceButton invoiceId={invoice.id} />
                      ) : null}
                    </TableCell>
                  ) : null}
                </TableRow>
              ))}
            </TableBody>
          </Table>
        )}
      </CardContent>
    </Card>
  );
}
```

## Prompt

Paste this into a coding agent to build the recipe in your app.

### Web

```text
Build "Invoices table" in a React web app that uses shadcn/ui components. The organization's invoices with their status, amount and a link to the PDF.

It uses the better-supabase `invoices` block (Invoices). It loads its data on the server through the block's client.

Steps:

1. If the app doesn't have the block yet, add the SQL with `npx better-supabase add invoices` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add alert badge button card empty skeleton table`.
3. The recipe needs no glue: it calls the block's client on the server.
4. Write the component. Load the data on the server with the block's client, starting from the recipe's snippet (`apps/examples/nextjs/src/features/billing/components/invoices-table.tsx`). Adapt it to the app's existing components, styling and data loading instead of copying it as is.
5. Check the result against the recipe and the block's docs: it typechecks, it reads the same block data, and it handles the loading and empty states the snippet handles.

Read the full code first:

- Recipe: https://bettersupabase.com/recipes/invoices-table.md
- Block docs: https://bettersupabase.com/docs/blocks/invoices
```
