// sub-task 5a Task 9 - the PURE half of "who is in the meeting". Deliberately dependency-free // (no service contexts, no React): `MeetingParticipantsPanel.tsx` imports these to do the actual // fetching, but the resolution RULES live here so `meetingParticipants.test.ts` can exercise them in // plain node without pulling in `environment.ts` (which touches `window` at module scope and only // survives under jsdom). // // NEVER RENDER THE RAW REFERENCE. `Practitioner/9f2c-...` means nothing to a patient and leaks an // internal id into a room a related person may be sitting in. Every path through `resolveParticipantLabel` // below ends in either a real name or one of two fixed, PHI-free fallbacks. /** The three resource types this client can actually resolve a name for - one fetch each, via their * own service context. Anything else (a type with no fetch available) is treated as unresolvable * rather than guessed at. */ export type ResolvableParticipantType = 'Practitioner' | 'Patient' | 'RelatedPerson'; export const RESOLVABLE_PARTICIPANT_TYPES: ResolvableParticipantType[] = ['Practitioner', 'Patient', 'RelatedPerson']; /** Fallback #2's copy. Deliberately generic ("A practitioner", not a name) - this is what renders * when the type is known but no usable name resolved (never fetched, fetch failed, or the resource has * no name on file). */ export const PARTICIPANT_NEUTRAL_LABEL: Record = { Practitioner: 'A practitioner', Patient: 'The patient', RelatedPerson: 'A related person', }; /** Splits `externalUserId` into a FHIR reference's `{ResourceType}/{id}` - and nothing further is ever * done with the raw string once this returns. `null` for anything that isn't a genuine two-part * `Type/id` shape, including an absent value; the caller reads `null` as fallback 3. */ export function parseParticipantReference(externalUserId: string | undefined): { type: string; id: string } | null { if (!externalUserId) return null; const slash = externalUserId.indexOf('/'); if (slash <= 0 || slash === externalUserId.length - 1) return null; return { type: externalUserId.slice(0, slash), id: externalUserId.slice(slash + 1) }; } /** THE THREE FALLBACKS, IN ORDER, and none may leak the raw reference: * 1. a resolved, non-empty name use it * 2. no resolvable name but a known resource type that type's neutral label * 3. absent/unparseable `externalUserId`, OR a type this client has no fetch for `'Someone'` * * `resolvedName` must already be `undefined` for "no usable name" - NEVER `''`. `formatFhirHumanName` * returns `''` for an unusable name, and `'' ?? x` is still `''` (nullish coalescing does not catch an * empty string) - that exact bug shipped once already and produced a window titled "Notes - ". The * caller converts with `||`, never `??`, before this runs. */ export function resolveParticipantLabel( externalUserId: string | undefined, resolvedName: string | undefined, ): string { const ref = parseParticipantReference(externalUserId); if (!ref) return 'Someone'; if (resolvedName) return resolvedName; return RESOLVABLE_PARTICIPANT_TYPES.includes(ref.type as ResolvableParticipantType) ? PARTICIPANT_NEUTRAL_LABEL[ref.type as ResolvableParticipantType] : 'Someone'; } /** Up to two initials from a display label, for a name card or a roster avatar. */ export const initialsOf = (label: string): string => label.split(/\s+/).filter(Boolean).slice(0, 2).map((word) => word[0]).join('').toUpperCase();