# SLA badges

> A badge on a record with the time left before its SLA is breached.

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

## Install the block

```bash
npx better-supabase add sla
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add badge button card empty
```

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

```tsx
const VARIANT = {
  running: "info-light",
  paused: "secondary",
  met: "success-light",
  breached: "destructive-light",
} as const satisfies Record<SlaClockRow["state"], string>;

/**
 * The customer's SLA clocks as badges. Without clocks, the empty state
 * starts them; with no policy for customers, a caller who manages SLAs gets
 * a default policy first.
 */
export function SlaBadges({
  customerId,
  clocks,
  canCreatePolicy,
}: {
  customerId: string;
  clocks: Loaded<readonly SlaClockRow[]>;
  canCreatePolicy: boolean;
}) {
  const t = useExtracted("customers");
  const format = useFormatter();
  const errorMessage = useErrorMessage();

  const start = useAction(startCustomerSla, {
    onSuccess: (count) => {
      if (count === 0) {
        toast.error(t("No SLA policy covers customers yet."));
      } else {
        toast.success(t("SLA clocks started"));
      }
    },
    onError: (error) => {
      toast.error(errorMessage(error));
    },
  });

  const metric = {
    first_response: t("First response"),
    next_response: t("Next response"),
    resolution: t("Resolution"),
  } as const satisfies Record<SlaClockRow["metric"], string>;

  const state = {
    running: t("On track"),
    paused: t("Paused"),
    met: t("Met"),
    breached: t("Breached"),
  } as const satisfies Record<SlaClockRow["state"], string>;

  const timing = (clock: SlaClockRow): string => {
    if (clock.state === "met") {
      return t("Done");
    }

    if (clock.secondsLeft === null || clock.state === "paused") {
      return t("Due {date}", {
        date: format.dateTime(clock.dueAt, {
          dateStyle: "medium",
          timeStyle: "short",
        }),
      });
    }

    const minutes = Math.round(Math.abs(clock.secondsLeft) / 60);

    const span =
      minutes >= 1440
        ? format.number(Math.round(minutes / 1440), {
            style: "unit",
            unit: "day",
            unitDisplay: "long",
          })
        : minutes >= 60
          ? format.number(Math.round(minutes / 60), {
              style: "unit",
              unit: "hour",
              unitDisplay: "long",
            })
          : format.number(minutes, {
              style: "unit",
              unit: "minute",
              unitDisplay: "long",
            });

    return clock.secondsLeft < 0
      ? t("{span} overdue", { span })
      : t("{span} left", { span });
  };

  return (
    <Card size="sm" data-recipe="sla-badges">
      <CardHeader>
        <CardTitle>{t("SLA")}</CardTitle>
      </CardHeader>
      <CardContent>
        {clocks.kind === "error" ? (
          <RecordLoadError title={t("The SLA clocks didn't load")} />
        ) : clocks.data.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <TimerIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No SLA clocks")}</EmptyTitle>
              <EmptyDescription>
                {canCreatePolicy
                  ? t(
                      "Start the clocks. Without a policy for customers, a default one is created: first response in 4 hours, resolution in 3 days.",
                    )
                  : t("Start the clocks of the policy that covers customers.")}
              </EmptyDescription>
            </EmptyHeader>
            <EmptyContent>
              <Button
                size="sm"
                disabled={start.pending}
                onClick={() => {
                  void start.run({ customerId });
                }}
              >
                <TimerIcon />
                {t("Start SLA clocks")}
              </Button>
            </EmptyContent>
          </Empty>
        ) : (
          <ul className="flex flex-col gap-2" data-testid="sla-clocks">
            {clocks.data.map((clock) => (
              <li
                key={clock.id}
                className="flex items-center justify-between gap-2"
              >
                <div className="flex min-w-0 flex-col gap-1">
                  <span className="text-sm font-medium">
                    {metric[clock.metric]}
                  </span>
                  <span className="text-xs text-muted-foreground">
                    {timing(clock)}
                  </span>
                </div>
                <Badge variant={VARIANT[clock.state]}>
                  {state[clock.state]}
                </Badge>
              </li>
            ))}
          </ul>
        )}
      </CardContent>
    </Card>
  );
}

/** The SLA card while its clocks load. */
export function SlaBadgesSkeleton() {
  return <RecordCardSkeleton rows={2} />;
}
```

## Prompt

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

### Web

```text
Build "SLA badges" in a React web app that uses shadcn/ui components. A badge on a record with the time left before its SLA is breached.

It uses the better-supabase `sla` block (Service levels). 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 sla` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add badge button card empty`.
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/customers/components/sla-badges.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/sla-badges.md
- Block docs: https://bettersupabase.com/docs/blocks/sla
```
