Invoices table
The organization's invoices with their status, amount and a link to the PDF. It loads its data on the server through the block's client.
pnpm dev:example) to see it.Add the components
terminal npx shadcn add alert badge button card empty skeleton tableInstall the glue
Install the block with
npx better-supabase add invoices. The glue items are copied into your app, so edit them as you need.None: the page calls the block's client on the server.
Write the component
React and shadcn
View in the exampleinvoices-table.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> ); }