Lesson 3 — The manager’s overview
The dashboard route exists. Right now it shows a placeholder. This lesson lands the manager’s version: the national overview of the entire dispatch operation. By the end, the manager signs in to a page showing the operation’s scale, a map covering all 9 depots and 340 service areas, and a side panel with recent activity and busiest regions.
Three new techniques appear together:
- Service objects for view data. A service object produces a hash of everything the dashboard needs. The controller stays thin.
- Phlex page components. Up to now we’ve used Phlex for small reusable units (PageHeader, ServiceAreaMap). Pages themselves have been ERB. The dashboard is the right place to introduce Phlex pages because role-specific variation is hard to express cleanly in ERB.
- Phlex Kits. A Phlex feature that lets components be
called as methods (
PageHeader(...)) instead of withrender Components::PageHeader.new(...). Composition gets much cleaner.
Step 1 — Phlex Kits in the chassis
To call components as methods (PageHeader(...) instead of
render Components::PageHeader.new(...)), Phlex Kits need
enabling.
A Phlex Kit is a module that, when extended with
Phlex::Kit, makes the constants under it callable as methods
inside any class that includes the module. The setup is two
steps: define a namespace module, extend it with Phlex::Kit,
then include that module in the base component class.
Open app/components/base.rb. It currently looks something
like:
|
|
Add the module definition and include directive at the top:
|
|
The module Components; extend Phlex::Kit; end block defines
the namespace and turns it into a kit. The
include Components line on Components::Base is what makes
every component callable as a method inside any subclass.
extend Phlex::Kit directly on Components::Base?
Phlex::Kit is designed to be extended on modules, not
classes. Extending it on a class produces a runtime error.
The pattern above — kit on the namespace module, included by
the base class — is the supported shape.After saving, restart the Rails server. The kit machinery loads at boot, and components become callable as methods.
Step 2 — Define the Dashboards namespace
The dashboards we’re about to build live at
app/components/dashboards/. Zeitwerk will autoload classes
under this directory as Components::Dashboards::* —
nesting the namespace based on the directory structure under
app/components/.
For Zeitwerk to recognise Components::Dashboards as a valid
module, it needs an explicit definition. Create
app/components/dashboards.rb:
|
|
A two-line file that just defines the empty module. Zeitwerk
sees this and now considers Components::Dashboards a known
constant. Without it, references to
Components::Dashboards::ManagerDashboard would fail with
NameError: uninitialized constant.
This is a small Zeitwerk convention — every namespace sub-directory under autoload paths needs a module file at the parent level. As we add more grouped components in later modules (forms, panels, dialogs), we’ll create similar single-line module files.
Step 3 — The service object
The dashboard needs three sets of data: top-level counts, recent activity, and busiest regions. Putting the queries in the controller fattens it up unnecessarily; a service object handles it cleanly.
Create app/services/manager_dashboard_data.rb:
|
|
The pattern is the same module + extend self shape as the
SerializeServiceAreas service from Module 4 Lesson 1. Public
call returns the data hash; private methods do the work.
Each query lives in its own method, named for what it returns.
A few details worth noting.
recent_jobs includes both complete and cancelled
statuses. The dashboard shows actual operational outcomes
— jobs that finished one way or another. Pending and
in-progress jobs aren’t “recent activity” in this sense; they
haven’t completed their lifecycle yet.
recent_jobs uses .includes(:customer, :service_area, :assigned_to)
to avoid N+1 queries. Each entry needs the customer name, the
service area name, and the assigned field officer’s name —
without .includes, rendering 5 jobs would trigger 15 extra
queries.
busiest_regions is scoped to the last 7 days. A
dashboard’s “busiest” should reflect current operational
reality, not the all-time backlog. Seven days smooths out
day-to-day noise while staying recent enough to act on. We
use created_at rather than completed_at so the count
includes pending and in-progress jobs — work that’s being
handled in those regions, not just completed work.
Step 4 — The small components
Three new components for the dashboard. Each is small,
single-purpose, and lives flat in app/components/ (no
sub-directory, so the namespace stays simple — just
Components::*).
app/components/stat_tile.rb — a single metric tile in
the stat strip:
|
|
app/components/recent_activity_list.rb — the recent
activity section of the side panel. Each entry shows the
job’s status, the customer with their suburb for context, and
the field officer who handled it.
|
|
Three rows of information per item: status pill at the top
(green for complete, rose for cancelled), customer name and
suburb in the middle, the field officer’s name on the bottom
prefixed FO:. The FO: prefix matters because field
officer names are easy to mistake for customer names —
prefixing makes it unambiguous at a glance.
The assigned_to&.name || '—' handles the rare case where a
cancelled job was never assigned to a field officer. Defensive
because it’s cheap; rendering “FO: —” is clearer than blank
or nil-error.
app/components/busiest_regions_list.rb — the busiest
regions section. The heading reflects the data’s actual time
window so the manager isn’t left guessing whether they’re
looking at all-time totals or recent activity.
|
|
Each takes simple props and produces a small, self-contained piece of UI. They can be composed in any order; the dashboard determines the order.
Step 5 — The dashboard page component
Create app/components/dashboards/manager_dashboard.rb:
|
|
A few things worth pausing on.
The class is Components::Dashboards::ManagerDashboard. The
full path matches the file location: app/components/dashboards/manager_dashboard.rb.
Zeitwerk infers the nested namespace from the directory
structure.
The Kit syntax in action. PageHeader(...),
StatTile(...), MapPanelLayout(...), ServiceAreaMap(),
RecentActivityList(...), BusiestRegionsList(...) — all
calls without .new or render. This is the kit machinery
from Step 1 paying off.
The data flows in through props. The dashboard receives
@data from the controller (which got it from
ManagerDashboardData.call). The page is purely about
rendering — no queries, no logic about what to show.
Methods like render_stat_strip and render_side_panel
break up the view template into named regions. Same pattern
as helper methods in any view.
Step 6 — The controller
Replace app/controllers/dashboard_controller.rb:
|
|
And the view at app/views/dashboard/index.html.erb:
|
|
One line. The page component takes over from there.
Activity 1 — Visit the manager’s dashboard
Restart the Rails server (the extend Phlex::Kit change in
base.rb and the new module file at app/components/dashboards.rb
both load at boot, not on reload).
Sign in as the manager (manager@vera.test / password).
Click Dashboard in the sidebar (or visit /dashboard).
You should see:
- The page header with “Operations dashboard” and breadcrumb
- The stat strip showing 9 depots, 340 service areas, 30 field officers, and your active job count
- The map filling the left, showing all of Australia with 9 depot pins and 340 service area outlines
- The side panel on the right with two sections — Recent activity (5 completed jobs) and Busiest regions (5 SA3s)
The map is your Components::ServiceAreaMap from Module 4 —
no changes needed; it already shows the manager’s national
view. Hover over an SA3 polygon and it highlights, just as
in the Lab page. Click a depot pin and the popup appears.
All of Module 4’s work composes into the dashboard
unchanged.
Activity 2 — Sign in as a different role
Sign out and sign back in as dispatcher@vera.test. Visit
/dashboard. You’ll see… a routing error or a nil rendering.
The DashboardController’s case statements don’t yet handle the dispatcher or field officer roles. Lesson 4 builds those out. For now, the manager’s dashboard works; the others follow the same pattern.
Where this leaves us
The manager has a working dashboard. The architectural patterns are all in place:
- Service objects for view data (the controller stays thin)
- Phlex page components for pages with significant internal structure
- Phlex Kits for terse, readable composition
- The namespace pattern for component groupings
(
app/components/<group>.rbdefines the namespace; classes inside live in<group>/)
Lesson 4 reuses these patterns to build the dispatcher’s regional dashboard and the field officer’s personal dashboard. Each will have a service object, a page component, and a controller case clause — same shape as the manager’s, different content.