| Invoice # | Vendor | Amount | Status |
|---|---|---|---|
| INV-10234 | Acme Supply Co. | $4,208.00 | Pending |
| INV-10235 | Globex Industrial | $18,940.50 | Approved |
| INV-10236 | Initech GmbH | $730.25 | Rejected |
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.
Anatomy
- Period control — one filter governing every widget; it belongs in the URL so a dashboard is linkable.
- Widget grid —
.bo-grid; each widget is independently swappable. - 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.
- Container queries — widgets respond to THEIR box, not the viewport, so the same widget works in a half-width column.
Data contract
| Request | Returns / 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
| State | What ships |
|---|---|
| Loading | per-widget skeletons — the grid layout is known before the data, so the page must not reflow as widgets land |
| Empty widget | name the reason ("no invoices in this period"), not a bare zero |
| Error in one widget | that widget shows its own retry; the rest of the dashboard stays usable |
| Stale data | a timestamp ("as of 09:12") on cached aggregates — an unlabelled number is assumed live |
| No permission for a metric | omit the widget; a redacted tile invites a support ticket |
| Export running | a progress affordance and a completion signal; never a frozen button |
| No JS | widgets 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