# SLA badges

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

Source: https://bettersupabase.com/docs/registry/ui-recipes/sla-badges

{/* Generated by scripts/build-recipes.ts from the example apps. Edit the examples or the script, then run `pnpm recipes`. */}

It loads its data on the server through the block's client; the [sla block](https://bettersupabase.com/blocks/sla) must be installed (`npx better-supabase add sla`). Its page on bettersupabase.com is [/recipes/sla-badges](https://bettersupabase.com/recipes/sla-badges).

Web, with shadcn components:

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

```tsx title="apps/examples/nextjs/src/features/customers/components/sla-badges.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} />;
}
```