TL;DR: Rebuild the old dashboard into the new stack with 100% parity and fix issues along the way.
Shape doc: https://docs.google.com/document/d/1Rs3KAZX7-4dMbJzsMIQGmtzN-UqPmU8nbM_cUeFzKB0/edit?tab=t.0
- Performant — loads quickly
- Consistent with the rest of the product design system
- Data mirrors the old dashboard (no regression)
- Permissions respected — no regression in visibility
- Widget move/resize UX is functional and user-friendly
- Filters and selections persist per user
- Reports embedded and displayed on load (no "generate" required)
- Users can opt in/out via a discoverable CTA with in-product feedback; opt-in/out is tracked
- What I Should Work On
- Time Clocks
- Live Clock-in Feed
- Who's Using Workforce
- Birthdays and Work Anniversaries
- Who's In Today
- Live Wage Tracker / Live Insights (with oncost toggle)
- Leave Calendar (new design)
UKPROD-1320 — Add Feature Flag | Done
No description.
UKPROD-1321 — Create New Dashboard Controller and View | Done
No description.
UKPROD-1322 — Create Grid System for Widgets | Done
No description.
UKPROD-1323 — Create Skeleton Widget Partial for Widgets to Sit In | Done
No description.
UKPROD-1375 — Initial Widget Design Implementation | Done
No description.
UKPROD-1324 — Widget: What Should I Work On? | Todo | Medium | Assignee: Tom Bradley
Should be relatively easy to copy the existing widget. Bonus points if you are able to bring in HR and Payroll "what should I work on" items.
UKPROD-1325 — Widget: Time Clocks | Done | Easy | Assignee: Axel Whitford
Just load devices, low amount of data loaded here so not fussed.
UKPROD-1326 — Widget: Live Clock-in Feed | Done | Easy | Assignee: Daniel Chick
Easy.
UKPROD-1356 — Data Fetcher: Query Logins from the Last 3 Days | Done | 3pts
The core of the live feed is a query that pulls recent clock-in/out events. The legacy DashboardWidgets::LiveFeed already does this, but it's tangled up with photo URL generation, timezone resolution, and presentation concerns. We need a clean data layer in the new stack.
What to add:
- A
NewDashboard::LiveFeed::LoginFetcherthat queriesLoginrecords from the last 3 days - Scoped to users accessible to the current user (
User.accessible) - Filtered to time clock devices only (excluding non-timeclock device modes)
- Respects department-limited managers (only show logins for departments they manage)
- Supports optional device filtering (when user selects a specific time clock)
- Returns structured data — not raw plucked arrays like the legacy version
- Eager loads associations to avoid N+1s:
user,department,shift.department.location
The legacy version uses raw SQL pluck with login_attrs — we should use proper AR relations and return structs or presentable objects instead. This is the heaviest query in the widget so getting it right matters.
Reference: app/models/dashboard_widgets/live_feed.rb lines 74-91 (base_query), lines 34-63 (logins)
UKPROD-1357 — Timezone Resolution for Login Events | Done | 1pt
Each clock-in event needs to display the time in the correct timezone. The tricky part is the fallback hierarchy — there's no single source of truth, so we walk a chain until we find one.
Timezone priority:
- The login's department → location timezone
- The login's shift → department → location timezone
- The user's personal timezone
- Fall back to
Time.zone.name(org default)
The legacy version does this inline in get_timezone and returns UTC offset in minutes. We should extract this into its own helper so it's testable and reusable. The eager loads from the fetcher ticket handle the N+1 risk here.
What to add:
- A timezone resolver that takes a login record (with preloaded associations) and returns the appropriate timezone
- Should return something the frontend can use to display times correctly (UTC offset or timezone name)
Reference: app/models/dashboard_widgets/live_feed.rb lines 93-101 (get_timezone)
UKPROD-1358 — Photo URL Generation for Login Events | Done | 2pts
Clock-in events can have photos taken by the time clock. The widget displays these as profile thumbnails next to each event. The legacy version does something a bit hacky — it constructs a Login object with just the photo columns set, then calls login.photo.redirect_url to get a signed URL.
What to add:
- A clean way to get photo URLs for login records without the legacy hack of
instance_variable_set(:@new_record, false) - Should handle the case where there's no photo (show a default avatar or initials)
- Photos should use
cached_urlwhen available (avoids regenerating signed URLs) - Lazy loading on
<img>tags for performance
Reference: app/models/dashboard_widgets/live_feed.rb lines 50-63 (photo URL generation)
UKPROD-1359 — Wire Up Controller and JSON Endpoint | Done | 2pts
The widget controller already exists as a skeleton (NewDashboard::Widgets::LiveFeedController) but it doesn't do anything. We need it to actually fetch and serve data.
What to add:
- The existing
showaction should render the initial widget HTML with the feed list (server-rendered via Turbo Frame) - A separate JSON endpoint for polling updates — the widget refreshes every 60 seconds
- The initial load should fetch a small batch (10 items) for fast first paint, then the frontend can request the full 3-day dataset
- Pass the login data to the view as structured locals (not instance variables — per project conventions)
The legacy version uses two separate endpoints (live_feed_data for JSON, live_feed_users for the search user list). We should keep a similar split or combine if it makes more sense with Turbo.
Reference: app/controllers/new_dashboard/widgets/live_feed_controller.rb, legacy view line 54 (init call with URLs)
UKPROD-1360 — Build the Feed List UI (Server-Rendered) | Done | 3pts
The main visual piece — a scrollable list of clock-in/out events, each showing the employee's photo, name, device name, time, and relative time ("3 minutes ago").
Each feed item needs:
- Profile photo (or default avatar)
- Employee name
- Device/time clock name
- Clock-in time (formatted in the correct timezone)
- Relative time label ("3 minutes ago") — this needs to update client-side
- Visual distinction between clock-in (green) and clock-out (different colour)
- Links to the employee's timesheet and login history
What to add:
_feed_itempartial for a single clock-in/out event- Main feed list in the widget's
show.html.haml - Use
ds.cardand design system components where possible - Skeleton loading state (already has 4 short skeletons, may need adjusting)
- Empty state when there are no recent logins
The legacy version renders everything client-side with crel() DOM construction. The new stack should server-render the initial list and only use JS for updates/filtering.
Reference: app/assets/javascripts/dashboard/widgets/live_feed.js lines 167-204 (render_clockin)
UKPROD-1361 — Add Polling and Relative Time Updates (Stimulus) | Done | 2pts
The feed needs to stay fresh — new clock-ins should appear without a page refresh, and the "3 minutes ago" labels need to tick over.
What to add:
- A Stimulus controller that handles:
- Polling the JSON endpoint every 60 seconds (matches the widget's
refresh_interval) - Prepending new login events to the feed when they arrive
- Updating all relative time labels ("3 minutes ago" → "4 minutes ago") every 60 seconds
- Polling the JSON endpoint every 60 seconds (matches the widget's
- Should use Turbo Frame refresh or morph if possible, falling back to manual DOM updates if needed
- The legacy version does an initial small fetch (10 items) then a larger fetch for the full 3 days — consider whether this optimisation is still needed or if server-side pagination is better
Reference: app/assets/javascripts/dashboard/widgets/live_feed.js lines 207-268 (init), lines 270-283 (update_login_times)
UKPROD-1362 — Add Search and Clock-in/out Filter | Done | 2pts
The legacy widget has a search bar to filter by employee name and checkboxes to show/hide clock-ins vs clock-outs. This is genuinely useful when there are lots of events happening and you want to find a specific person.
What to add:
- Text input for searching by employee name (with typeahead/autocomplete from the accessible user list)
- Checkbox filters: "In" and "Out" to toggle clock-in/clock-out visibility
- Filtering should be client-side for instant feedback (the data is already loaded)
- Filter state should persist across polls (so a new fetch doesn't reset your search)
- The legacy version stores checkbox state in cookies — we could use Stimulus values or similar
Reference: app/assets/javascripts/dashboard/widgets/live_feed.js lines 47-118 (ClockinSearch), lines 285-330 (get_filters, init_employee_filter)
UKPROD-1363 — Add Device Filter Dropdown | Done | 1pt
Orgs with multiple time clocks need to filter the feed by device. The legacy widget shows a dropdown of active time clocks in the widget header — selecting one filters the feed to only show events from that device.
What to add:
- Dropdown in the widget header listing active time clocks for the current user (
Device.active_time_clocks_for(current_user).display_names) - "All Time Clocks" option to clear the filter
- Selecting a device should reload the feed filtered server-side (pass
device_idto the fetcher) - Only show the dropdown when there are multiple devices — if there's one or zero, hide it
- The legacy version persists the selection in a cookie — we should use query params or Stimulus state
Reference: app/views/dashboard/widgets/_live_feed.html.haml lines 16-29 (device filter dropdown)
UKPROD-1364 — Styling and Design System Alignment | Done | 2pts
The legacy widget has 254 lines of custom SCSS. The new stack should use Tailwind + design system components as much as possible, only adding custom CSS where truly needed.
Key visual elements:
- Feed items with photo on the left, name/device/time on the right
- Clock-in events get a green accent, clock-out gets a different colour (match the project's Tailwind palette)
- Relative time labels styled subtly
- Scrollable feed list within the widget card
- Links to timesheet and login history on hover/focus
- Responsive layout within the dashboard grid
- Loading skeleton that matches the final layout shape
What to add:
- Tailwind classes on the HAML partials (prefer over custom CSS)
- Any necessary custom CSS in a new stylesheet if Tailwind can't cover it
- Make sure colours use the project's custom palette (not default Tailwind)
Reference: app/assets/stylesheets/dashboard/widget-live-feed.css.scss (254 lines of legacy styles)
UKPROD-1366 — Benchmark and Improve Performance | Done
No description.
UKPROD-1327 — Widget: Who's Using Workforce? | In Progress | Medium | Assignee: Axel Whitford
Medium.
UKPROD-1367 — Controller to Load Users In Today | Todo
No description.
UKPROD-1368 — Split Up by Working, On Break, Finished, On Leave, Absent/Missed Shift | Archived
No description.
UKPROD-1369 — Change to "Daily Operations" | Archived
No description.
UKPROD-1370 — Apply "Alerts" | Archived
No description.
UKPROD-1328 — Widget: Birthdays and Anniversaries | Done | Easy | Assignee: Louis Drinkwater
Easy ~
This one doesn't need a ton of innovation, just gonna load in users that meet the requirements of "birthday" or "anniversary" and display them ~ could be tricky with big orgs but not stressed at all.
- Load in users
- Group based on birthday or anniversary
- Display in UI
UKPROD-1329 — Widget: Who's In Today? | Todo | Easy | Assignee: Tom Bradley
Easy.
UKPROD-1330 — Widget: Leave Calendar | In Progress | Medium/Hard (new design) | Assignee: Axel Whitford
Medium/Hard — new design.
Possible new design: https://docs.google.com/document/d/16SpivAH426gLVDK_h81pJJYohvTFtJo5-UIccE3Z5zM/edit?tab=t.0#heading=h.4fn9pmc7ltr
UKPROD-1331 — Widget: Weekly Planner | Todo | Hard? | Assignee: Louis Drinkwater
Hard?
This is also looking to be deprecated and replaced with just embedding the weekly planner report itself.
UKPROD-1332 — Widget: Live Wage / Insights | In Progress | Hard | ⚠️ Potential Rabbit Hole | Assignee: Daniel Chick
Hard.
Rebuilding this in the new stack will be pretty tricky as there are a lot of moving parts. There's also the additional scope of adding oncosts.
UKPROD-1344 — Define Data Types & Value Objects | Done | 1pt
We need shared Sorbet T::Struct types that every layer of the feature will pass around. Without these, we'd be throwing loose hashes everywhere and it'd be a nightmare to reason about what data is where.
What to add:
SalesBucket— time + counts hash for revenue dataMetricPair— actual vs rostered values (used for both staff and cost)Metrics— the full set of numbers for one date: staff pair, cost pair, sales actual/predictedDepartmentEntry/LocationEntry— named groupings with their own metricsChartData— series arrays + labels ready for the JS chart
These are pure data containers with no behaviour — just shape definitions. Everything else in the feature builds on top of them, so this needs to go first.
UKPROD-1345 — Add Time-Bucket Math Helpers | Done | 1pt
The AwardInterpretationCache::Bucketer stores staff/cost data in 15-minute time buckets as floats (8.0 = 8:00am, 8.25 = 8:15am). Pretty much every part of the feature needs to sum these buckets, check if we're looking at today vs a past date, and format the floats as readable time labels for the chart axis.
What to add:
BucketMathmodule with:sum_counts/sum_costs/sum_sales— sum the:totalfrom each bucket, optionally stopping at a cutoff timetoday?/past_date?— date checks so we know whether to apply the cutoff (today = only show data up to now)current_time_as_bucket— convertsTime.zone.nowto the bucket float formatformat_label— turns 14.5 into "14:30" via.to_ls("tfour_hour.base")
Small, pure functions that are easy to test in isolation. The fetchers, calculator, and chart builder all call these.
UKPROD-1346 — Add Oncost Calculator | Done | 1pt
Organisations configure oncost percentages (super, workcover, payroll tax, etc.) in OncostConfig records. The widget needs to show wage cost both with and without oncosts and let the user toggle between them. Rather than scattering this logic everywhere, one class should own it.
What to add:
OncostCalculatorclass that:- Reads the org's
oncost_configsand sums allconfig_valuepercentages into a single decimal multiplier (e.g. 11.5% super + 2% workcover = 0.135) apply(cost)— returnscost * (1 + multiplier)oncosts_configured?— tells the UI whether to show the toggle at all (no configs = no toggle)
- Reads the org's
Self-contained, only depends on the existing OncostConfig model.
UKPROD-1347 — Add Scope Builder for Dashboard Filters | Done | 2pts
The dashboard has filters — the user might be viewing one location, one department, one team, or everything. The data fetchers need to know which AwardInterpretationCache records to query, so we need something to translate the UI filter params into AR scopes.
What to add:
ScopeBuilderthat takes dashboard filter params (location_id,department_id,team_id) and the organisation- Returns the set of location IDs, department IDs, and user IDs that match the current filter
- Handles the "all" case (no filter), single selection, and nested lookups (e.g. a team filter needs to resolve to its member user IDs)
The fetchers depend on this — without it they'd have to know about filter logic themselves, which would get messy fast.
UKPROD-1348 — Add Data Fetchers (Staff, Costs, Sales) | Done | 3pts
The raw data lives in AwardInterpretationCache (staff/costs) and a sales source (revenue). We need to pull this for a specific date and scope, already bucketed into 15-minute intervals. Each data source has different query patterns so they're separate fetchers.
What to add:
BucketFetcher— queriesAwardInterpretationCache::Bucketerfor actual and rostered staff headcounts and wage costs, returns arrays of bucket hashesSalesFetcher— queries sales data for actual and predicted revenue, returnsSalesBucketarraysDepartmentBuilder— runs the fetchers per-department and groups results intoDepartmentEntrystructs for the breakdown view
These are the raw data pipeline — the calculator and chart builder consume their output.
UKPROD-1349 — Add Metrics Calculator | Done | 2pts
The widget's metric cards need single aggregate numbers — "42 staff on now", "$12,450 wage cost", "$18,200 sales". The fetchers return bucket arrays, so something needs to sum the buckets, apply the today-cutoff, apply oncosts, and assemble the final numbers.
What to add:
MetricsCalculatorthat takes a date, scope, and organisation- Calls
BucketFetcherandSalesFetcherto get the raw data - Uses
BucketMath.sum_*to total the buckets (withcurrent_time_as_bucketas cutoff if today) - Uses
OncostCalculatorto compute cost-with-oncosts - Returns a
Metricsstruct with actual/rostered pairs for staff, cost, and sales
This is where the business logic lives — depends on the fetchers, BucketMath, and OncostCalculator.
UKPROD-1350 — Add Chart Builder | Done | 2pts
The frontend chart needs data in a specific shape — named series with value arrays, matching labels, and a "now" marker. The raw buckets don't match this shape, so we need a translation layer between backend data and the exact JSON the Stimulus controller expects.
What to add:
ChartBuilderthat takes raw bucket arrays from the fetchers and producesChartData:- Series for actual staff (line), rostered staff (dashed line), actual cost (bar), rostered cost (bar), sales (bar), predicted sales (bar)
- Time labels generated via
BucketMath.format_label - "Now" marker index for today's chart
Depends on BucketMath and the fetcher output shapes. Not needed until the chart UI exists, but good to build before the UI so the Stimulus controller has data to consume from the start.
UKPROD-1351 — Add Top-Level Coordinator | Done | 1pt
The controller shouldn't know about scope builders, fetchers, calculators, and chart builders — that's way too much for a controller to manage. One entry point should take dashboard params and return everything the view needs.
What to add:
LiveInsightsDatacoordinator that takes the organisation, current user, and filter params- Calls
ScopeBuilderto resolve the scope,MetricsCalculatorfor the aggregate numbers, andChartBuilderfor the chart series - Returns a single object the controller can pass straight to the view
This is just glue — it only exists once all the pieces it coordinates are built. Keeps the controller thin and makes it easy to test the full pipeline end-to-end.
UKPROD-1352 — Wire Up Controllers & Routes | Backlog | 2pts
The widget needs two endpoints: a Turbo Frame one that renders the HTML widget (loaded lazily when the dashboard scrolls to it), and a JSON one that returns chart data (fetched by the Stimulus controller after the frame loads, so the initial page load stays fast).
What to add:
Widgets::LiveInsightsController#show— Turbo Frame endpoint, callsLiveInsightsData, renders the widget partial with metric cards and chart containerLiveInsightsDataController#show— JSON endpoint, returns chart series data for the Stimulus controller- Route registration under the
new_dashboardnamespace - Widget registration in the dashboard grid so it actually appears on the page
- Wire the widget's Turbo Frame into
dashboard/show.html.haml
Can't serve any UI without this. Depends on the coordinator being done.
UKPROD-1353 — Build Metric Cards & Widget Layout | Backlog | 2pts
The first visible piece. Users need to see at-a-glance numbers: how many staff are on right now vs rostered, what the wage cost is, what sales look like. These cards are the primary information layer — the chart is supplementary detail.
What to add:
_headerpartial — widget title and date display_metrics_gridpartial — layout for the cards_metric_cardpartial — reusable card showing a label, actual value, rostered/predicted comparison, and delta indicator- Uses
ds.cardfor the container and design system typography - Skeleton loading states shown before data arrives
- Oncosts toggle on the cost card (shows/hides the oncost-adjusted value)
No JS needed for this bit — pure server-rendered HTML via Turbo Frame.
UKPROD-1354 — Build Chart & Controls (Stimulus + ApexCharts) | Backlog | 3pts
The metric cards show current totals, but managers want to see the shape of the day — when the rush is, whether staffing tracks demand, where costs spike. The chart shows staff/cost/sales over time with 15-minute granularity.
What to add:
- Stimulus controller (
live_insights_controller.js) that fetches chart data from the JSON endpoint and renders an ApexCharts combo chart — lines for staff headcount, bars for cost and sales - Colours mapped to the project's custom Tailwind palette (blue-600, cyan-400, pink-500, pink-300)
- "Now" vertical marker on today's view so you can see where current time is
_chart_controlspartial with legend dots and series toggles- Loading CSS for the skeleton state
Most complex frontend piece — depends on the JSON data endpoint and chart builder being done.
UKPROD-1355 — Add Filter Dropdowns (Location/Department) | Backlog | 2pts
Managers oversee multiple locations and departments. They need to drill down to a specific one without leaving the dashboard. Filters should reload just the widget, not the whole page.
What to add:
_filterspartial with dropdown selects for location and department- Selecting a filter triggers a Turbo Frame reload of the widget with the new filter params, which flows through
ScopeBuilderon the backend - Remembers the last selection via query params or Stimulus state
The widget works fine without filters (defaults to "all"), so this is the last piece. It's polish that depends on everything else being wired up first.
UKPROD-1365 — Benchmark and Improve Performance | Todo
No description.
UKPROD-1333 — Widget: Embed Reports | Todo | Hard | ⚠️ Potential Rabbit Hole | Assignee: Louis Drinkwater
Hard — embedding a SPA in the new stack breaks CSS, have to use iframes.
Reports can be embedded and displayed on load without requiring a user to 'generate'.
UKPROD-1334 — GRID: Move Widgets | Todo | ⚠️ Potential Rabbit Hole | Assignee: Daniel Chick
No description (screenshot only).
UKPROD-1371 — Implement Drag and Drop Functionality and Stimulus | Todo
No description.
UKPROD-1372 — Save State When Dragged and Dropped | Todo
No description.
UKPROD-1373 — Backwards Compatibility with jQuery State | Todo
No description.
UKPROD-1335 — GRID: Resize Widgets | Todo | ⚠️ Potential Rabbit Hole | Assignee: Daniel Chick
No description.
UKPROD-1336 — GRID: Hide Widgets | In Progress | Assignee: Axel Whitford
Should be relatively easy to implement. There's a model that already saves the state of whether or not a widget is visible on the dashboard — just need to check the state of the model and decide whether or not to render it based on that.
Will need to put in the work to make sure the data isn't loaded if it is hidden.
UKPROD-1337 — GRID: Add Widgets Back / Add New Widgets | In Progress | Assignee: Axel Whitford
No description.
UKPROD-1338 — Opt Out of New Dashboard | Todo
No description.
UKPROD-1339 — Start Rolling Out Dashboard | Todo
No description.
UKPROD-1340 — Perf Benchmarking and Comparison | Todo
No description.
UKPROD-1341 — Design Clean Up | Todo
No description.
UKPROD-1342 — Opt In from Old Dashboard | Todo
No description.
UKPROD-1374 — Circle Back Rabbit Holes | Todo
Catch-all ticket for deferred rabbit holes flagged during the sprint.
- Foundation — all done. Controller, grid, skeleton widget, feature flag, initial design.
- Easy widgets — Timeclocks, Live Clock-in Feed, Birthdays & Anniversaries done. Next: Who's In Today, What Should I Work On.
- Medium widgets — Who's Using Workforce (in progress), Leave Calendar (in progress, new design).
- Hard widgets — Live Wage/Insights (backend done, frontend backlogged), Embed Reports (iframes), Weekly Planner (may be cut/replaced by embed).
- Grid interactions — Hide (in progress), Add Back (in progress), then Move (drag & drop + state save + jQuery compat) and Resize (potential rabbit holes).
- Rollout — Opt-in/out CTAs, perf benchmarking, design clean-up, broader rollout.