| // The reviewer session identity: the name and role every confirmation is attributed to. It comes from | |
| // the per-user login (#128), set once when a reviewer signs in (LoginBar.tsx) and cleared on sign-out, | |
| // not a field re-entered on each decision. PRD sections 4.4 and 8.4: each confirmed value records who | |
| // confirmed it and in what role, so the identity has to be the session's. | |
| // | |
| // localStorage rather than sessionStorage, matching the session token: a reload should not drop a | |
| // reviewer out of their session. The corpus is public de-identified TCGA data, and this is a | |
| // local-first tool. | |
| // | |
| // Identity changes are observable. setIdentity and clearIdentity dispatch a window CustomEvent so a | |
| // screen reflects login and logout without a reload; subscribeIdentity also listens for the cross-tab | |
| // "storage" event so a sign-in in another tab updates this one. | |
| const NAME_KEY = "endopath.reviewerName"; | |
| const ROLE_KEY = "endopath.reviewerRole"; | |
| const IDENTITY_EVENT = "endopath:identity"; | |
| // The roles PRD section 4.4 distinguishes: a licensed pathologist confirms the pathology variables; a | |
| // data manager or curator abstracts the rest. Offered as a fixed set so the recorded role reads | |
| // consistently across the ledger. | |
| export const REVIEWER_ROLES = ["pathologist", "data manager", "curator"] as const; | |
| export type ReviewerRole = (typeof REVIEWER_ROLES)[number]; | |
| export interface ReviewerIdentity { | |
| name: string; | |
| role: string; | |
| } | |
| export function getIdentity(): ReviewerIdentity { | |
| return { | |
| name: localStorage.getItem(NAME_KEY) ?? "", | |
| role: localStorage.getItem(ROLE_KEY) ?? "", | |
| }; | |
| } | |
| export function setIdentity(identity: ReviewerIdentity): void { | |
| localStorage.setItem(NAME_KEY, identity.name.trim()); | |
| localStorage.setItem(ROLE_KEY, identity.role.trim()); | |
| window.dispatchEvent(new CustomEvent(IDENTITY_EVENT)); | |
| } | |
| export function clearIdentity(): void { | |
| localStorage.removeItem(NAME_KEY); | |
| localStorage.removeItem(ROLE_KEY); | |
| window.dispatchEvent(new CustomEvent(IDENTITY_EVENT)); | |
| } | |
| export function hasIdentity(): boolean { | |
| const { name, role } = getIdentity(); | |
| return name.trim().length > 0 && role.trim().length > 0; | |
| } | |
| // Subscribe to identity changes: the in-tab CustomEvent from setIdentity/clearIdentity, and the | |
| // cross-tab "storage" event so a sign-in or sign-out in another tab updates this one. Returns an | |
| // unsubscribe. | |
| export function subscribeIdentity(cb: () => void): () => void { | |
| window.addEventListener(IDENTITY_EVENT, cb); | |
| window.addEventListener("storage", cb); | |
| return () => { | |
| window.removeEventListener(IDENTITY_EVENT, cb); | |
| window.removeEventListener("storage", cb); | |
| }; | |
| } | |