Order state board
Today's kitchen orders in preparation-state columns; dragging an order forward moves all of its lines. It loads its data on the server through the block's client.
pnpm dev:example) to see it.Add the components
terminal npx shadcn add badge card @reui/kanbanInstall the glue
Install the block with
npx better-supabase add orders. The glue items are copied into your app, so edit them as you need.None: the page calls the block's client on the server.
Write the component
React and shadcn
View in the exampleorder-state-board.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> ); }