Skip to content

Instantly share code, notes, and snippets.

@AwolDes
Last active March 12, 2026 05:47
Show Gist options
  • Select an option

  • Save AwolDes/e64527bde9438dfdf50e2b0f4f4fc71d to your computer and use it in GitHub Desktop.

Select an option

Save AwolDes/e64527bde9438dfdf50e2b0f4f4fc71d to your computer and use it in GitHub Desktop.
UKPROD-1319: New Dashboard - Project Plan

UKPROD-1319: New Dashboard — Project Plan

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

Phase 1 Goals

  • 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

Core widgets (1:1 parity required)

  • 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)

Foundation Tickets (all Done)

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.


Widget Tickets


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.

Sub-tickets

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::LoginFetcher that queries Login records 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:

  1. The login's department → location timezone
  2. The login's shift → department → location timezone
  3. The user's personal timezone
  4. 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_url when 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 show action 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_item partial for a single clock-in/out event
  • Main feed list in the widget's show.html.haml
  • Use ds.card and 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
  • 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_id to 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.

Sub-tickets

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.

Sub-tickets

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 data
  • MetricPair — 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/predicted
  • DepartmentEntry / LocationEntry — named groupings with their own metrics
  • ChartData — 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:

  • BucketMath module with:
    • sum_counts / sum_costs / sum_sales — sum the :total from each bucket, optionally stopping at a cutoff time
    • today? / past_date? — date checks so we know whether to apply the cutoff (today = only show data up to now)
    • current_time_as_bucket — converts Time.zone.now to the bucket float format
    • format_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:

  • OncostCalculator class that:
    • Reads the org's oncost_configs and sums all config_value percentages into a single decimal multiplier (e.g. 11.5% super + 2% workcover = 0.135)
    • apply(cost) — returns cost * (1 + multiplier)
    • oncosts_configured? — tells the UI whether to show the toggle at all (no configs = no toggle)

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:

  • ScopeBuilder that 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 — queries AwardInterpretationCache::Bucketer for actual and rostered staff headcounts and wage costs, returns arrays of bucket hashes
  • SalesFetcher — queries sales data for actual and predicted revenue, returns SalesBucket arrays
  • DepartmentBuilder — runs the fetchers per-department and groups results into DepartmentEntry structs 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:

  • MetricsCalculator that takes a date, scope, and organisation
  • Calls BucketFetcher and SalesFetcher to get the raw data
  • Uses BucketMath.sum_* to total the buckets (with current_time_as_bucket as cutoff if today)
  • Uses OncostCalculator to compute cost-with-oncosts
  • Returns a Metrics struct 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:

  • ChartBuilder that takes raw bucket arrays from the fetchers and produces ChartData:
    • 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:

  • LiveInsightsData coordinator that takes the organisation, current user, and filter params
  • Calls ScopeBuilder to resolve the scope, MetricsCalculator for the aggregate numbers, and ChartBuilder for 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, calls LiveInsightsData, renders the widget partial with metric cards and chart container
  • LiveInsightsDataController#show — JSON endpoint, returns chart series data for the Stimulus controller
  • Route registration under the new_dashboard namespace
  • 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:

  • _header partial — widget title and date display
  • _metrics_grid partial — layout for the cards
  • _metric_card partial — reusable card showing a label, actual value, rostered/predicted comparison, and delta indicator
  • Uses ds.card for 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_controls partial 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:

  • _filters partial 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 ScopeBuilder on 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'.


Grid Interaction Tickets


UKPROD-1334 — GRID: Move Widgets | Todo | ⚠️ Potential Rabbit Hole | Assignee: Daniel Chick

No description (screenshot only).

Sub-tickets

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.


Rollout & Polish Tickets


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.


Order of Attack

  1. Foundation — all done. Controller, grid, skeleton widget, feature flag, initial design.
  2. Easy widgets — Timeclocks, Live Clock-in Feed, Birthdays & Anniversaries done. Next: Who's In Today, What Should I Work On.
  3. Medium widgets — Who's Using Workforce (in progress), Leave Calendar (in progress, new design).
  4. Hard widgets — Live Wage/Insights (backend done, frontend backlogged), Embed Reports (iframes), Weekly Planner (may be cut/replaced by embed).
  5. Grid interactions — Hide (in progress), Add Back (in progress), then Move (drag & drop + state save + jQuery compat) and Resize (potential rabbit holes).
  6. Rollout — Opt-in/out CTAs, perf benchmarking, design clean-up, broader rollout.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment