# Inspection run

> One inspection with its status, checklist and team, its findings with a Raise case button for each nonconformity, and a Close button once every one has its case.

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

## Install the block

```bash
npx better-supabase add inspections
```

## Web (Next.js)

Add the shadcn components:

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

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

```tsx
/**
 * One inspection with its team, checklist and findings. A finding that needs a
 * case gets a Raise case button, and Close is enabled once each one has its
 * case; the database checks again when it runs.
 */
export function InspectionRun({
  state,
  canWork,
}: {
  state: Loaded<InspectionRunView>;
  canWork: boolean;
}) {
  const t = useExtracted("inspections");
  const router = useRouter();
  const [retrying, startRetry] = useTransition();
  const errorMessage = useErrorMessage();
  const kind = useKindLabel();
  const findingType = useFindingTypeLabel();
  const role = useRoleLabel();

  const raise = useAction(raiseFindingCase, {
    onSuccess: () => {
      toast.success(t("Case raised"));
    },
    onError: (error) => {
      toast.error(errorMessage(error));
    },
  });

  const close = useAction(closeInspection, {
    onSuccess: ({ number }) => {
      toast.success(t("Closed {number}", { number }));
    },
    onError: (error) => {
      toast.error(errorMessage(error));
    },
  });

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

  if (state.kind === "error") {
    return (
      <Frame spacing="sm">
        <FramePanel>
          <Alert variant="destructive">
            <TriangleAlertIcon />
            <AlertTitle>{t("The inspection didn't load")}</AlertTitle>
            <AlertDescription>{t("Try again in a moment.")}</AlertDescription>
            <AlertAction>
              <Button
                size="sm"
                variant="outline"
                type="button"
                disabled={retrying}
                onClick={() => {
                  startRetry(() => {
                    router.refresh();
                  });
                }}
              >
                {t("Retry")}
              </Button>
            </AlertAction>
          </Alert>
        </FramePanel>
      </Frame>
    );
  }

  const run = state.data;

  const waiting = run.findings.filter(
    (finding) => finding.needsCase && finding.caseId === null,
  ).length;

  const working = run.status === "in_progress" || run.status === "reporting";

  return (
    <Frame spacing="sm" data-recipe="inspection-run">
      <FrameHeader className="gap-0.5">
        <FrameTitle>
          {t("{number}: {title}", { number: run.number, title: run.title })}
        </FrameTitle>
        <FrameDescription>{kind(run.kind)}</FrameDescription>
      </FrameHeader>
      <FramePanel className="flex flex-col gap-4">
        <div className="flex flex-wrap items-center gap-2">
          <InspectionStatusBadge status={run.status} />
          {run.checklist === null ? null : run.checklist === "submitted" ? (
            <Badge variant="success-light">{t("Checklist submitted")}</Badge>
          ) : (
            <Badge variant="warning-light">{t("Checklist in draft")}</Badge>
          )}
          <span className="text-xs text-muted-foreground">
            {t(
              "{count, plural, =0 {No team yet} one {# team member} other {# team members}}",
              { count: run.team.length },
            )}
            {run.team.length === 0
              ? ""
              : ` (${run.team.map((member) => role(member.role)).join(", ")})`}
          </span>
        </div>
        {working && waiting > 0 ? (
          <Alert variant="warning">
            <CircleDotIcon />
            <AlertTitle>
              {t(
                "{count, plural, one {# finding needs a case} other {# findings need a case}} before this inspection can close",
                { count: waiting },
              )}
            </AlertTitle>
          </Alert>
        ) : run.status === "reporting" ? (
          <Alert variant="success">
            <CircleCheckIcon />
            <AlertTitle>{t("Every nonconformity has its case")}</AlertTitle>
          </Alert>
        ) : null}
        {run.findings.length === 0 ? (
          <Empty>
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <SearchCheckIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No findings yet")}</EmptyTitle>
              <EmptyDescription>
                {t(
                  "The lead and auditors record findings while the inspection is in progress.",
                )}
              </EmptyDescription>
            </EmptyHeader>
          </Empty>
        ) : (
          <ul className="flex flex-col gap-2">
            {run.findings.map((finding) => (
              <li
                key={finding.id}
                className="flex items-center justify-between gap-2 text-sm"
              >
                <span className="flex items-center gap-2">
                  <span className="font-mono text-xs text-muted-foreground tabular-nums">
                    {finding.number}
                  </span>
                  <Badge
                    variant={
                      finding.needsCase ? "destructive-light" : "outline"
                    }
                  >
                    {findingType(finding.type)}
                  </Badge>
                  {finding.title}
                  {finding.clause === null ? null : (
                    <span className="text-xs text-muted-foreground">
                      {finding.clause}
                    </span>
                  )}
                </span>
                {finding.caseId === null ? (
                  finding.needsCase && working && canWork ? (
                    <Button
                      type="button"
                      size="sm"
                      variant="outline"
                      disabled={raise.pending}
                      onClick={() => {
                        void raise.run({ findingId: finding.id });
                      }}
                    >
                      {t("Raise case")}
                    </Button>
                  ) : null
                ) : (
                  <Link
                    href={`/cases?case=${finding.caseId}`}
                    className="text-sm underline-offset-4 transition-colors hover:underline"
                  >
                    {t("Open case")}
                  </Link>
                )}
              </li>
            ))}
          </ul>
        )}
      </FramePanel>
      {run.status === "reporting" && canWork ? (
        <FrameFooter>
          <Button
            type="button"
            size="sm"
            disabled={waiting > 0 || close.pending}
            onClick={() => {
              void close.run({ inspectionId: run.id });
            }}
          >
            {t("Close inspection")}
          </Button>
        </FrameFooter>
      ) : null}
    </Frame>
  );
}
```

## Prompt

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

### Web

```text
Build "Inspection run" in a React web app that uses shadcn/ui components. One inspection with its status, checklist and team, its findings with a Raise case button for each nonconformity, and a Close button once every one has its case.

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