Skip to content

Field service dispatch

Technicians are the rows and the day view is the board. Jobs wait in a queue, and each button books the earliest slot any technician has, inside their shift and around lunch, training and the jobs they already have. Booked jobs can be dragged to another technician or resized.

Three kinds of data, only two of which the scheduler sees:

import type { ResourceInput, SchedulerEvent } from "resource-scheduler";
// Technicians: a shift and the breaks inside it.
const technicians: ResourceInput[] = [
{
id: "ann",
name: "Ann Lee",
businessHours: { daysOfWeek: [0, 1, 2, 3, 4, 5, 6], startHour: 7, endHour: 15 },
unavailable: [{ start: new Date(2026, 9, 7, 12), end: new Date(2026, 9, 7, 12, 30) }], // lunch
},
// ...
];
// Booked jobs: a flat list, one row per job.
const jobs: SchedulerEvent[] = [
{ id: "j1", title: "Boiler service", resourceId: "ann", startDate: new Date(2026, 9, 7, 8), endDate: new Date(2026, 9, 7, 9, 30) },
];
// Jobs waiting for a technician: your own list, not in the scheduler.
const queue = [{ id: "q1", title: "No heat, 4 Oak Ave", minutes: 90 }];

A job leaves the queue and becomes an event when somebody is chosen. In a database that is a jobs table with a nullable technician_id, starts_at and ends_at: rows without a technician are the queue.

You want Use
A technician’s shift businessHours, per technician, so shifts can differ
Lunch, training, a van in the garage unavailable ranges, shaded in the day view
Jobs that cannot overlap or touch the grey eventOverlap={false} and blockUnavailable
The technician who should take a job suggestResources: earliest slot, or the least booked
How full each technician’s shift is showUtilization (7.5 of 8 hours, minus lunch)
Where “now” is on the board nowIndicator
Jump to the booked slot the goTo method of the ref

suggestResources applies the same rules the grid shows: the technician’s jobs, their unavailable time and their shift. It asks every technician and ranks the answers. The default is the earliest slot, and limit: 1 keeps the winner:

import { addDays } from "date-fns";
import { suggestResources, withEvents } from "resource-scheduler";
const assign = (job: { id: string; title: string; minutes: number }) => {
const [best] = suggestResources(
withEvents(technicians, jobs),
{
from: new Date(), // never in the past
to: addDays(new Date(), 3), // a slot must end by then
duration: job.minutes, // starts on the half hour (the default step)
},
{ limit: 1 }
);
if (!best) return alert("Nobody can take it in the next three days");
setJobs((all) => [...all, { id: crypto.randomUUID(), title: job.title, resourceId: best.resourceId, startDate: best.start, endDate: best.end }]);
ref.current?.goTo(best.start); // show the day it landed on
};

Suggestions are free slots only: the candidate’s whole length has to fit inside one shift and miss every other job and break. See Suggest who takes a job for the strategies and the other options.

  • Travel time. Book job.minutes + travel in the search, and draw the event for the on-site part only, or keep the travel as its own event so it shows on the board.
  • Skills and territories. Keep them in your own data and pass the technicians who qualify: { resourceIds: technicians.filter((t) => skills[t.id].includes(job.skill)).map((t) => t.id) }.
  • Fairness. To spread work out, use strategy: "least-booked", which ranks by the share of each technician’s shift that is already booked, or a function that mixes start time and utilization.
  • Check on the server. Two dispatchers can book the same technician at once. Run suggestResources with the fixed time { start, end }, or isPlacementAllowed, in your API against the saved jobs before committing, and tell the second dispatcher to pick again.
  • Load only the day on screen. Use onRangeChange to fetch the jobs and breaks for the visible range, and withEvents to merge them when you search.