# Case board

> The open cases in stage columns, reported to verifying; dragging a card moves the case to that stage of its kind's pipeline.

Source: https://bettersupabase.com/docs/registry/ui-recipes/case-board

{/* 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 [cases block](https://bettersupabase.com/blocks/cases) must be installed (`npx better-supabase add cases`). Its page on bettersupabase.com is [/recipes/case-board](https://bettersupabase.com/recipes/case-board).

Web, with shadcn components:

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

```tsx title="apps/examples/nextjs/src/features/cases/components/case-board.tsx"
/**
 * The organization's open cases in a column for their stage. Dragging a card to
 * another column moves the case there; a failed move puts it back. The parent
 * keys it by the cards' stages, so a refresh starts it from the saved board.
 */
export function CaseBoard({
  data,
  canMove,
}: {
  data: CaseBoardData;
  canMove: boolean;
}) {
  const errorMessage = useErrorMessage();
  const label = useStageLabel();
  const [columns, setColumns] = useState(() => columnsOf(data.cards));
  const move = useAction(moveCase);
  const byId = new Map(data.cards.map((card) => [card.id, card]));

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

          const from = v.parse(StageSchema, meta.activeContainer);
          const to = v.parse(StageSchema, meta.overContainer);
          const card = byId.get(String(meta.event.active.id));

          const stageId =
            card === undefined ? undefined : data.stages[card.pipelineId]?.[to];

          if (from === to || card === undefined || stageId === undefined) {
            setColumns(meta.previousValue);

            return;
          }

          void move.run({ caseId: card.id, stageId }).then((result) => {
            if (!result.ok) {
              setColumns(meta.previousValue);
              toast.error(errorMessage(result.error));
            }

            return result;
          });
        }}
      >
        <KanbanBoard className="flex overflow-x-auto">
          {OPEN_STAGES.map((stage) => {
            const column = columns[stage] ?? [];

            return (
              <KanbanColumn
                key={stage}
                value={stage}
                className="w-72 shrink-0 gap-3 rounded-lg bg-muted/50 p-3"
              >
                <div className="flex items-center justify-between gap-2">
                  <span className="text-sm font-medium">{label(stage)}</span>
                  <Badge variant="outline">{column.length}</Badge>
                </div>
                <KanbanColumnContent value={stage} className="min-h-16">
                  {column.map((card) => (
                    <KanbanItem
                      key={card.id}
                      value={card.id}
                      disabled={!canMove || move.pending}
                    >
                      <KanbanItemHandle cursor={canMove}>
                        <CaseItem card={card} />
                      </KanbanItemHandle>
                    </KanbanItem>
                  ))}
                </KanbanColumnContent>
              </KanbanColumn>
            );
          })}
        </KanbanBoard>
        <KanbanOverlay>
          {({ value, variant }) => {
            const card =
              variant === "item" ? byId.get(String(value)) : undefined;

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