# Dispatch

> Schedule field service work orders into technicians' slots on a board, move them from scheduled to done, plan each technician's day as a route, and record the parts each job used.

Source: https://bettersupabase.com/docs/blocks/dispatch

`dispatch` is the field service desk. A dispatcher creates a work order at an
asset location or an address, puts it in a technician's slot on the schedule
board, and plans the order the technician drives to their jobs. The
technician opens their day plan, marks each job en route, on site and done,
and records the parts they used.

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

```ts
import { createDispatch } from "better-supabase/blocks/dispatch";
```

The module requires `tenant` and `access`. With `assets` a work order and a
part can name an asset location and an asset, with `bookings` a work order
can take its slot from a booking, and with `catalog` and `inventory` a part
can name the product variant and the stock location it came from. With
`outbox` every change records an event, and with `audit` the writes are
audited. Technicians are the organization's members who hold
`dispatch.work`; members hold it by default.

## Tables [#tables]

| Table                  | Holds                                                                                                                                                                                          |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `work_orders`          | One job: `title`, `priority`, `status`, the `technician_id` and the slot (`starts_at`, `ends_at`), the `location_id`, `asset_id`, `booking_id`, record or `address`, and the time of each step |
| `dispatch_routes`      | A technician's plan for one day                                                                                                                                                                |
| `dispatch_route_stops` | The work orders of a route, by `position`                                                                                                                                                      |
| `work_order_parts`     | The parts a job used: `name`, `quantity`, `unit`, and the `asset_id`, `variant_id` or inventory `location_id`                                                                                  |

Every table is read through the functions only. Readers with `dispatch.read`
see every work order, and a technician with `dispatch.work` sees and works
their own. `blocks.dispatch.entities` renames the tables and functions.

## Create and schedule work orders [#create-and-schedule-work-orders]

```ts
const dispatch = createDispatch({ transport, tenant: organizationId });

const order = await dispatch
  .create({
    title: "Service the boiler",
    priority: "high",
    locationId,
    assetId,
  })
  .orThrow();

await dispatch
  .schedule(order.id, {
    technicianId,
    startsAt: Temporal.Instant.from("2030-01-07T09:00:00Z"),
  })
  .orThrow();
```

A work order without a slot waits in the backlog (`unscheduled`). Pass
`technicianId` and `startsAt` to `create`, or a `bookingId` to take the slot
from a booking, to schedule it at once. A slot without `endsAt` lasts
`defaultDuration` (one hour). The database refuses a slot that overlaps
another scheduled, en route or on site work order of the same technician,
under an advisory lock per technician, so two dispatchers can't book the same
hour. Creating, changing and scheduling need `dispatch.manage`.

| Method                                                 | Returns                                                                                              |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `create(input)`                                        | the work order, `unscheduled` or `scheduled`                                                         |
| `update(id, patch)`                                    | the work order with a new title, description, priority or place                                      |
| `schedule(id, slot)`                                   | the work order in the technician's slot; moving it to another technician drops it from the old route |
| `unschedule(id)`                                       | the work order back in the backlog and off its route                                                 |
| `setStatus(id, status, note)`                          | the work order after the step, with `note` as the completion note or the reason                      |
| `get(id)`                                              | the work order, or `null`                                                                            |
| `list({ status, technicianId, subject, from, until })` | the organization's work orders, newest first                                                         |
| `board({ from, to, technicianIds })`                   | the schedule board                                                                                   |
| `technicianDay(day, technicianId)`                     | the technician's day plan                                                                            |
| `planRoute(technicianId, day, ids)`                    | the route with its stops in order                                                                    |
| `recordPart(workOrderId, input)`                       | the recorded part                                                                                    |
| `parts(workOrderId)`                                   | the work order's parts, oldest first                                                                 |
| `removePart(partId)`                                   | `true` when the part was removed                                                                     |

The [dispatch section](/docs/blocks/records) lists a record's work orders on
its page, with `create` and `cancel` writes.

## The status lifecycle [#the-status-lifecycle]

| From                    | To          | Who                            |
| ----------------------- | ----------- | ------------------------------ |
| `scheduled`             | `en_route`  | the technician or a dispatcher |
| `scheduled`, `en_route` | `on_site`   | the technician or a dispatcher |
| `on_site`               | `done`      | the technician or a dispatcher |
| anything not finished   | `cancelled` | a dispatcher                   |

Each step stamps its time (`enRouteAt`, `arrivedAt`, `completedAt`,
`cancelledAt`) and records `work_order.status_changed` with `from` and `to`.
Setting the status a work order already has changes nothing.

## The board and the day plan [#the-board-and-the-day-plan]

```ts
const board = await dispatch
  .board({
    from: Temporal.PlainDate.from("2030-01-07"),
    to: Temporal.PlainDate.from("2030-01-11"),
  })
  .orThrow();

const day = await dispatch
  .technicianDay(Temporal.PlainDate.from("2030-01-07"))
  .orThrow();
```

The board reads 1 to 31 days in the block's `timeZone` (UTC by default). It
returns the scheduled work orders with their `day`, the technicians (the ones
you pass, else the ones with work), and the backlog, most urgent first.

`planRoute` sets the order a technician drives to their work orders on a
day; each must be a live work order of theirs. `technicianDay` returns the
route's stops in that order, then the day's other work orders by start.
`technicianId` defaults to the caller, so a technician opens their own plan.
A dispatcher plans any route; a technician plans their own.

```ts title="better-supabase.config.ts"
blocks: {
  dispatch: { options: { timeZone: "Europe/Amsterdam", defaultDuration: "90 minutes" } },
},
```

## Parts [#parts]

```ts
await dispatch
  .recordPart(order.id, {
    name: "Gasket",
    quantity: 2,
    unit: "pc",
    variantId,
    locationId,
  })
  .orThrow();
```

A part is free text, an asset, or a catalog variant taken from an inventory
location. Recording a part moves no stock: book the movement in the
[inventory](/docs/blocks/inventory) block when the job is done. The
technician on the work order and dispatchers record parts, and the one who
recorded a part can remove it.

## UI recipes [#ui-recipes]

The [`dispatch-board`](/docs/registry/ui-recipes/dispatch-board) UI recipe renders the board
as a kanban with the backlog as the first column and a column per
technician, and [`technician-day`](/docs/registry/ui-recipes/technician-day) renders a technician's route as a list with
the status buttons. Both are in the Next.js example.

## Errors [#errors]

| Hint                                    | Cause                                                                           |
| --------------------------------------- | ------------------------------------------------------------------------------- |
| `DISPATCH_FORBIDDEN`                    | the caller lacks `dispatch.read` or `dispatch.manage`, or can't plan this route |
| `DISPATCH_NOT_FOUND`                    | no work order with this id that the caller manages or works                     |
| `DISPATCH_INVALID`                      | the title isn't 1 to 200 characters, or the priority is unknown                 |
| `DISPATCH_SUBJECT_INVALID`              | only one of the record type and id was passed                                   |
| `DISPATCH_SUBJECT_NOT_FOUND`            | the record doesn't exist in the organization                                    |
| `DISPATCH_TECHNICIAN_REQUIRED`          | a slot was passed without a technician                                          |
| `DISPATCH_TECHNICIAN_INVALID`           | the technician isn't a member with `dispatch.work`                              |
| `DISPATCH_SLOT_INVALID`                 | the slot doesn't end after it starts                                            |
| `DISPATCH_SLOT_TAKEN`                   | the technician already has a work order in that slot                            |
| `DISPATCH_STATUS_INVALID`               | the work order has started, or is cancelled                                     |
| `DISPATCH_TRANSITION_INVALID`           | the step isn't in the lifecycle                                                 |
| `DISPATCH_RANGE_INVALID`                | the board spans more than 31 days                                               |
| `DISPATCH_ROUTE_INVALID`                | a stop repeats, or isn't a live work order of the technician                    |
| `DISPATCH_PART_INVALID`                 | the part has no name or a quantity of zero or less                              |
| `DISPATCH_LOCATION_NOT_FOUND`           | the location doesn't exist in the organization                                  |
| `DISPATCH_ASSET_NOT_FOUND`              | the asset doesn't exist in the organization                                     |
| `DISPATCH_BOOKING_NOT_FOUND`            | the booking doesn't exist in the organization                                   |
| `DISPATCH_VARIANT_NOT_FOUND`            | the product variant doesn't exist in the organization                           |
| `DISPATCH_INVENTORY_LOCATION_NOT_FOUND` | the inventory location doesn't exist in the organization                        |
| `DISPATCH_ASSETS_MISSING`               | a location or asset was named without the assets block                          |
| `DISPATCH_BOOKINGS_MISSING`             | a booking was named without the bookings block                                  |
| `DISPATCH_CATALOG_MISSING`              | a variant was named without the catalog block                                   |
| `DISPATCH_INVENTORY_MISSING`            | an inventory location was named without the inventory block                     |

## Events [#events]

`work_order.created`, `work_order.updated`, `work_order.scheduled`,
`work_order.unscheduled`, `work_order.status_changed`,
`work_order.part_used` and `dispatch_route.planned`. The
[events page](/docs/extending/events) lists their payloads.