From 0c35531b8736d67514148022f4f747280a109ab7 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Apr 2026 13:00:20 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20extend=20=C2=A7D=20=E2=80=94=20context-?= =?UTF-8?q?aware=20default=20dashboard=20+=20standardized=20role=20switche?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After review, two additions to the multi-role UX section: 1. Replace static-priority post-login redirect with context-aware "go where the work is" via new user.getDefaultDashboard() — a juror+observer landing during an active jury round goes to /jury even though observer has no work; falls back to static priority when no role has actionable work. 2. Standardize the role switcher's location across all dashboards. Extract shared useRoleSwitcher hook + new RoleSwitcherPill that renders in the top-right of every layout, including admin (which currently puts switching in the bottom-left sidebar pill). Co-Authored-By: Claude Opus 4.7 (1M context) --- ...026-04-28-mentor-round-readiness-design.md | 80 ++++++++++++++++--- 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/docs/superpowers/specs/2026-04-28-mentor-round-readiness-design.md b/docs/superpowers/specs/2026-04-28-mentor-round-readiness-design.md index ac80507..3a5c585 100644 --- a/docs/superpowers/specs/2026-04-28-mentor-round-readiness-design.md +++ b/docs/superpowers/specs/2026-04-28-mentor-round-readiness-design.md @@ -212,27 +212,55 @@ A standalone test PR is *not* planned — tests ride with the change they cover. - `src/lib/email/templates/mentor-onboarding.tsx` (new) - `src/server/services/notifications.ts` (or equivalent — call site to send mentor-onboarding email when MENTOR role is freshly added to a user) -**1. Post-login redirect — priority-based on `roles[]`:** +**1. Post-login redirect — context-aware "go where the work is":** -Replace single-`role` switch in `src/app/page.tsx` with priority order: +Replace single-`role` switch in `src/app/page.tsx` with a priority list that is *filtered by actionable work*. The user lands on the highest-priority role for which they have something to do right now; if no role has active work, fall back to the static priority order. + +New tRPC query: `user.getDefaultDashboard()` (server-side, called from `src/app/page.tsx`): ```ts +// Static priority — used as fallback ordering AND as the order we check for work. const ROLE_DASHBOARD_PRIORITY: Array<[UserRole, Route]> = [ ['SUPER_ADMIN', '/admin'], ['PROGRAM_ADMIN', '/admin'], ['AWARD_MASTER', '/award-master'], ['JURY_MEMBER', '/jury'], ['MENTOR', '/mentor'], - ['OBSERVER', '/observer'], ['APPLICANT', '/applicant'], + ['OBSERVER', '/observer'], ['AUDIENCE', '/audience'], ] -const userRoles = session.user.roles ?? [session.user.role] -const target = ROLE_DASHBOARD_PRIORITY.find(([role]) => userRoles.includes(role))?.[1] -if (target) redirect(target) ``` -**Decision: priority-based, not "remember last view".** The "remember last view" approach requires a new column on User and adds login-side complexity. Priority is deterministic, easy to explain, and the role-switcher dropdown handles the case where the user wants a different view. Revisit if users complain. +For each role the user holds (in priority order), the server checks "does this user have actionable work in this role right now?": + +| Role | "Has actionable work" predicate | +|------|---------------------------------| +| SUPER_ADMIN / PROGRAM_ADMIN | Always true (admin work is always present) | +| AWARD_MASTER | Any unfinalized award decision in an active round in current edition | +| JURY_MEMBER | Any `JuryAssignment` linked to a round whose `status = ROUND_ACTIVE` AND the user has at least one PENDING evaluation | +| MENTOR | Any `MentorAssignment` whose linked round is `ROUND_ACTIVE` AND `workspaceEnabled = true` | +| APPLICANT | Any `Project` led by user with at least one `ProjectRoundState` in a non-terminal state in an active round | +| OBSERVER | Always false (observers have nothing to act on) | +| AUDIENCE | Always false | + +Algorithm: + +1. Try roles in priority order. Return the first role whose predicate is true. +2. If no role has actionable work, return the highest-priority role the user holds (static fallback). +3. Always end with a non-null route (worst case: any signed-in user has at least their primary role). + +**Why this matters (your example):** a juror+observer who logs in during an open jury round lands on `/jury` (because they have a pending evaluation), not `/observer`. A mentor+juror logs in during an active MENTORING round → `/mentor`. After both rounds close, same user logs in → static fallback (jury > mentor) → `/jury`. The role switcher in the user menu is always available to override. + +**Decision: context-aware, not "remember last view".** "Remember last view" requires a new column and surprises users when their last context disappears (round closed, role removed). Context-aware is deterministic, explains itself, and handles the cross-role overlap cleanly. The role switcher dropdown is the user's escape hatch. + +**Tests** (in PR 6): +- Juror with pending evaluation in active round + Observer → `/jury` +- Juror with no active assignments + Observer → `/jury` (fallback to static priority) +- Mentor+Juror, MENTORING round active, no jury work → `/mentor` +- Mentor+Juror, both rounds active with work in both → `/jury` (priority order breaks the tie) +- Observer-only user → `/observer` +- Multi-role with no active work anywhere → static-priority fallback **2. Bulk juror→mentor promotion** on `/admin/members`: - Add row checkboxes to the Members table (already a table — confirm during impl). @@ -254,11 +282,42 @@ if (target) redirect(target) **5. Banner shows all roles:** - When `session.user.roles.length > 1`, render comma-separated list: "Impersonating Dr. Sophie Laurent (JURY MEMBER, MENTOR)". +**6. Standardize the role-switcher (location + presentation):** + +Today's state: +- Header layouts (`role-nav.tsx`) — used by jury, mentor, applicant, observer, award-master — put the user menu **top-right** with role-switcher items inside the dropdown. +- Admin layout (`admin-sidebar.tsx`) puts the user menu **bottom-left of the sidebar** with its own duplicate `ROLE_SWITCH_OPTIONS` constant + `switchableRoles` filter (lines 161, 191, 377-401). + +Two problems: (a) duplicated logic across two files; (b) different physical placement, so a multi-role user has to learn two patterns to find "Switch View". + +Changes: + +- **Extract a shared module** at `src/components/layouts/role-switcher.tsx` exporting: + - `useRoleSwitcher()` hook returning `{ switchableRoles: Array<{ label, path, icon }>, currentBasePath }`. Both `role-nav.tsx` and `admin-sidebar.tsx` import this. Source of truth for `ROLE_SWITCH_OPTIONS` lives here only. + - `RoleSwitcherMenuItems` component — renders the dropdown items (used inside both layouts' user menus). Keeps rendering inline-consistent. + - `RoleSwitcherPill` component — a standalone visible button that renders just outside the user-menu dropdown, with label "Switch View" + the icon of the next-best alternate role. Visible only when `switchableRoles.length > 0`. Click opens a small popover listing alternates. + +- **Place the `RoleSwitcherPill` in a consistent location across all layouts**: top-right of the header, immediately to the LEFT of the notifications bell. For the admin layout (sidebar-based), add a top-right header strip that hosts the pill + notifications + theme toggle, mirroring the other dashboards. (The admin sidebar keeps everything else; just the top-bar is added.) + + Why top-right: that's where the existing role-nav layouts already put switching/profile actions. Admins gain the pill in the same spot — no learning curve when switching from /admin to /jury. + +- **Pill behavior:** + - Hidden if `switchableRoles.length === 0` (single-role users see nothing — clean default). + - Hidden when `isImpersonating` (impersonator UX is already different; the existing impersonation banner with "Return to Admin" handles role-switching for that path). + - On hover/focus: shows tooltip "Switch dashboard view". + - Keyboard: `Cmd+Shift+V` shortcut opens the popover (nice-to-have; ship if it doesn't add much code). + +- **Admin sidebar bottom user pill stays** (so admin users can still sign out / open settings from there). The role-switcher items are removed from that menu — they live exclusively in the new pill + the user-dropdown's switch list. (Avoids three places to switch view.) + +**Acceptance for §D.6:** any signed-in user with `roles.length > 1` sees a "Switch View" pill in the same screen position regardless of which dashboard they're currently in. + **Tests** (in PR 6): -- Post-login redirect honors priority for multi-role user. +- `user.getDefaultDashboard` test cases enumerated above (juror+observer with active jury round → /jury; etc.). - `bulkUpdateRoles` adds MENTOR to N users and sends N onboarding emails. - Idempotency: second `bulkUpdateRoles` with same input does NOT resend email. - Impersonation banner does not intercept clicks on user dropdown (Playwright e2e if available). +- `RoleSwitcherPill` renders in the top-right of every dashboard for multi-role users; renders nothing for single-role users. +- Single shared `useRoleSwitcher` source means changing `ROLE_SWITCH_OPTIONS` updates both layouts simultaneously. --- @@ -452,7 +511,10 @@ Each PR independently revertable. PRs 1, 2, 4 ship with no migration → straigh - `/admin/mentors` shows real list of MENTOR-role users with current assignments. **PR 6 (§D + §F.2):** -- Multi-role user (jury+mentor) lands on `/jury` after login (priority order). Role switcher dropdown shows "Mentor View". +- Juror+observer logging in during an active jury round lands on `/jury` (context-aware default). Same user logging in after the round closes lands on `/jury` via static fallback (still highest-priority role they hold). +- Mentor+juror with active mentoring assignments and no jury work lands on `/mentor`. +- `RoleSwitcherPill` ("Switch View") renders in the top-right of the header on every dashboard for multi-role users, in the same screen position regardless of layout. Single-role users don't see it. +- Admin sidebar still has the user pill at the bottom-left for sign-out / settings; role-switcher entries are removed from that menu (live in the new pill instead). - `/admin/members` allows multi-select + "Add MENTOR role to selected" → all selected users get email + role. - Impersonation banner doesn't intercept clicks on the user dropdown. - Mentor `/mentor` dashboard shows "Recent Messages" card; applicant `/applicant` dashboard shows "Conversation with [Mentor]" card.