Trial balance
Total assets, liabilities and equity and this year's net income, then each account's debit, credit and balance with the totals. 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 accounting. 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 exampletrial-balance.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 balance sheet totals and this year's net * income, then every account's debits, credits and closing balance. */ export async function TrialBalance() { const [state, t, format, locale] = await Promise.all([ getBooks(), getExtracted("accounting"), getFormatter(), getLocale(), ]); if (state.kind === "no-organization") { return null; } if (state.kind === "error") { return ( <Alert variant="destructive" data-recipe="trial-balance"> <TriangleAlertIcon /> <AlertTitle>{t("The books 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> ); } if (state.data === "not-set-up" || state.data.accounts.length === 0) { return ( <Empty className="border" data-recipe="trial-balance"> <EmptyHeader> <EmptyMedia variant="icon"> <BookOpenIcon /> </EmptyMedia> <EmptyTitle>{t("No entries in the books yet")}</EmptyTitle> <EmptyDescription> {t( "Set up the books with a chart of accounts, open a fiscal year, then post the first entry.", )} </EmptyDescription> </EmptyHeader> <EmptyContent> <a className="text-sm underline underline-offset-4" href="https://bettersupabase.com/docs/blocks/accounting" > {t("Read the accounting guide")} </a> </EmptyContent> </Empty> ); } const books = state.data; const amount = (value: MoneyJson) => formatMoney(moneyFromJson(value), locale); const asOf = format.dateTime(new Date(books.asOf), { dateStyle: "medium", timeZone: "UTC", }); return ( <div className="flex flex-col gap-6" data-recipe="trial-balance"> <div className="grid gap-4 md:grid-cols-3"> <Card> <CardHeader> <CardDescription>{t("Assets")}</CardDescription> <CardTitle>{amount(books.totalAssets)}</CardTitle> </CardHeader> </Card> <Card> <CardHeader> <CardDescription>{t("Liabilities and equity")}</CardDescription> <CardTitle>{amount(books.liabilitiesAndEquity)}</CardTitle> </CardHeader> <CardContent> {books.balanced ? ( <Badge variant="success-light">{t("Balanced")}</Badge> ) : ( <Badge variant="destructive-light">{t("Out of balance")}</Badge> )} </CardContent> </Card> <Card> <CardHeader> <CardDescription>{t("Net income this year")}</CardDescription> <CardTitle>{amount(books.netIncome)}</CardTitle> </CardHeader> </Card> </div> <Card> <CardHeader> <CardTitle>{t("Trial balance")}</CardTitle> <CardDescription> {t("Posted entries through {date}.", { date: asOf })} </CardDescription> </CardHeader> <CardContent> <Table> <TableHeader> <TableRow> <TableHead>{t("Code")}</TableHead> <TableHead>{t("Account")}</TableHead> <TableHead className="text-right">{t("Debit")}</TableHead> <TableHead className="text-right">{t("Credit")}</TableHead> <TableHead className="text-right">{t("Balance")}</TableHead> </TableRow> </TableHeader> <TableBody> {books.accounts.map((row) => ( <TableRow key={row.accountId}> <TableCell className="font-mono text-xs"> {row.code} </TableCell> <TableCell className="font-medium">{row.name}</TableCell> <TableCell className="text-right tabular-nums"> {amount(row.debit)} </TableCell> <TableCell className="text-right tabular-nums"> {amount(row.credit)} </TableCell> <TableCell className="text-right tabular-nums"> {amount(row.closing)} </TableCell> </TableRow> ))} </TableBody> <TableFooter> <TableRow> <TableCell colSpan={2}>{t("Total")}</TableCell> <TableCell className="text-right tabular-nums"> {amount(books.totals.debit)} </TableCell> <TableCell className="text-right tabular-nums"> {amount(books.totals.credit)} </TableCell> <TableCell /> </TableRow> </TableFooter> </Table> </CardContent> </Card> </div> ); } export function TrialBalanceSkeleton() { return ( <div className="flex flex-col gap-6" aria-busy="true"> <div className="grid gap-4 md:grid-cols-3"> <Skeleton className="h-28 rounded-xl" /> <Skeleton className="h-28 rounded-xl" /> <Skeleton className="h-28 rounded-xl" /> </div> <Skeleton className="h-72 rounded-xl" /> </div> ); }