From e417201367681b22bb9eafb0ecd3071ff6e30fad Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 05:50:31 +0000 Subject: [PATCH 1/2] Therapy: list the Review sign-off queue for site administrators only The owner's account now carries the site administrator role, so the queue's page-list entry can be limited to administrators. The route still opens for anyone and the server still refuses sign-offs from non-administrators. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01VRAKeUNKFmnQHMbMmnHWDW --- .../clinical-dashboard/master-search-header.tsx | 6 +++++- src/lib/mode-secondary-navigation.ts | 7 ++++--- tests/mode-secondary-navigation.test.ts | 8 ++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index f2128be3e2..4ce2be1b8b 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -137,6 +137,8 @@ import { visibleModeSecondaryNavigationEntries, } from "@/lib/mode-secondary-navigation"; import { readOnCallEditorFlag, subscribeOnCallEditorFlag } from "@/lib/on-call/device-state-keys"; +import { isAdministratorUser } from "@/lib/authorization"; +import { useAuthIfAvailable } from "@/lib/example-data/store"; import { modePageVisible, useOpenShiftsIsPoster, @@ -621,6 +623,8 @@ export function MasterSearchHeader(props: MasterSearchHeaderProps) { * needs no fetch of its own (F24). False on the server. */ const onCallEditor = useSyncExternalStore(subscribeOnCallEditorFlag, readOnCallEditorFlag, () => false); + // Administrator-only pages (the Therapy sign-off queue) list only for a site administrator. + const isAdministrator = isAdministratorUser(useAuthIfAvailable()?.session?.user); /** What the mode pill names (the page, the mode or a work area) and this address's work frame. */ const { activeModePage, @@ -1395,7 +1399,7 @@ export function MasterSearchHeader(props: MasterSearchHeaderProps) { // Hidden pages and editor-only pages leave the sheet; the pill still names // them when one is open, because `modeOwnPages` itself is unfiltered. const { main, tools, more } = groupModeSecondaryNavigationEntries( - visibleModeSecondaryNavigationEntries(modeOwnPages, { isEditor: onCallEditor }), + visibleModeSecondaryNavigationEntries(modeOwnPages, { isEditor: onCallEditor, isAdministrator }), ); return (
diff --git a/src/lib/mode-secondary-navigation.ts b/src/lib/mode-secondary-navigation.ts index 13469c9360..92f096e66e 100644 --- a/src/lib/mode-secondary-navigation.ts +++ b/src/lib/mode-secondary-navigation.ts @@ -93,12 +93,13 @@ export const modeSecondaryNavigationRegistry = { { id: "recommend", label: "Recommend", href: "/therapy-compass/recommend" }, { id: "compare", label: "Compare", href: "/therapy-compass/compare" }, { id: "pathways", label: "Pathways", href: "/therapy-compass/pathways" }, - // The qualified-clinician sign-off queue. It becomes `audience: "administrators"` once the owner's - // account carries the site administrator role; until then hiding it would hide it from the owner too. + // The qualified-clinician sign-off queue, listed for site administrators only (owner decision, + // 13 Oct 2026, once the owner's account carried the role). The route still opens for anyone and + // the server still refuses a sign-off from anyone else. // `group: "more"` keeps it registered (the route, the pill name, `activeId`) but off the main // tabs, which list only ungrouped entries (owner decision, October 2026). The Therapy search // page links to it, so it stays one tap away. - { id: "review", label: "Review", href: "/therapy-compass/review", group: "more" }, + { id: "review", label: "Review", href: "/therapy-compass/review", group: "more", audience: "administrators" }, ], // Two genuinely distinct surfaces: `/factsheets/search` (query + filters + // result rows) and `/factsheets/topics` (category browse). Search leads, the diff --git a/tests/mode-secondary-navigation.test.ts b/tests/mode-secondary-navigation.test.ts index a044377c25..ec86b3724e 100644 --- a/tests/mode-secondary-navigation.test.ts +++ b/tests/mode-secondary-navigation.test.ts @@ -643,6 +643,14 @@ describe("mode secondary navigation registry", () => { } }); + it("lists Therapy's sign-off queue for site administrators only", () => { + const entries = modeSecondaryNavigationEntries("therapy-compass"); + const ids = (isAdministrator: boolean) => + visibleModeSecondaryNavigationEntries(entries, { isEditor: true, isAdministrator }).map((entry) => entry.id); + expect(ids(false)).not.toContain("review"); + expect(ids(true)).toContain("review"); + }); + it("names the right On Call page in the pill, including the two editors and the picker", () => { const cases: Record = { "/on-call": "now", From 05859a7a85efb28aec08e236efcbfdc332035284 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 05:58:47 +0000 Subject: [PATCH 2/2] Therapy: show the search page's Review queue link to site administrators only Therapy has a results surface, so the top bar's page list never lists its pages; the header wiring had no effect and is reverted. The link on the Therapy search page is where readers actually reach Review, so that is what is now limited to administrators. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01VRAKeUNKFmnQHMbMmnHWDW --- .../master-search-header.tsx | 6 +--- .../therapy-compass/screens/search-screen.tsx | 34 +++++++++++-------- tests/therapy-compact-row.dom.test.tsx | 16 ++++++++- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 4ce2be1b8b..f2128be3e2 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -137,8 +137,6 @@ import { visibleModeSecondaryNavigationEntries, } from "@/lib/mode-secondary-navigation"; import { readOnCallEditorFlag, subscribeOnCallEditorFlag } from "@/lib/on-call/device-state-keys"; -import { isAdministratorUser } from "@/lib/authorization"; -import { useAuthIfAvailable } from "@/lib/example-data/store"; import { modePageVisible, useOpenShiftsIsPoster, @@ -623,8 +621,6 @@ export function MasterSearchHeader(props: MasterSearchHeaderProps) { * needs no fetch of its own (F24). False on the server. */ const onCallEditor = useSyncExternalStore(subscribeOnCallEditorFlag, readOnCallEditorFlag, () => false); - // Administrator-only pages (the Therapy sign-off queue) list only for a site administrator. - const isAdministrator = isAdministratorUser(useAuthIfAvailable()?.session?.user); /** What the mode pill names (the page, the mode or a work area) and this address's work frame. */ const { activeModePage, @@ -1399,7 +1395,7 @@ export function MasterSearchHeader(props: MasterSearchHeaderProps) { // Hidden pages and editor-only pages leave the sheet; the pill still names // them when one is open, because `modeOwnPages` itself is unfiltered. const { main, tools, more } = groupModeSecondaryNavigationEntries( - visibleModeSecondaryNavigationEntries(modeOwnPages, { isEditor: onCallEditor, isAdministrator }), + visibleModeSecondaryNavigationEntries(modeOwnPages, { isEditor: onCallEditor }), ); return (
diff --git a/src/components/therapy-compass/screens/search-screen.tsx b/src/components/therapy-compass/screens/search-screen.tsx index ebf2816984..5102ca017f 100644 --- a/src/components/therapy-compass/screens/search-screen.tsx +++ b/src/components/therapy-compass/screens/search-screen.tsx @@ -16,6 +16,8 @@ import { } from "@/components/clinical-dashboard/result-filter-control"; import { pageContainer } from "@/components/ui-primitives"; import { therapyScreenHref } from "@/lib/therapy-compass-navigation"; +import { isAdministratorUser } from "@/lib/authorization"; +import { useAuthIfAvailable } from "@/lib/example-data/store"; import { useTcBindings } from "../bindings"; import { matchesAvailability, matchesTopics } from "../data/select"; @@ -34,6 +36,7 @@ const MAX_CARDS = 24; export function SearchScreen() { const b = useTcBindings(); + const isAdministrator = isAdministratorUser(useAuthIfAvailable()?.session?.user); const q = b.search.query; // Nothing typed is browsing, not a search: show the list A to Z with no // "Best match" or "Why matched", so it never reads as the app having picked @@ -300,20 +303,23 @@ export function SearchScreen() { ) : null} {/* The sign-off queue left the main tabs (it is editorial work, not a - clinical destination). This quiet link keeps it one tap away. */} -

- -

+ clinical destination). This quiet link keeps it one tap away for a site + administrator, the only reader who can sign off (owner decision, 13 Oct 2026). */} + {isAdministrator ? ( +

+ +

+ ) : null} ); } diff --git a/tests/therapy-compact-row.dom.test.tsx b/tests/therapy-compact-row.dom.test.tsx index 046455229c..5f8d0b6780 100644 --- a/tests/therapy-compact-row.dom.test.tsx +++ b/tests/therapy-compact-row.dom.test.tsx @@ -41,6 +41,11 @@ function therapy(overrides: Partial = {}): Therapy { } const bindingsState = vi.hoisted(() => ({ results: [] as Therapy[] })); +const authState = vi.hoisted(() => ({ siteRole: undefined as string | undefined })); + +vi.mock("@/lib/example-data/store", () => ({ + useAuthIfAvailable: () => ({ session: { user: { app_metadata: { site_role: authState.siteRole } } } }), +})); vi.mock("@/components/therapy-compass/bindings", () => ({ useTcBindings: () => ({ @@ -118,8 +123,9 @@ describe("Therapy compact rows", () => { } }); - it("keeps the Review queue one tap away now that it has left the main tabs", () => { + it("keeps the Review queue one tap away for a site administrator", () => { bindingsState.results = [therapy()]; + authState.siteRole = "administrator"; render(); const link = screen.getByTestId("therapy-review-queue-link"); @@ -128,6 +134,14 @@ describe("Therapy compact rows", () => { expect(link).toHaveTextContent("12 to review"); }); + it("leaves the Review queue link out for anyone who is not a site administrator", () => { + bindingsState.results = [therapy()]; + authState.siteRole = undefined; + render(); + + expect(screen.queryByTestId("therapy-review-queue-link")).toBeNull(); + }); + it("still shows what the row used for Best used for and Best fit, word for word, in the record", () => { const record = therapy(); render();