fieldboard
v0.1.0
Published
A dispatch board for field service work — resource timeline, unassigned backlog, travel-aware scheduling, and risk detection for React.
Downloads
28
Maintainers
Readme
Fieldboard
A dispatch board for field service work — the kind where a crew has to physically drive to the job.
Generic resource schedulers can draw a timeline. None of them know that your plumber can't be in two suburbs at once, or that the person you just assigned isn't ticketed for the work. Fieldboard does.
import { DispatchBoard } from "fieldboard";
import "fieldboard/styles.css";
<DispatchBoard technicians={crew} jobs={jobs} onJobsChange={setJobs} />;The stylesheet is prebuilt — you do not need Tailwind in your project to use the board. To avoid a flash of the wrong theme, run the theme script before first paint:
import { THEME_SCRIPT } from "fieldboard";
<script dangerouslySetInnerHTML={{ __html: THEME_SCRIPT }} />;MIT licensed. React 19, Tailwind v4, no backend required.
What makes it a field service board
| | |
|---|---|
| Travel-aware scheduling | Jobs know where they are. The board won't offer a slot the crew can't drive to in time, and flags one that's already impossible. |
| Skill matching | A technician holds trades. Assigning work they aren't ticketed for is flagged — never blocked, because dispatchers override for good reasons. |
| An unassigned backlog | The tray is a first-class surface, not an afterthought. Virtualised, so 500 waiting jobs stay fast. |
| Risk detection | Double-booked, late, unreachable, unticketed, outside shift — ranked, so a block shows the worst problem rather than all of them. |
| Shift boundaries | Working hours are a real constraint, not decoration. Per-technician capacity bars show who has room. |
| Bulk assignment | "Fill this technician's day" places as many selected jobs as actually fit and tells you which didn't. |
| Knows what you may not do | readOnly and canEdit remove the affordances rather than greying them, so nothing is offered that would only fail later. |
| Speaks your words | Every string is replaceable through messages, as whole sentences with named holes — so a translation can reorder them, not just swap them. Counts use Intl.PluralRules. |
| Reads in your locale | Times, hours and durations follow the locale you pass — 16:30 in Berlin, 4:30p in Denver — with the full form reserved for screen readers. |
| Built for two dispatchers | Every change is emitted as an intent your server can reconcile, external edits are announced, and a drag whose job moved underneath is refused. |
| Undo that names itself | Every change is reversible and labelled — "Scheduled 4 jobs with Cole Vargas · Undo". ⌘Z, multi-step, with redo. |
Installing
npm install fieldboardThe data you provide
type Tech = {
id: string;
name: string;
initials: string;
skills: Trade[]; // every trade they're ticketed for
shiftStartMin: number; // minutes from midnight — 480 is 08:00
shiftEndMin: number;
};
type Job = {
id: string;
ref: string;
customer: string;
address: string;
lat: number; // used for travel time and the map
lng: number;
durationMin: number;
trade: Trade;
urgent: boolean;
techId: string | null; // null means it's in the tray
startMin: number | null;
dayOffset: number | null; // days from today; null while unassigned
state: "planned" | "active" | "done";
};Props
| Prop | Type | Notes |
|---|---|---|
| technicians | Tech[] | required |
| jobs | Job[] | controlled mode; pair with onJobsChange |
| onJobsChange | (jobs: Job[]) => void | |
| defaultJobs | Job[] | uncontrolled mode instead of jobs |
| travel | TravelEstimator | see below |
| trends | { unassigned, atRisk, late, unscheduledHours } | seven days of history for the KPI deltas |
Travel time
By default the board estimates travel from straight-line distance, inflated for real roads:
createTravelEstimator({
kmPerHour: 32, // average urban speed
detourFactor: 1.3, // nobody drives in a straight line
minimumMin: 5, // parking, finding the door, signing in
});That works with no API key. For real road distances, pass your own — the board never computes travel itself:
<DispatchBoard travel={(from, to) => myRoutingService(from, to)} … />Risk ranking
A job shows one problem: the worst one. In order —
unassigned— sitting in the traylate— should have started, hasn't (today only)overlap— double-booked, costs you two jobsunreachable— can't drive there in time; slips everything afterskill-mismatch— not ticketed for the tradeoutside-shift— overtime, expensive but the work still happens
Theming
Three themes ship — Ivory, Cabin, Emerald — set with data-theme on <html>.
Everything is CSS custom properties, so a fourth is one block:
[data-theme="yours"] {
--void: …; /* canvas */
--slate: …; /* chrome */
--drawer: …; /* the tray, a plane above the board */
--brass: …; /* selection and focus */
--hivis: …; /* at risk */
--late: …; /* late */
}Colour means trouble and nothing else. A healthy board is grey.
Using the scheduling logic on its own
Every rule is a pure function you can call server-side:
import { riskOf, earliestFreeSlot, canDo } from "fieldboard";
const slot = earliestFreeSlot(tech, job, jobs, { travel, notBefore: now });
const problem = riskOf(job, { jobs, techs, nowMin: now });Development
This is an npm workspace:
packages/fieldboard the library — what npm install gives you
apps/demo the example app in the screenshotsnpm install
npm run build:lib # build the package (tsc + Tailwind CSS)
npm run dev # demo at http://localhost:3111
npm test # 48 tests over the scheduling logic
npm run typecheckHow this project is built
Fieldboard follows the Human-Centric Product Harness — an executable
process kept in .ai/. It defines the product context, the design
system, the review roles, and the gates a change must pass.
Its central rule:
A change that compiles, type-checks and passes tests is not done until the intended human can perceive the situation, understand it, act confidently, see what happened, and remain in control.
Contributors and coding agents should start at .ai/README.md.
The harness/ and policies/ directories are project-agnostic and can be
lifted into another codebase.
Not built yet
Honest list, so nobody adopts this expecting more than it does:
- Address geocoding. A job created through the form has no coordinates, so
travel to it can't be estimated until you set
lat/lng. - Calendar view. The tab exists and says so.
- Recurring jobs, multi-day jobs, SLA windows, overtime rules.
- Persistence. The board is a controlled component; storage is yours.
- Translations. Fieldboard ships English and the means to replace every word of it. It bundles no other language, because it could not verify or maintain one.
- Authorisation.
readOnlyandcanEditshape what the board offers. They are not a security boundary and cannot be — a removed button is not a permission. Your server must enforce the same rules against the same user. - Conflict resolution. The board reports conflicts and gives you the intent to reconcile them. Merging is your server's job — it has one, the board does not.
Licence
MIT — see LICENSE.
