Skip to Content

← All archived runs

Run: user-record-and-reporting-line

run.md

Run: user-record-and-reporting-line

  • branch: claude/user-record-and-reporting-line
  • pr: #843

02_define/output/spec.md

Spec: The user record and the reporting line

  • slug: user-record-and-reporting-line
  • personas: Admin
  • touches: packages/services/src/db/models/user.ts, packages/services/src/db/services/users, packages/services/src/db/services/hierarchy (new), packages/services/src/db/migrations, apps/web/components/users, apps/web/app/(app)/users, apps/web/app/(app)/admin/users, apps/web/lib/resolve-app-user-for-tenant.ts
  • complexity: complex

Problem

A Sustentus organisation has no reporting structure. Nobody has a manager, there is no notion of a team, and there is nowhere to record who a person reports to. Every other part of the v1-user-hierarchy scope — inherited visibility, ownership handoff, regional roll-up — needs that single fact to exist first. This advances Q2-2026 Objective 1 (Establish Product-Market Fit with Vendor Partners) under the Refine-the-bridge initiative: vendor partners need one named owner and one reporting line per person before leaders can see their teams' work.

Proposed change

Every user record carries exactly seven facts — name, email, role, manager, region, country and status — and the manager pointer is the organisation structure. There is no separate chart.

Model (packages/services):

  • manager (ObjectId ref user, optional) joins the user schema. employment (internal | external) joins it too — it is what distinguishes an unplaced internal person from an external Expert reached by assignment (D-2, design notes), and it is not a second role. isHierarchyTop (boolean) marks the organisation's single top-level user; a partial unique index on {tenantId} where isHierarchyTop: true makes "exactly one top" (BR-2, D-3) a database invariant. Indexes ship in a migration (autoIndex is off).
  • Backfill (per Jamie's 2026-08-17 rulings): per tenant, the tenant admin becomes the single top-level user; every other active single-internal-role user (csm, sdm) initially reports to them. Employment backfills internal for admin/csm/sdm and external for expert, vendor and customer docs. Vendor-role and multi-role user documents are untouched and sit outside the hierarchy in v1 — the hierarchy covers internal roles only.
  • A pure, unit-tested hierarchy core (breadth-first, cycle-guarded, depth-capped at 10 — BR-3, BR-4) following the lead-scope.ts precedent: line-of, depth, cycle and validity checks. A hierarchyService wraps it with tenant-scoped reads and the manager-change write.
  • Manager-change validation (BR-3, BR-5): the manager must be an active internal user in the same organisation; self-management and loops are refused with a clear explanation; a change that would push any affected reporting line past 10 levels is refused. A valid change moves the person and their whole subtree, intact, in one step — nobody else's line changes (that is true by construction: only the one manager pointer is written).
  • The top-level user cannot be given a manager while top, and cannot be disabled while top — an administrator appoints the replacement top first (scope §13 "the top person leaves"). An explicit admin appoint-as-top action swaps the marker.
  • Disabling (BR-7, BR-8): a disabled user cannot sign in (enforced where the app resolves the signed-in user). Disabling never removes historical ownership or reporting — no data is rewritten; each of their open items is held by their nearest active manager until an administrator reassigns it, derived at read time (design notes: ownership is derived, never stored twice). This run surfaces the held-items note in user management; the lead surfaces adopt the same derivation in service-lead-ownership-and-handoff.

User management (apps/web):

  • The users tables (/users, /admin/users) show all seven facts at a glance: name, email, role, manager (with Unplaced warning for internal users with no manager, Top for the top user, By assignment for external Experts, Outside hierarchy for vendor-role/multi-role rows), region, country and status. The grouped-by-person aggregation is extended to carry them.
  • A summary strip on the admin users page counts unplaced internal users, so who is still unplaced is visible at a glance.
  • The user detail page gains a reporting panel: set or clear the manager (candidates: active internal users of the tenant, minus the user and their own subtree), flip an Expert's employment, appoint-as-top. Region and country become editable free-text alongside name and status (their semantics — curated regions, the ISO country list — arrive with regions-and-countries, which replaces these inputs with pickers).
  • Name and status stay editable as today; email is set at invitation (Clerk identity) and stays read-only afterwards; role changes keep their existing admin panels.

Acceptance criteria

  • An administrator can set and see all seven user fields at a glance in user management.
  • Every user has exactly one role and one manager; only the single top-level user has none.
  • A manager change creating a loop is refused with a clear message.
  • A valid manager change carries the person's whole team with them, and changes nobody else.
  • A user with no manager and no team sees exactly their own work — verified with a fresh account.
  • Disabling a user hands their open items to their manager and leaves their history intact.
  • Administrators can see at a glance who is still unplaced.

Out of scope

  • Any visual hierarchy editor or org-chart builder.
  • The inherited visibility rules themselves — that is inherited-visibility.
  • Region and Country semantics — that is regions-and-countries; this feature only stores them.
  • Multiple managers, matrix or dotted-line reporting.
  • The Sales role — that is service-lead-ownership-and-handoff; the hierarchy admits it when it lands.
  • Migrating vendor contacts or multi-role people into the hierarchy (Jamie 2026-08-17: later scope).
  • Manager powers over team members' records — visibility only, and visibility is stub 3's.

Open questions

  • none

03_build/output/notes.md

Build notes: user-record-and-reporting-line

  • commits: feat: user-record-and-reporting-line — seven-field record + manager pointer

What changed

  • packages/services/src/shared/hierarchy.ts (+ tests): the pure hierarchy core — breadth-first, cycle-guarded, depth-capped traversal (lineOf, chainAbove, subtreeDepth), the manager-change validator (BR-3/4/5), the unplaced and effective-holder derivations (BR-6/8), employment defaults and placement labels. Exported via @sustentus/services/shared.
  • packages/services/src/db/models/user.ts: manager (ObjectId ref), employment (internal/external), isHierarchyTop on the base schema; tenantId_manager index and the tenantId_hierarchy_top_unique partial unique index (one top per tenant, as a DB invariant).
  • packages/services/src/db/migrations/1787529600000-user-reporting-line.ts: builds both indexes; backfills employment (staff internal, expert/vendor/ customer external); per tenant appoints the oldest active single-role admin as top and points every other active single-role internal doc at them (Jamie's 2026-08-17 rulings). Vendor-role and multi-role docs untouched.
  • packages/services/src/db/services/hierarchy/: hierarchyService — org index, summary, manager candidates, setManager (validated write), appointTop, effective holder.
  • packages/services/src/db/services/users/index.ts: the grouped directory rows now carry location, employment, manager (+ page-scoped $lookup for the manager's name), isHierarchyTop and the per-person doc count.
  • apps/web/lib/resolve-app-user-for-tenant.ts: BR-7 — a disabled admin/csm/sdm resolves to a clean failure (DISABLED_ACCOUNT_ERROR), so disabled staff cannot use the app. Expert docs keep isActive:false as their onboarding gate; customers keep auto-reactivation.
  • apps/web/components/users/users-table/users-table.tsx: Manager (placement — manager name / Unplaced warning badge / Top of organisation / By assignment / Outside hierarchy), Region and Country columns.
  • apps/web/components/admin/users/hierarchy-summary.tsx (+ admin users page): unplaced / disabled / deepest-line strip, top named.
  • apps/web/components/users/user-profile/: reporting-line panel (manager select over valid candidates, appoint-as-top, expert employment toggle), region/country inputs on the edit form, header badges, and the disabled-user "open items held by <manager>" note.
  • apps/web/app/(app)/users/[id]/actions.ts: setUserManager, appointHierarchyTop, setUserEmployment (all tenantConfig.manage); updateTenantUser gains region/country and refuses disabling the top.

Acceptance criteria status

  • An administrator can set and see all seven user fields at a glance in user management — table shows all seven; name/status/region/country on the edit form, manager in the reporting panel, role in the existing panels; email is set at invitation and read-only after.
  • Every user has exactly one role and one manager; only the single top-level user has none — one manager pointer per doc, one top per tenant enforced by partial unique index. Existing multi-role people sit outside the hierarchy per the 2026-08-17 ruling.
  • A manager change creating a loop is refused with a clear message — validator + unit test.
  • A valid manager change carries the person's whole team with them, and changes nobody else — single pointer write; asserted in tests.
  • A user with no manager and no team sees exactly their own work — own-scoped resolution is unchanged; inherited visibility ships in stub 3 and reads the same fail-safe core.
  • Disabling a user hands their open items to their manager and leaves their history intact — effective-holder derivation (nothing rewritten) + profile note; the lead surfaces adopt it in service-lead-ownership-and-handoff.
  • Administrators can see at a glance who is still unplaced — warning badges in both users tables + the reporting-line summary count.

Notes for Verify

  • appointTop clears the old marker and sets the new one in two writes (no transaction); the partial unique index makes the failure mode "no top" rather than "two tops".
  • BR-7 sign-in blocking is enforced where the app resolves the signed-in user (resolveAppUserForTenant); pages that render from the role claim alone don't hit that path. Expert-role docs are deliberately exempt: their isActive:false is the onboarding gate (pre-existing model debt — flagged rather than changed here).
  • The backfill skips tenants with no active admin, with a warning — operator follow-up, not silent.
  • Migration + schema indexes are in lockstep (tenantId_manager, tenantId_hierarchy_top_unique).