# Contract renewals

> Active contracts with their value, end date and notice deadline, the nearest deadline first, so a renewal is decided in time.

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

## Install the block

```bash
npx better-supabase add contracts
```

## Web (Next.js)

Add the shadcn components:

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

From `apps/examples/nextjs/src/features/compliance/components/contract-renewals.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/compliance/components/contract-renewals.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();
}

const SOON_DAYS = 30;

/**
 * Render inside `<Suspense>`: the organization's active contracts, the nearest
 * notice deadline first, so the team decides on a renewal before the
 * counterparty's notice period runs out.
 */
export async function ContractRenewals() {
  const [state, t, format, locale] = await Promise.all([
    getRenewals(),
    getExtracted("compliance"),
    getFormatter(),
    getLocale(),
  ]);

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

  const day = (date: string) =>
    format.dateTime(new Date(date), { dateStyle: "medium", timeZone: "UTC" });

  const renewal = (row: RenewalRow) => {
    const days = row.noticeInDays;

    if (days === null) {
      return <Badge variant="secondary">{t("No end date")}</Badge>;
    }

    if (days < 0) {
      return (
        <Badge variant="destructive-light">
          {row.autoRenew ? t("Renews, notice passed") : t("Notice passed")}
        </Badge>
      );
    }

    if (days <= SOON_DAYS) {
      return (
        <Badge variant="warning-light">
          {t("Notice in {days, number} days", { days })}
        </Badge>
      );
    }

    return row.autoRenew ? (
      <Badge variant="info-light">{t("Auto-renews")}</Badge>
    ) : (
      <Badge variant="success-light">{t("Active")}</Badge>
    );
  };

  return (
    <Card data-recipe="contract-renewals">
      <CardHeader>
        <CardTitle>{t("Contract renewals")}</CardTitle>
        <CardDescription>
          {t(
            "Active contracts, the nearest notice deadline first. A contract that auto-renews extends itself unless someone gives notice before the deadline.",
          )}
        </CardDescription>
      </CardHeader>
      <CardContent>
        {state.kind === "error" ? (
          <Alert variant="destructive">
            <TriangleAlertIcon />
            <AlertTitle>{t("The contracts 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>
        ) : state.data.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <FileSignatureIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No active contracts")}</EmptyTitle>
              <EmptyDescription>
                {t(
                  "A contract becomes active once every signer has signed. Draft one from a customer's record.",
                )}
              </EmptyDescription>
            </EmptyHeader>
            <EmptyContent>
              <Link href="/customers" className={buttonVariants()}>
                {t("Open customers")}
              </Link>
            </EmptyContent>
          </Empty>
        ) : (
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>{t("Contract")}</TableHead>
                <TableHead className="text-right">{t("Value")}</TableHead>
                <TableHead>{t("Ends")}</TableHead>
                <TableHead>{t("Notice by")}</TableHead>
                <TableHead>{t("Renewal")}</TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {state.data.map((row) => (
                <TableRow key={row.id}>
                  <TableCell className="font-medium">{row.title}</TableCell>
                  <TableCell className="text-right tabular-nums">
                    {row.value
                      ? formatMoney(moneyFromJson(row.value), locale)
                      : "–"}
                  </TableCell>
                  <TableCell className="text-xs text-muted-foreground">
                    {row.endsOn ? day(row.endsOn) : "–"}
                  </TableCell>
                  <TableCell className="text-xs text-muted-foreground">
                    {row.noticeDeadline ? day(row.noticeDeadline) : "–"}
                  </TableCell>
                  <TableCell>{renewal(row)}</TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        )}
      </CardContent>
    </Card>
  );
}

export function ContractRenewalsSkeleton() {
  return <Skeleton className="h-40 rounded-xl" aria-busy="true" />;
}
```

## Prompt

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

### Web

```text
Build "Contract renewals" in a React web app that uses shadcn/ui components. Active contracts with their value, end date and notice deadline, the nearest deadline first, so a renewal is decided in time.

It uses the better-supabase `contracts` block (Contracts). 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 contracts` 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/compliance/components/contract-renewals.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/contract-renewals.md
- Block docs: https://bettersupabase.com/docs/blocks/contracts
```
