# Order state board

> Today's kitchen orders in preparation-state columns; dragging an order forward moves all of its lines.

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

## Install the block

```bash
npx better-supabase add orders
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add badge card @reui/kanban
```

From `apps/examples/nextjs/src/features/shop/components/order-state-board.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/shop/components/order-state-board.tsx):

```tsx
/**
 * The orders sent to the kitchen today, in a column for the state of their
 * least advanced line. Dragging a card to a later column moves every line of
 * the order up to that state; a move back or within a column puts the card
 * back. The parent keys it by the cards' states, so a refresh starts it from
 * the saved board.
 */
export function OrderStateBoard({
  cards,
  canPrepare,
}: {
  cards: readonly BoardCardView[];
  canPrepare: boolean;
}) {
  const t = useExtracted("shop");
  const errorMessage = useErrorMessage();
  const label = usePreparationLabel();
  const [columns, setColumns] = useState(() => columnsOf(cards));
  const bump = useAction(bumpTicket);
  const byId = new Map(cards.map((card) => [card.orderId, card]));

  return (
    <div data-recipe="order-state-board">
      <Kanban
        value={columns}
        onValueChange={setColumns}
        getItemValue={(card) => card.orderId}
        onValueCommit={(_value, meta) => {
          if (meta.kind !== "item") {
            return;
          }

          const from = v.parse(StateSchema, meta.activeContainer);
          const to = v.parse(StateSchema, meta.overContainer);

          if (
            PREPARATION_STATES.indexOf(to) <= PREPARATION_STATES.indexOf(from)
          ) {
            setColumns(meta.previousValue);

            if (from !== to) {
              toast.warning(t("An order only moves forward on this board."));
            }

            return;
          }

          void bump
            .run({
              orderId: String(meta.event.active.id),
              state: to,
              stationId: null,
            })
            .then((result) => {
              if (!result.ok) {
                setColumns(meta.previousValue);
                toast.error(errorMessage(result.error));
              }
            });
        }}
      >
        <KanbanBoard className="flex overflow-x-auto">
          {PREPARATION_STATES.map((state) => {
            const column = columns[state] ?? [];
            const count = column.length;

            return (
              <KanbanColumn
                key={state}
                value={state}
                className="w-72 shrink-0 gap-3 rounded-lg bg-muted/50 p-3"
              >
                <div className="flex items-center justify-between gap-2">
                  <Badge variant={preparationBadgeVariant(state)}>
                    {label(state)}
                  </Badge>
                  <Badge variant="outline">{count}</Badge>
                </div>
                <KanbanColumnContent value={state} className="min-h-16">
                  {column.map((card) => (
                    <KanbanItem
                      key={card.orderId}
                      value={card.orderId}
                      disabled={!canPrepare || bump.pending}
                    >
                      <KanbanItemHandle cursor={canPrepare}>
                        <OrderCard card={card} />
                      </KanbanItemHandle>
                    </KanbanItem>
                  ))}
                  {count === 0 ? (
                    <p className="text-xs text-muted-foreground">
                      {t("No orders here.")}
                    </p>
                  ) : null}
                </KanbanColumnContent>
              </KanbanColumn>
            );
          })}
        </KanbanBoard>
        <KanbanOverlay>
          {({ value, variant }) => {
            const card =
              variant === "item" ? byId.get(String(value)) : undefined;

            return card ? <OrderCard card={card} /> : null;
          }}
        </KanbanOverlay>
      </Kanban>
    </div>
  );
}
```

## Prompt

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

### Web

```text
Build "Order state board" in a React web app that uses shadcn/ui components. Today's kitchen orders in preparation-state columns; dragging an order forward moves all of its lines.

It uses the better-supabase `orders` block (Orders). 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 orders` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add badge card @reui/kanban`.
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/order-state-board.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/order-state-board.md
- Block docs: https://bettersupabase.com/docs/blocks/orders
```
