import type { CodeableConcept } from "fhir/r4"; import { relationshipType } from "../../../components/FhirRelatedPerson/FhirRelations"; /** * WHAT A PROXY ROW SAYS ABOUT THE PERSON, FROM THE READER'S SIDE. * * `RelatedPerson.relationship` stores what the READER is to the subject. *Accounts I can access* is a * list of SUBJECTS. So a passthrough prints **"parent" underneath the child's name** - and it reads as * plausible, which is why it would ship: it is only wrong if you know which end of the relationship the * word describes. * * AND THIS IS WHY IT IS NOT A ONE-LINE FORMATTER FIX. `formatFhirCodeableConcept` reads only `.text`, * so a concept carrying only a coding renders empty - which looks like the bug. Teaching that shared * formatter to fall back to `coding[0].display` would change what a CodeableConcept renders as across a * dozen call sites, and a `display` string is whatever vocabulary the resource's author happened to use. * The inversion belongs HERE, at the one surface that knows it is looking at subjects, from a table this * product controls. * * THERE ARE TWO SOURCES OF RELATIONSHIP CODES AND A MAPPING THAT READS ONE IS HALF-BLIND * * The PICKER writes relationships a patient adds - twelve groups, ~40 codes, in `FhirRelations.tsx`. * The SERVER writes relationships the system creates when a dependent or co-guardian is set up: * `Patient/dependent.ts` writes `PRN`, and `coGuardianInvite.ts` writes `PRN`, `STPPRN` and `GUARD`. * * **None of those three appears in the picker.** A table keyed only on the picker's groups sends every * server-created guardian row to the fallback - which is the exact broken state this was raised about. * So the server codes are keyed on the CODE, and everything else on the GROUP. * * THE INVERSE IS NEVER GENDERED, AND THAT IS LOAD-BEARING * * *Mother* inverts to *son* or *daughter* depending on the SUBJECT's gender, which this surface does not * have - exactly as it does not have their age. **"Your child" needs no fact we do not hold.** A gendered * inverse would put the same class of guess back in, one field along. */ /** Keyed on the GROUP, because the group is what the picker writes and what the product controls. */ const GROUP_INVERSE: Record = { Partner: "your partner", Mother: "your child", Father: "your child", Daughter: "your parent", Son: "your parent", Sister: "your sibling", Brother: "your sibling", "Grand Parents": "your grandchild", "Grand Children": "your grandparent", "In Laws": "your relative by marriage", "Extended Family": "your relative", Unrelated: "someone you help", }; /** * Keyed on the CODE, because these three come from the server and are in no picker group. * * `GUARD` is "in your care" rather than "your dependent", and the wording is deliberate: a legal * guardian is not necessarily a parent and the subject is not necessarily a child - an adult daughter is * guardian for her father. "Your dependent" is how a benefits form talks; this is true in every * direction the code is used and still reads as a person rather than a case. * * `PRN` and `STPPRN` both invert to "your child". The step distinction describes the READER's * standing, not the subject's, and this list is naming the subject. */ const SERVER_CODE_INVERSE: Record = { PRN: "your child", STPPRN: "your child", GUARD: "in your care", }; /** * DERIVED FROM THE PICKER, never re-typed. Forty codes copied into this file would drift from the * control that writes them the first time somebody adds a relationship - and drift here is silent, * because an unmapped code renders as the fallback rather than as an error. */ const CODE_TO_GROUP: Record = Object.fromEntries( relationshipType.flatMap((group) => (group.items ?? []).map((item) => [item.code, group.label] as const), ), ); /** * What this surface says when it cannot invert the relationship. * * TWO DIFFERENT STATES SHARE IT, deliberately: nothing stored at all, and a code this table does not * know. `SPS` is the live example of the second - a real v3-RoleCode that nothing in the product writes, * but that imported or seeded data can carry. * * AN UNKNOWN CODE MUST NOT FALL THROUGH TO ITS OWN `display`. That is the passthrough this whole * module exists to prevent: the display names the READER's role, so showing it would print the wrong * person's relationship under the subject's name - the plausible-looking failure, not a loud one. */ export const PROXY_RELATIONSHIP_FALLBACK = "Someone whose account you can reach"; /** * The reader-side sentence for one proxy, or `undefined` when it cannot be said. * * `undefined` rather than the fallback, so each caller decides: the ROW stays quiet (a list of identical * "someone whose account you can reach" lines is noise), while the SHEET, which describes one person, * says it. */ export const proxyRelationshipLabel = ( // BOTH SHAPES, matching the shared formatter beside it. `MyProxy.relationship` is a // `CodeableConcept[]`, and a signature taking only the singular compiles against a cast and then // reads `.coding` off an ARRAY at runtime - undefined, every row silently unlabelled, which is // indistinguishable from the defect this replaces. relationship: CodeableConcept | CodeableConcept[] | undefined, ): string | undefined => { const concepts = Array.isArray(relationship) ? relationship : relationship ? [relationship] : []; for (const coding of concepts.flatMap((concept) => concept?.coding ?? [])) { const code = coding?.code; if (!code) continue; // The server's own codes first. They are not in any picker group, so a group lookup would miss // them and send the guardian rows - the ones this was raised about - to the fallback. const fromServer = SERVER_CODE_INVERSE[code]; if (fromServer) return fromServer; const group = CODE_TO_GROUP[code]; if (group && GROUP_INVERSE[group]) return GROUP_INVERSE[group]; } return undefined; };