# Targets progress

> Sales targets for the period with the progress toward each one.

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

## Install the block

```bash
npx better-supabase add targets
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add badge card empty progress skeleton
```

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

```tsx
/** Render inside `<Suspense>`: the targets running today and how far along they are. */
export async function TargetsProgress() {
  const [session, t, format, locale] = await Promise.all([
    getSession(),
    getExtracted("crm"),
    getFormatter(),
    getLocale(),
  ]);

  if (!can(session, "targets.read")) {
    return null;
  }

  const targets = await settle(loadTargets);

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

  const canManage = can(session, "targets.manage");

  const amountOf = (target: TargetRow, value: bigint) =>
    target.currency === null
      ? format.number(value)
      : formatMoney(money(value, target.currency), locale);

  return (
    <Card data-recipe="targets-progress">
      <CardHeader>
        <CardTitle>{t("Targets")}</CardTitle>
        <CardDescription>
          {t("Won deals against this period's goals.")}
        </CardDescription>
        {canManage && targets.kind === "ok" && targets.data.length > 0 ? (
          <CardAction>
            <CreateTargetDialog variant="outline" />
          </CardAction>
        ) : null}
      </CardHeader>
      <CardContent>
        {targets.kind === "error" ? (
          <CrmLoadError title={t("The targets didn't load")} />
        ) : targets.data.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <TargetIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No targets running")}</EmptyTitle>
              <EmptyDescription>
                {canManage
                  ? t("Set a goal for the amount or number of deals won.")
                  : t("An admin sets the sales targets.")}
              </EmptyDescription>
            </EmptyHeader>
            {canManage ? (
              <EmptyContent>
                <CreateTargetDialog />
              </EmptyContent>
            ) : null}
          </Empty>
        ) : (
          <ul className="grid gap-6 md:grid-cols-2">
            {targets.data.map((target) => (
              <li key={target.id} className="flex flex-col gap-2">
                <Progress
                  value={Math.min(target.attainment * 100, 100)}
                  aria-label={target.name}
                >
                  <ProgressLabel>{target.name}</ProgressLabel>
                  {target.attainment >= 1 ? (
                    <Badge variant="success-light" className="ml-auto">
                      {t("Reached")}
                    </Badge>
                  ) : (
                    <span className="ml-auto text-sm text-muted-foreground tabular-nums">
                      {format.number(target.attainment, {
                        style: "percent",
                        maximumFractionDigits: 0,
                      })}
                    </span>
                  )}
                </Progress>
                <span className="text-xs text-muted-foreground tabular-nums">
                  {t("{progress} of {goal}, {start} to {end}", {
                    progress: amountOf(target, target.progress),
                    goal: amountOf(target, target.goal),
                    start: Temporal.PlainDate.from(
                      target.periodStart,
                    ).toLocaleString(locale, { dateStyle: "medium" }),
                    end: Temporal.PlainDate.from(
                      target.periodEnd,
                    ).toLocaleString(locale, { dateStyle: "medium" }),
                  })}
                </span>
              </li>
            ))}
          </ul>
        )}
      </CardContent>
    </Card>
  );
}
```

## Prompt

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

### Web

```text
Build "Targets progress" in a React web app that uses shadcn/ui components. Sales targets for the period with the progress toward each one.

It uses the better-supabase `targets` block (Targets). 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 targets` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add badge card empty progress skeleton`.
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/crm/components/targets-progress.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/targets-progress.md
- Block docs: https://bettersupabase.com/docs/blocks/targets
```
