Lesson 7 — The field officer dashboard
The manager works the whole country. The dispatcher works their depot’s region. The field officer works one SA3 — the patch they’re posted to, where their jobs live and where their day plays out.
This is the simplest dashboard of the three. One SA, one list of today’s jobs, no FO layer (the FO is the user — no sense putting a single truck on a map of one SA). The point of the lesson is to show the role-scoping idea applies cleanly across roles, with each scope smaller than the last.
Three scopes, one pattern
By the end of this module the tutorial has built three dashboards with the same shape:
- Manager — national scope, 340 SAs, 9 depots
- Dispatcher — regional scope, ~38 SAs, 1 depot
- Field officer — local scope, 1 SA, today only
Each filters the same models (Job, ServiceArea) by a
different boundary derived from the user. The plumbing is
identical:
- A service object that takes the user, scopes the data, and returns either a hash (for dashboards) or a GeoJSON FeatureCollection (for map sources)
- A controller action that calls the service and renders
- A page or map component that reads the result
What changes between roles is the scoping clause. The
manager’s job count uses no scope. The dispatcher’s uses
service_area_id IN (depot.service_areas). The FO’s uses
service_area_id = current_user.service_area_id and
filters by today.
Part 1 — The local map
The FO’s map is small: their assigned SA3, with the day’s jobs as circles. No SA outline (only one polygon — its boundary doesn’t help orient them; it would just box the view), no FO pin (they’re the user). Just the SA’s shape and their jobs.
The service object. Create
app/services/local_jobs.rb:
|
|
Two narrowings compared to RegionalJobs:
service_area_id = user.service_area_id— single SA rather than the depot’s set. TheUsermodel hasservice_area_iddirectly (per the schema we built in Module 5 Lesson 1); the FO is “posted” to one SA at a time.- Today filter —
scheduled_for IS NULL OR scheduled_for::date = ?. Pending jobs (noscheduled_for) and jobs scheduled for today both belong on the FO’s queue. Anything scheduled for tomorrow or later isn’t their problem yet.
The SA boundary. Create
app/services/local_service_area.rb:
|
|
A FeatureCollection with a single feature. Same encoding pattern as the regional service; just one polygon.
The controllers. Add local actions to the existing
JobsController and ServiceAreasController:
|
|
|
|
Routes:
|
|
The URL hierarchy continues: /something/regional for
dispatcher scope, /something/local for FO scope. National
routes drop the prefix entirely (e.g. /service_areas for
the manager’s whole-country set).
The map component. Create
app/components/local_map.rb:
|
|
Same add_* pattern from Lesson 5. Two features (SA
boundary, jobs), two methods, three lines in
view_template.
The SA outline is #94a3b8 (slate-400) instead of
#dc2626 (red). At regional scope the dispatcher needs the
red outline to distinguish 38 polygons from each other.
At local scope the FO has one polygon — it doesn’t need
to fight for attention. A subtle slate boundary keeps the
viewport oriented without dominating.
fit_bounds: { padding: 40 } gives more breathing room
than the dispatcher’s regional map; for a single SA the
padding controls how much “negative space” surrounds the
polygon at the default zoom.
Part 2 — The dashboard data service
Same pattern as the dispatcher’s:
DispatcherDashboardData.call(user). Build
app/services/field_officer_dashboard_data.rb:
|
|
The “today’s queue” section is the FO’s whole working day, ordered by status — in-progress first (what they’re doing right now), scheduled second (what’s coming up), pending last (the catch-all). Within each group, scheduled jobs order by their booking time.
The page component. Create
app/components/dashboards/field_officer_dashboard.rb:
|
|
The page header carries the SA name — “Brisbane Inner”, “Toowong - West End”, whatever the FO’s posting is. That single piece of context is what the FO needs to ground themselves.
The today’s queue list. Create
app/components/todays_queue_list.rb:
|
|
Same shape as the dispatcher’s lists — header with count, list of items, empty state. The status pill carries the same colour vocabulary as the map circles, so the FO can glance at the list and the map and read both in one language.
Wire up the controller. Update
app/controllers/dashboard_controller.rb:
|
|
The controller now handles all three roles. Each case
arm is one line; both the case statements stay legible
even as the application grows.
Look what we just did. Sign out, sign in as a field
officer (any of the seeded FO accounts will work). Visit
/dashboard. You’ll see:
- A page header naming your SA
- A stats strip showing pending, scheduled, in progress, complete-today counts (all scoped to your single SA)
- A map zoomed to your SA, with job circles inside its boundary
- A sidebar with today’s queue, ordered by status
Each FO sees a different SA. The Brisbane Inner FO sees Brisbane Inner. The Toowong FO sees Toowong. Same code, local data.
Activity 1 — The full role tour
Sign in as one user from each role and visit /dashboard.
Three completely different operational views from the same
controller and the same page-routing logic:
- Manager — nine depots, 340 SAs, full national operation
- Dispatcher — one depot’s region, ~38 SAs, lots of jobs and FOs
- Field officer — one SA, today’s queue
The role-scoping idea has reached its smallest scale. Module 6 will make these scopes interactive — clicking an SA on the manager’s map drills into the dispatcher’s view; clicking a job pin on the dispatcher’s map opens the FO’s local view. For now, the routes are role-static.
Activity 2 — Try a quiet SA
Find an SA where the FO has nothing on their queue today. You can either:
- Look in console for an FO whose SA has no current jobs, or
- Move an FO to a quiet SA temporarily:
|
|
Sign in as that FO. The map shows their SA with no job circles. The today’s queue says “No jobs in your queue today.” The page is silent but useful — they’re not broken; there’s just nothing for them right now.
Where this leaves us
Module 5 closes with three role-scoped dashboards on the same foundation. The plumbing — service object, controller action, page or map component — is identical across roles; what varies is the scope clause.
Two architectural ideas worth carrying forward:
- One scope per role, applied uniformly. Manager sees national, dispatcher sees their depot, FO sees their SA. The data layer never decides scope; the user’s role decides, and every service object respects the same rule. This makes role-based tests possible and predictable.
- The same visual language across scales. Status colours, pill shapes, list layouts, popup styling — the three dashboards share design even though they show different data. A user who’s seen one dashboard can read the others without a manual.
Module 6 introduces real spatial predicates — ST_Contains,
ST_Within, ST_DistanceSphere — and the maps start
answering questions that can only be answered with
geometry. For now the scoping is relational; the
foundation is solid; the dashboards work.