# Inventory table

> Stock levels per product and location, with a form to adjust a count.

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

## Install the block

```bash
npx better-supabase add inventory
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add alert badge button card empty skeleton table
```

From `apps/examples/nextjs/src/features/shop/components/inventory-table.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/shop/components/inventory-table.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>`: stock per tracked variant and active location. */
export async function InventoryTable() {
  const session = await getSession();

  if (!can(session, "inventory.read")) {
    forbidden();
  }

  const [stock, t] = await Promise.all([
    getStockLevels(),
    getExtracted("shop"),
  ]);

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

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

  const locationName = new Map(
    stock.kind === "ok"
      ? stock.data.locations.map((location) => [location.id, location.name])
      : [],
  );

  return (
    <Card data-recipe="inventory-table">
      <CardHeader>
        <CardTitle>{t("Stock levels")}</CardTitle>
        <CardDescription>
          {t(
            "Units on hand, reserved by open orders and available to sell, per location.",
          )}
        </CardDescription>
      </CardHeader>
      <CardContent>
        {stock.kind === "error" ? (
          <Alert variant="destructive">
            <TriangleAlertIcon />
            <AlertTitle>{t("The stock levels 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>
        ) : stock.data.locations.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <WarehouseIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No stock locations yet")}</EmptyTitle>
              <EmptyDescription>
                {t("Stock is counted per location. Add one to start.")}
              </EmptyDescription>
            </EmptyHeader>
            {canManage ? (
              <EmptyContent>
                <CreateLocationButton />
              </EmptyContent>
            ) : null}
          </Empty>
        ) : stock.data.levels.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <BoxesIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No products to count")}</EmptyTitle>
              <EmptyDescription>
                {t("Products from the catalog show up here with their stock.")}
              </EmptyDescription>
            </EmptyHeader>
            <EmptyContent>
              <Link href="/shop" className={buttonVariants({ size: "sm" })}>
                {t("Go to the catalog")}
              </Link>
            </EmptyContent>
          </Empty>
        ) : (
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>{t("Product")}</TableHead>
                <TableHead>{t("Location")}</TableHead>
                <TableHead className="text-right">{t("On hand")}</TableHead>
                <TableHead className="text-right">{t("Reserved")}</TableHead>
                <TableHead className="text-right">{t("Available")}</TableHead>
                <TableHead>
                  <span className="sr-only">{t("Status")}</span>
                </TableHead>
                {canManage ? (
                  <TableHead>
                    <span className="sr-only">{t("Actions")}</span>
                  </TableHead>
                ) : null}
              </TableRow>
            </TableHeader>
            <TableBody>
              {stock.data.levels.map((level) => {
                const location = locationName.get(level.locationId) ?? "";

                return (
                  <TableRow key={`${level.variantId}:${level.locationId}`}>
                    <TableCell>
                      <div className="flex flex-col gap-1">
                        <span className="text-sm">{level.productTitle}</span>
                        <span className="font-mono text-xs text-muted-foreground">
                          {level.sku}
                        </span>
                      </div>
                    </TableCell>
                    <TableCell>{location}</TableCell>
                    <TableCell className="text-right tabular-nums">
                      {level.onHand}
                    </TableCell>
                    <TableCell className="text-right tabular-nums">
                      {level.reserved}
                    </TableCell>
                    <TableCell className="text-right tabular-nums">
                      {level.available}
                    </TableCell>
                    <TableCell>
                      {level.available <= 0 ? (
                        <Badge variant="destructive-light">
                          {t("Out of stock")}
                        </Badge>
                      ) : level.reorderPoint !== null &&
                        level.available <= level.reorderPoint ? (
                        <Badge variant="warning-light">{t("Low")}</Badge>
                      ) : (
                        <Badge variant="success-light">{t("In stock")}</Badge>
                      )}
                    </TableCell>
                    {canManage ? (
                      <TableCell className="text-right">
                        <AdjustStockDialog
                          variantId={level.variantId}
                          locationId={level.locationId}
                          label={`${level.productTitle} (${level.sku}), ${location}`}
                        />
                      </TableCell>
                    ) : null}
                  </TableRow>
                );
              })}
            </TableBody>
          </Table>
        )}
      </CardContent>
    </Card>
  );
}
```

## Prompt

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

### Web

```text
Build "Inventory table" in a React web app that uses shadcn/ui components. Stock levels per product and location, with a form to adjust a count.

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