# Approvals queue

> Requests waiting for the caller's decision, with approve and reject buttons.

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

## Install the block

```bash
npx better-supabase add approvals
```

## Web (Next.js)

Add the shadcn components:

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

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

```tsx
/**
 * Render inside `<Suspense>`: the organization's pending approval requests,
 * newest first, with Approve and Reject on each. The block checks that the
 * caller is an approver of the current step.
 */
export async function ApprovalsQueue() {
  const [session, t, format] = await Promise.all([
    getSession(),
    getExtracted("approvals"),
    getFormatter(),
  ]);

  if (!can(session, "approvals.read")) {
    forbidden();
  }

  const queue = await getApprovalsQueue();

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

  const canRequest = can(session, "approvals.request");
  const canDecide = can(session, "approvals.manage");
  const customers = queue.kind === "ok" ? queue.data.customers : [];

  const actionLabel = (action: string) =>
    action === "discount" ? t("Discount") : action;

  return (
    <Card data-recipe="approvals-queue">
      <CardHeader>
        <CardTitle>{t("Pending requests")}</CardTitle>
        <CardDescription>
          {t("Each request waits for its approvers, one step at a time.")}
        </CardDescription>
        {canRequest && queue.kind === "ok" && queue.data.requests.length > 0 ? (
          <CardAction>
            <RequestApprovalDialog customers={customers} />
          </CardAction>
        ) : null}
      </CardHeader>
      <CardContent>
        {queue.kind === "error" ? (
          <Alert variant="destructive">
            <TriangleAlertIcon />
            <AlertTitle>{t("The requests didn't load")}</AlertTitle>
            <AlertDescription>{t("Try again in a moment.")}</AlertDescription>
            <AlertAction>
              <form action={retryApprovals}>
                <Button size="sm" variant="outline" type="submit">
                  {t("Retry")}
                </Button>
              </form>
            </AlertAction>
          </Alert>
        ) : queue.data.requests.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <ShieldCheckIcon />
              </EmptyMedia>
              <EmptyTitle>{t("Nothing waiting")}</EmptyTitle>
              <EmptyDescription>
                {canRequest
                  ? t(
                      "Ask for an approval before giving a customer a discount.",
                    )
                  : t("New requests show up here.")}
              </EmptyDescription>
            </EmptyHeader>
            {canRequest ? (
              <EmptyContent>
                <RequestApprovalDialog customers={customers} />
              </EmptyContent>
            ) : null}
          </Empty>
        ) : (
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>{t("Request")}</TableHead>
                <TableHead>{t("Requested by")}</TableHead>
                <TableHead>{t("Step")}</TableHead>
                <TableHead>{t("Requested")}</TableHead>
                {canDecide ? (
                  <TableHead className="text-right">{t("Decision")}</TableHead>
                ) : null}
              </TableRow>
            </TableHeader>
            <TableBody>
              {queue.data.requests.map((request) => {
                const label = `${actionLabel(request.action)}: ${
                  request.subjectName ?? request.subjectId
                }`;

                return (
                  <TableRow key={request.id}>
                    <TableCell>
                      <div className="flex flex-col gap-1">
                        <span className="font-medium">{label}</span>
                        {request.reason ? (
                          <span className="text-xs text-muted-foreground">
                            {request.reason}
                          </span>
                        ) : null}
                      </div>
                    </TableCell>
                    <TableCell className="text-muted-foreground">
                      {request.requestedBy ?? t("Unknown")}
                    </TableCell>
                    <TableCell>
                      <Badge variant="warning-light">
                        {t("{step, number} of {steps, number}", {
                          step: request.step + 1,
                          steps: request.steps,
                        })}
                      </Badge>
                    </TableCell>
                    <TableCell className="text-muted-foreground">
                      {format.dateTime(new Date(request.createdAt), {
                        dateStyle: "medium",
                        timeStyle: "short",
                      })}
                    </TableCell>
                    {canDecide ? (
                      <TableCell>
                        <ApprovalDecision id={request.id} label={label} />
                      </TableCell>
                    ) : null}
                  </TableRow>
                );
              })}
            </TableBody>
          </Table>
        )}
      </CardContent>
    </Card>
  );
}
```

## Prompt

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

### Web

```text
Build "Approvals queue" in a React web app that uses shadcn/ui components. Requests waiting for the caller's decision, with approve and reject buttons.

It uses the better-supabase `approvals` block (Approvals). 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 approvals` 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 screen. Load the data on the server with the block's client, starting from the recipe's snippet (`apps/examples/nextjs/src/features/approvals/components/approvals-queue.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/approvals-queue.md
- Block docs: https://bettersupabase.com/docs/blocks/approvals
```
