# Dispatch board

> A week of technicians by day with their scheduled work orders, and the backlog to schedule.

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

## Install the block

```bash
npx better-supabase add dispatch
```

## Web (Next.js)

Add the shadcn components:

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

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

```tsx
/**
 * Render inside `<Suspense>`: a week of technicians by day with their scheduled
 * work orders, and the unscheduled backlog below. `?week=YYYY-MM-DD` picks the
 * week.
 */
export async function DispatchBoard({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  const [session, t, format, query] = await Promise.all([
    getSession(),
    getExtracted("field"),
    getFormatter(),
    searchParams,
    connection(),
  ]);

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

  const manage = can(session, "dispatch.manage");
  const monday = mondayOf(query["week"]);
  const board = await settle(() => loadDispatchWeek(monday.toString()));

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

  const dayLabel = (date: string) =>
    format.dateTime(new Date(`${date}T12:00:00Z`), {
      weekday: "short",
      day: "numeric",
      timeZone: "UTC",
    });

  const range = format.dateTimeRange(
    new Date(`${monday.toString()}T12:00:00Z`),
    new Date(`${monday.add({ days: 6 }).toString()}T12:00:00Z`),
    { day: "numeric", month: "short", year: "numeric", timeZone: "UTC" },
  );

  const newWorkOrder = manage ? <NewWorkOrderDialog /> : null;

  return (
    <div data-recipe="dispatch-board" className="flex flex-col gap-6">
      <Card>
        <CardHeader>
          <CardTitle>{t("Week of {range}", { range })}</CardTitle>
          <CardDescription>
            {board.kind === "ok"
              ? t("Scheduled work per technician, in {zone}.", {
                  zone: board.data.timeZone,
                })
              : t("Scheduled work per technician.")}
          </CardDescription>
          <CardAction className="flex items-center gap-2">
            <Link
              href={`/field/dispatch?week=${monday.subtract({ weeks: 1 }).toString()}`}
              className={buttonVariants({
                variant: "outline",
                size: "icon-sm",
              })}
              aria-label={t("Previous week")}
            >
              <ChevronLeftIcon />
            </Link>
            <Link
              href={`/field/dispatch?week=${monday.add({ weeks: 1 }).toString()}`}
              className={buttonVariants({
                variant: "outline",
                size: "icon-sm",
              })}
              aria-label={t("Next week")}
            >
              <ChevronRightIcon />
            </Link>
          </CardAction>
        </CardHeader>
        <CardContent>
          {board.kind === "error" ? (
            <FieldLoadError title={t("The schedule board didn't load")} />
          ) : board.data.rows.length === 0 ? (
            <Empty className="border">
              <EmptyHeader>
                <EmptyMedia variant="icon">
                  <CalendarRangeIcon />
                </EmptyMedia>
                <EmptyTitle>{t("Nothing scheduled this week")}</EmptyTitle>
                <EmptyDescription>
                  {t("Work orders show here once they have a technician.")}
                </EmptyDescription>
              </EmptyHeader>
              <EmptyContent>
                <Link
                  href={`/field/dispatch?week=${monday.add({ weeks: 1 }).toString()}`}
                  className={buttonVariants({ variant: "outline" })}
                >
                  {t("Show next week")}
                </Link>
              </EmptyContent>
            </Empty>
          ) : (
            <Table>
              <TableHeader>
                <TableRow>
                  <TableHead>{t("Technician")}</TableHead>
                  {board.data.days.map((day) => (
                    <TableHead key={day}>{dayLabel(day)}</TableHead>
                  ))}
                </TableRow>
              </TableHeader>
              <TableBody>
                {board.data.rows.map((row) => (
                  <TableRow key={row.technicianId}>
                    <TableCell className="align-top text-sm font-medium">
                      {row.name}
                    </TableCell>
                    {row.days.map(({ date, orders }) => (
                      <TableCell key={date} className="align-top">
                        <div className="flex flex-col gap-2">
                          {orders.map((order) => (
                            <div key={order.id} className="flex flex-col gap-1">
                              <span className="text-sm font-medium">
                                {order.title}
                              </span>
                              <span className="text-xs text-muted-foreground tabular-nums">
                                {order.startsAt
                                  ? format.dateTime(new Date(order.startsAt), {
                                      timeStyle: "short",
                                      timeZone: board.data.timeZone,
                                    })
                                  : null}
                              </span>
                              <WorkOrderStatusBadge status={order.status} />
                            </div>
                          ))}
                        </div>
                      </TableCell>
                    ))}
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          )}
        </CardContent>
      </Card>
      <Card>
        <CardHeader>
          <CardTitle>{t("Backlog")}</CardTitle>
          <CardDescription>
            {t("Work orders without a slot, most urgent first.")}
          </CardDescription>
          {newWorkOrder ? <CardAction>{newWorkOrder}</CardAction> : null}
        </CardHeader>
        <CardContent>
          {board.kind === "error" ? (
            <FieldLoadError title={t("The backlog didn't load")} />
          ) : board.data.backlog.length === 0 ? (
            <Empty className="border">
              <EmptyHeader>
                <EmptyMedia variant="icon">
                  <InboxIcon />
                </EmptyMedia>
                <EmptyTitle>{t("The backlog is empty")}</EmptyTitle>
                <EmptyDescription>
                  {t("Every open work order has a slot.")}
                </EmptyDescription>
              </EmptyHeader>
              {newWorkOrder ? (
                <EmptyContent>{newWorkOrder}</EmptyContent>
              ) : null}
            </Empty>
          ) : (
            <Table>
              <TableHeader>
                <TableRow>
                  <TableHead>{t("Job")}</TableHead>
                  <TableHead>{t("Address")}</TableHead>
                  <TableHead>{t("Priority")}</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {board.data.backlog.map((order) => (
                  <TableRow key={order.id}>
                    <TableCell className="text-sm font-medium">
                      {order.title}
                    </TableCell>
                    <TableCell className="text-sm text-muted-foreground">
                      {order.address ?? ""}
                    </TableCell>
                    <TableCell>
                      <WorkOrderPriorityBadge priority={order.priority} />
                    </TableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          )}
        </CardContent>
      </Card>
    </div>
  );
}
```

## Prompt

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

### Web

```text
Build "Dispatch board" in a React web app that uses shadcn/ui components. A week of technicians by day with their scheduled work orders, and the backlog to schedule.

It uses the better-supabase `dispatch` block (Dispatch). 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 dispatch` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add alert badge button card dialog empty field input skeleton table`.
3. The recipe needs no glue: it calls the block's client on the server.
4. Write the screen. Load the data on the server with the block's client, starting from the recipe's snippet (`apps/examples/nextjs/src/features/field/components/dispatch-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/dispatch-board.md
- Block docs: https://bettersupabase.com/docs/blocks/dispatch
```
