# Payroll runs

> The organization's payroll runs, newest period first, with their line count, export format and whether they are locked.

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

## Install the block

```bash
npx better-supabase add payroll-export
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add alert badge button empty skeleton table @reui/frame
```

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

/**
 * Render inside `<Suspense>`: the organization's payroll runs, newest period
 * first, with how many lines each holds and the format it was exported in.
 */
export async function PayrollRuns() {
  const [state, t] = await Promise.all([
    getPayrollRuns(),
    getExtracted("payroll-export"),
  ]);

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

  if (state.kind === "error") {
    return (
      <Alert variant="destructive" data-recipe="payroll-runs">
        <TriangleAlertIcon />
        <AlertTitle>{t("The payroll runs 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.length === 0) {
    return (
      <Empty className="border" data-recipe="payroll-runs">
        <EmptyHeader>
          <EmptyMedia variant="icon">
            <WalletIcon />
          </EmptyMedia>
          <EmptyTitle>{t("No payroll runs yet")}</EmptyTitle>
          <EmptyDescription>
            {t(
              "Create a run for a period to add up the approved hours and leave, then export it in your payroll provider's format.",
            )}
          </EmptyDescription>
        </EmptyHeader>
        <EmptyContent>
          <a
            className="text-sm underline underline-offset-4"
            href="https://bettersupabase.com/docs/blocks/payroll-export"
          >
            {t("Read the payroll export guide")}
          </a>
        </EmptyContent>
      </Empty>
    );
  }

  return (
    <Frame spacing="sm" data-recipe="payroll-runs">
      <FrameHeader className="gap-0.5">
        <FrameTitle>{t("Payroll runs")}</FrameTitle>
        <FrameDescription>
          {t("An exported run is locked; its checksum proves the file.")}
        </FrameDescription>
      </FrameHeader>
      <FramePanel>
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead>{t("Period")}</TableHead>
              <TableHead>{t("Label")}</TableHead>
              <TableHead>{t("Lines")}</TableHead>
              <TableHead>{t("Format")}</TableHead>
              <TableHead>{t("Status")}</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {state.data.map((run) => (
              <TableRow key={run.id}>
                <TableCell>
                  {run.periodFrom} – {run.periodTo}
                </TableCell>
                <TableCell>{run.label ?? "–"}</TableCell>
                <TableCell>{run.lineCount}</TableCell>
                <TableCell>{run.format ?? "–"}</TableCell>
                <TableCell>
                  {run.status === "exported" ? (
                    <Badge variant="success-light">{t("Exported")}</Badge>
                  ) : (
                    <Badge variant="warning-light">{t("Draft")}</Badge>
                  )}
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </FramePanel>
    </Frame>
  );
}

export function PayrollRunsSkeleton() {
  return (
    <Frame spacing="sm">
      <FrameHeader className="gap-1">
        <Skeleton className="h-4 w-36" />
        <Skeleton className="h-4 w-64" />
      </FrameHeader>
      <FramePanel>
        <Skeleton className="h-48" />
      </FramePanel>
    </Frame>
  );
}
```

## Prompt

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

### Web

```text
Build "Payroll runs" in a React web app that uses shadcn/ui components. The organization's payroll runs, newest period first, with their line count, export format and whether they are locked.

It uses the better-supabase `payroll-export` block (Payroll export). 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 payroll-export` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add alert badge button empty skeleton table @reui/frame`.
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/payroll-export/components/payroll-runs.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/payroll-runs.md
- Block docs: https://bettersupabase.com/docs/blocks/payroll-export
```
