Lesson 6 — The dispatcher’s dashboard
Lessons 4 and 5 built the dispatcher’s map: scoped to one depot’s region, layered with SAs, jobs, and FOs, organised into clean composition methods. The map answers what’s happening in my region right now?
A real dispatch deck doesn’t show only a map, though. The dispatcher needs more than spatial awareness — they need to know how many jobs are pending, what’s scheduled today, who’s going where. The map shows shape; the panels around it carry the numbers and the lists that drive action.
This lesson wraps the map in the rest of the dashboard. Two parts:
- A stats strip above the map — four tiles narrating the day’s job lifecycle from pending through to complete.
- A sidebar to the right of the map — pending jobs at the top (the action queue), today’s schedule below (what’s booked).
No new map code. The work is on the data layer (one service object handles all the panels) and the layout (composition).
Part 1 — The stats panel
Above the map, a strip of four tiles narrating the day’s job lifecycle: pending, scheduled today, in progress, complete today. The four counts read left to right as a job’s day.
The data service. Lesson 4 sketched
DispatcherDashboardData as an empty hash. Now it earns its
keep. Update app/services/dispatcher_dashboard_data.rb:
|
|
The four counts are scoped to the dispatcher’s region by the
same service_area_id IN (...) shape we used for the jobs
on the map. “Today” filters use Date.current.all_day,
which expands to a range covering midnight-to-midnight in
the application timezone — exactly what “scheduled today”
or “complete today” should mean.
Each Job.where(...).count is a single SQL COUNT(*)
query. Four queries per dashboard load is fine; we’re not
yet at the scale where this would matter. Module 7
introduces aggregation patterns that compute many counts in
one query, useful when the panel grows or the regions grow.
Wire the service into the controller. Update
app/controllers/dashboard_controller.rb:
|
|
Update the view to pass the data. In
app/views/dashboard/index.html.erb:
|
|
Update the page component to take data: as a prop and
render a stat strip above the map. In
app/components/dashboards/dispatcher_dashboard.rb:
|
|
StatTile is the same component the manager dashboard uses
in Module 4 — same visual language, same prop shape. The
dispatcher’s stats panel reuses Module 4’s tile rather than
inventing a new one; visual consistency across roles is part
of what makes the dashboard family feel like one product.
Look what we just did. Refresh the dashboard. Above the map, a strip of four tiles. The day’s lifecycle reads left to right: how many jobs need assigning, how many are booked, how many are running, how many are done.
Part 2 — The pending jobs sidebar section
The first sidebar section: pending jobs ordered by age. The oldest sits at the top — that’s what’s been waiting longest, which is what the dispatcher should look at first.
Extend the data service with a pending section:
|
|
The pending section returns both a count and the six oldest entries. The count drives the section header (“43 total — showing 6”); the six entries render the list itself. The dispatcher gets the truth about how big the queue is, even when only a slice fits on screen.
The total: count is a separate SQL query from the items:
fetch — both running through the same WHERE clause. We
could fetch the items and call .size on the array, but
that conflates “what’s the total” with “give me the first
six,” and the count would silently start meaning “either 6
or fewer.” Two queries makes the two questions explicit.
The component. Create
app/components/pending_jobs_list.rb:
|
|
Oldest first — the longest-waiting job sits at the top, since that’s what needs action soonest. The amber pill matches the map’s pending colour, so the side panel and the map speak the same visual language: amber means “needs assignment” whether it’s a circle on the map or a pill in the list.
Update the page component to add the side panel alongside the map:
|
|
The map and sidebar sit side by side inside the same rounded
container — visually they read as one composition. The
sidebar is fixed-width (w-96), the map fills the rest. On
narrow screens the sidebar would feel cramped; Module 7
addresses responsive behaviour.
Look what we just did. Refresh. To the right of the map, a column with the pending jobs section. The header says how many are pending across the whole region, the list shows the six oldest. If a dispatcher’s region has fewer than six pending jobs, the header says “5 total” or similar — no lie about “showing 6”.
Part 3 — Today’s schedule sidebar section
The second sidebar section: jobs scheduled for today, ordered by time, with the assigned FO shown.
Extend the data service:
|
|
Earliest first — the next thing to happen sits at the top, ordered by the time each job’s booked for.
The component. Create
app/components/todays_schedule_list.rb:
|
|
The schedule reads as a timeline of the day. Each row shows the customer, the time they’re booked for, and which FO will handle it. No status pill — the section heading already tells you these are scheduled.
Update the side panel to include both sections:
|
|
Look what we just did. Refresh. The side panel now has two sections: pending jobs at the top, today’s schedule below. The dispatcher’s day reads top to bottom — what needs assigning, then what’s already booked.
The dashboard is complete.
Activity 1 — Switch dispatchers, watch the panels reshape
Sign in as different dispatchers. The map reshapes, but so
does everything else: the depot name in the header, the four
counts in the strip, the pending list, the day’s schedule.
Every panel scopes to the current user’s depot through the
same service_area_id IN (...) filter.
The Brisbane dispatcher might have 23 pending jobs and 18 scheduled for today; the Hobart dispatcher might have 4 pending and 6 scheduled. Same code, completely different operational picture.
Activity 2 — Add a depot with no scheduled jobs
In the console:
|
|
Sign in as the Darwin dispatcher. The “Today’s schedule” section now shows “Nothing scheduled today.” — the empty state we built into the component handles the case gracefully. The page doesn’t break; the user gets clear information about why the section is empty.
Where this leaves us
The dispatcher’s dashboard is end-to-end. A header with the depot name, a stats strip narrating the day, a regional map showing the operation, a sidebar surfacing what needs action. Every panel reads from the same data service, scoped through the same FK to the current user’s depot, presented through the same shared design language as the manager’s dashboard.
Two patterns from this lesson worth keeping:
- One data service per dashboard.
DispatcherDashboardDataloads everything the page needs — counts, pending jobs, scheduled jobs — in one place. Each panel reads from@data[:something]rather than fetching its own data. This keeps the controller thin and the page component declarative. - Empty states are first-class. Every list component has an “if there are no items” branch. Real data is uneven — small depots, quiet days, weekends — and the dashboard has to look right under all of those conditions, not just when the demo data is full.
The next lesson closes the role coverage with the field officer’s dashboard — single SA, today’s queue, the simplest of the three.