Skip to Content

← All archived runs

Run: team-region-country-filters

run.md

Run: team-region-country-filters

  • branch: claude/team-region-country-filters
  • pr: #848

02_define/output/spec.md

Spec: My Team, Region and Country — the three standard filters

  • slug: team-region-country-filters
  • personas: Admin, CSM, SDM
  • touches: apps/web/components/scope-bar (new), apps/web/app/(app)/service-leads/page.tsx, apps/web/app/(app)/users/page.tsx, apps/web/app/(app)/admin/users/page.tsx, apps/web/components/users/users-table, apps/web/lib/actions (scope-filter persistence), packages/services/src/db/services/leads/index.ts, packages/services/src/db/services/users/index.ts, packages/services/src/db/services/hierarchy/index.ts, packages/services/src/db/models/user.ts
  • complexity: standard

Problem

Once a leader can see their whole team's work, the next question is immediately "just EMEA" or "just my direct line". Without a consistent way to narrow, every surface grows its own ad-hoc controls and two views of the same population disagree (FR-9, BR-19). This advances Q2-2026 Objective 1: consistent regional and team roll-ups are what the vendor-partner conversations read.

Proposed change

Three filters — My Team, Region and Country — as one shared component (ScopeBar, per the design notes: one component, not three variations), mounted identically on the surfaces in scope.

  • My Team narrows to the viewer and everyone below them in their reporting line — or to one direct report's line, picked by name with its headcount (the design's "Priya Nair's line — 4 people"). A person with no team simply sees their own work. The selection is resolved server-side against the viewer's own line (administrators may pick any line), and it is always intersected with the visibility model — so no combination ever shows a record the viewer could not already see (BR-19: filters narrow, never widen; geography filters data, never grants access).
  • Region narrows to one of the organisation's regions; Country to one country, with the country options narrowed by the chosen region (stub 2's curated groupings — a country in one region only is what makes a regional total add up without double counting).
  • The filters drive the URL (camelCase params team, region, country, per the list-surface convention) and are remembered per person between visits: the choice is stored on the user's record and applied as the default when the URL carries no filter.
  • Surfaces in scope (v1) — the two the Design stage prototyped the bar on: the service-leads board (columns, value tiles and counts all filter together, so the tiles never disagree with the columns) and user management (both /users and /admin/users). The CSM/SDM dashboards' existing mine/tenant toggles converge on the same component as those surfaces adopt the hierarchy (D-7) — deliberately not rebuilt in this run.
  • The demonstration app is not a delivery target — Design prototyped the filters there; this run touches no apps/demo code.

Acceptance criteria

  • My Team, Region and Country appear on every dashboard, tile, list and report in scope.
  • No filter combination ever shows a record the viewer could not already see.
  • A person with no team sees exactly their own work under My Team.
  • A regional total adds up without double counting.
  • Each person's filter choices are still applied on their next visit.
  • No demonstration-app code is touched by this feature's implementation.

Out of scope

  • Region curation and the country list — that is regions-and-countries.
  • The visibility model itself — that is inherited-visibility.
  • The demonstration app — Design already prototyped the filters there; Build does not touch it.
  • Any redesign of the dashboards these filters sit on — the CSM/SDM dashboard panels keep their current toggles until they adopt the shared bar.

Open questions

  • none

03_build/output/notes.md

Build notes: team-region-country-filters

  • commits: feat: team-region-country-filters — the three standard filters

What changed

  • apps/web/components/scope-bar/scope-bar.tsx (new): the one shared control — My Team (the viewer's line, or one direct report's line with its headcount), Region, and Country (options narrowed by the chosen region; changing region clears a country it doesn't hold). Selections ride the URL (team/region/country, camelCase per the list convention, page reset) and are persisted per person.
  • apps/web/lib/scope-filter-data.ts (new): one server-side resolution for every mounting surface — URL wins, the person's remembered choices apply when the URL carries none; the team selection resolves through hierarchyService.resolveTeamSelection, which only accepts the viewer's own line (admins may pick any line) and degrades stale choices to "everyone".
  • apps/web/lib/actions/scope-filters.ts (new): rememberScopeFilters — authn-only, self-scoped write of user.scopeFilters (new optional field on the user model; no migration needed — no index, no backfill).
  • packages/services — filters narrow, never widen (BR-19):
    • leads/lead-scope.ts: LeadScopeFilters + applyLeadScopeFilters — region/country equality plus a team clause ANDed onto whatever visibility filter is in place (ids cast explicitly so aggregation $match filters correctly).
    • leadService.findByStatuses and getLeadValueSummary accept filters, so the board's columns and its value tiles compute from the same filtered set and never disagree.
    • userService.findAllGroupedByPerson accepts region/country/teamIds.
    • hierarchyService.getTeamFilterOptions (the design's "Priya Nair's line — 4 people") and resolveTeamSelection.
  • Mounted on the surfaces in scope: the service-leads board, /users and /admin/users — identically, from the same component and the same resolution.

Acceptance criteria status

  • My Team, Region and Country appear on every dashboard, tile, list and report in scope — the board (columns + tiles) and both user-management lists, per the spec's v1 surface list; the CSM/SDM dashboard panels converge later as stated.
  • No filter combination ever shows a record the viewer could not already see — every clause is ANDed onto the visibility filter; team selections outside the viewer's line resolve to no narrowing; geography is equality-only.
  • A person with no team sees exactly their own work under My Team — "mine" for a non-member resolves to [viewerId].
  • A regional total adds up without double counting — one region per country (BR-16) + equality filtering; the tiles aggregate the same filtered set.
  • Each person's filter choices are still applied on their next visit — stored on the user record, applied as defaults when the URL carries no filter.
  • No demonstration-app code is touched by this feature's implementation — the diff contains no apps/demo change.

Notes for Verify

  • The remembered-filters write is fire-and-forget from the client; a failed write costs only the memory, never the filter.
  • Stored region/country values are re-validated implicitly (they filter to zero rows if stale) rather than cleared eagerly.
  • The CSM/SDM dashboard mine|tenant toggles are unchanged this run — the convergence path is stated in the spec's out-of-scope.
  • Stacked on service-lead-ownership-and-handoff.