UI recipesUI recipes
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; the accounting block must be installed (npx better-supabase add accounting). Its page on bettersupabase.com is /recipes/trial-balance.
Web, with shadcn components:
npx shadcn add alert badge button card empty skeleton table// 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-2 lg:grid-cols-4">
<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>
<Card>
<CardHeader>
<CardDescription>{t("Net tax this year")}</CardDescription>
<CardTitle>{amount(books.netTax)}</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-2 lg:grid-cols-4">
<Skeleton className="h-28 rounded-xl" />
<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>
);
}Last updated on