busy-office-ui
Menu

Pattern: reporting dashboard

Who uses it: a manager checking whether this period's AP activity is on track and where the exceptions are. What "done" looks like: three numbers read at a glance, an exception spotted without opening a single invoice. Composed entirely from existing components: breadcrumb, filter bar + saved views, a stat-tile row, and a widget grid mixing a data table, an audit-style activity feed, and a collapsible notes card. Zero new CSS — everything here already has its own component page. Not for an actionable worklist — this screen is read-only situational awareness: numbers and a feed a manager scans, not a queue anyone works from. A screen whose whole job is triaging items one by one is an inbox or a job monitor, not this.

Open invoices 1,284 +12% increase, worse, vs last quarter
Total spend $412K +8% increase, better, vs plan
Overdue 17 unchanged vs last quarter
Recent invoices
Invoice #VendorAmountStatus
INV-10234 Acme Supply Co. $4,208.00 Pending
INV-10235 Globex Industrial $18,940.50 Approved
INV-10236 Initech GmbH $730.25 Rejected
Recent activity
  1. M. Okafor received 5 of 8 monitor arms
  2. J. Kim approved PO-4021
Notes

Q3 spend is trending 8% under plan — mostly the delayed monitor-arm shipment. Revisit at month close.

Anatomy

  1. Period control — one filter governing every widget; it belongs in the URL so a dashboard is linkable.
  2. Widget grid — .bo-grid; each widget is independently swappable.
  3. Stat tiles — one number, its delta, and the comparison basis spelled out. This is the human-monitoring signal: the delta plus the "as of" staleness timestamp (see States) is what tells a reader whether automated work behind the number is current.
  4. Container queries — widgets respond to THEIR box, not the viewport, so the same widget works in a half-width column.

Data contract

RequestReturns / on failure
GET /dashboard?period=the grid shell with each widget as a lazy fragment
GET /dashboard/widget/:id?period=one widget — so a slow query never blocks the others. On 5xx/timeout: that widget shows its own error + retry (see States); the rest of the grid stays usable
GET /dashboard/export?period=CSV/XLSX; long exports return a job id (see the long-running-jobs note below)

The AI-manages / human-monitors split, stated: the aggregates this screen shows are typically produced by scheduled jobs, not the reader; the human's channel is the stat tiles' delta plus the "as of" timestamp on stale data — a number with no visible age is trusted as live, which is exactly the failure this screen must not ship.

States

StateWhat ships
Loadingper-widget skeletons — the grid layout is known before the data, so the page must not reflow as widgets land
Empty widgetname the reason ("no invoices in this period"), not a bare zero
Error in one widgetthat widget shows its own retry; the rest of the dashboard stays usable
Stale dataa timestamp ("as of 09:12") on cached aggregates — an unlabelled number is assumed live
No permission for a metricomit the widget; a redacted tile invites a support ticket
Export runninga progress affordance and a completion signal; never a frozen button
No JSwidgets render with their initial server-rendered data; row checkboxes still toggle individually as native inputs, but the select-all checkbox and the live "n selected" count need initDataTables() to sync. Saved-view links stay real, navigable <a> hrefs, but their aria-current active marking and re-populating the filter bar's fields from the URL need initSavedViews(). Collapsible cards stay fixed in whichever data-state the server rendered — the trigger buttons do nothing without initCollapsibleCards(), so a card rendered collapsed can never be reopened

Components used

complexity 2 of 4

↑↓ move Enter open Esc close