import type { MedicationRequest, MedicationStatement } from "fhir/r4"; import { formatDate, formatMonthYear, formatShortDate } from "../../../../utils/dateUtils"; import { createdAtOrganization } from "../../../../utils/createdAtOrganization"; import { fhirCalendarDate } from "../../../../utils/practiceTimezone"; import { medicationNameOf } from "../../../../utils/medicationName"; import { requestMeaning, statementMeaning, statementSource, type MedicationMeaning } from "../../../../utils/medicationMeaning"; /** * One line on the patient's medication list. * * A PRESCRIPTION AND A DECLARATION LAND ON THE SAME LIST, and the row says which it is. The * approved deck: "what you are actually taking is the answer a doctor needs - but every row says * which it is." Keeping them in one shape here is what stops the page branching on resource type. */ export type MedicationRow = { /** Resource-qualified, so an order and a statement with the same id are two rows. */ id: string; /** Where the row opens; absent for a record with no id, which is shown but cannot be opened. */ path?: string; name: string; schedule?: string; /** Who put them on it, or when they told us. */ provenance: string; /** * The practice the row came from, shown beside the prescriber. * * Spencer, 2026-08-29: wherever a practitioner is named, the organization is named with them - a * patient can hold clinicians at several practices, and the name alone does not say which. */ organization?: string; /** True when the patient (or someone acting for them) declared it themselves. */ own: boolean; /** Current, another record kept with its own words, or past. */ group: "current" | "other" | "past"; /** For a current row: a recorded prescription or reported use. */ kind?: "prescription" | "reported-use"; /** For a current statement: who reported it, from its own source. */ source?: "patient" | "helper" | "clinician" | "unknown"; /** The practice's id, whose calendar a fill-by instant is read on. */ organizationId?: string; /** * `dispenseRequest.validityPeriod.end`: the last day the prescription can be filled. It says nothing * about how much medicine is left, so it is never shown as days left. */ validUntil?: string; }; export type MedicationLists = { current: MedicationRow[]; other: MedicationRow[]; past: MedicationRow[]; prescribedCount: number; /** Reported by the patient themselves. */ ownCount: number; /** Reported by someone caring for the patient. */ helperCount: number; /** Reported with a clinician or practice as its source. */ clinicianCount: number; /** Reported with no source this system can name. */ unsourcedCount: number; }; const pastTense = (schedule: string | undefined, past: boolean) => past && schedule ? `Was ${schedule.charAt(0).toLowerCase()}${schedule.slice(1)}` : schedule; const groupOf = (meaning: Exclude) => meaning.group; const labelOf = (meaning: MedicationMeaning) => ("label" in meaning ? meaning.label : ""); const requestRow = (request: MedicationRequest, index: number): MedicationRow | null => { const meaning = requestMeaning(request); if (meaning.group === "hidden") return null; const prescriber = request.requester?.display?.trim(); const past = meaning.group === "past"; // Spencer 2026-09-01: a prescription is one written "within an encounter in our system", so the row names the visit. const visit = request.encounter ? formatShortDate(request.authoredOn) : ""; // The requester is who is named on the order, never taken as who stopped or proposed it. const provenance = meaning.group === "current" ? [prescriber ? `Prescribed by ${prescriber}` : "Prescribed for you", visit && `visit on ${visit}`].filter(Boolean).join(" · ") : [labelOf(meaning), prescriber && `named on the order: ${prescriber}`].filter(Boolean).join(" · "); return { id: request.id ? `MedicationRequest/${request.id}` : `MedicationRequest#unidentified-${index}`, path: request.id ? `medication-requests/${request.id}` : undefined, name: medicationNameOf(request) ?? "Prescription", schedule: pastTense(request.dosageInstruction?.[0]?.text?.trim(), past), provenance, organization: createdAtOrganization(request).name, organizationId: createdAtOrganization(request).id, own: false, group: groupOf(meaning), ...(meaning.group === "current" ? { kind: meaning.kind } : {}), validUntil: request.dispenseRequest?.validityPeriod?.end, }; }; const statementRow = (statement: MedicationStatement, index: number): MedicationRow | null => { const meaning = statementMeaning(statement); if (meaning.group === "hidden") return null; const source = statementSource(statement); const own = source.who === "patient" || source.who === "helper"; const past = meaning.group === "past"; // The date a statement was asserted, never taken as the day it was stopped. const added = formatMonthYear(statement.dateAsserted); const recordedOn = formatDate(statement.dateAsserted); const reason = statement.statusReason?.[0]?.text?.trim() || statement.note?.[0]?.text?.trim(); const who = source.who === "patient" ? "You" : source.who === "helper" ? source.name || "Someone caring for you" : source.who === "clinician" ? source.name || "A clinician" : undefined; // Who reported it and when it was asserted, never who stopped it or when; a past row keeps its recorded reason. const provenance = meaning.group === "current" ? [who ? (own ? `${who} added this` : `Recorded by ${who}`) : "Source not recorded", added].filter(Boolean).join(" · ") : [ labelOf(meaning), who ? `reported by ${source.who === "patient" ? "you" : who}` : "source not recorded", recordedOn && `recorded ${recordedOn}`, ].filter(Boolean).join(" · ") + (reason && past ? ` - ${reason}` : ""); return { id: statement.id ? `MedicationStatement/${statement.id}` : `MedicationStatement#unidentified-${index}`, path: statement.id ? `medication-statements/${statement.id}` : undefined, name: medicationNameOf(statement) ?? (own ? "A medicine you added" : "A medicine"), schedule: pastTense(statement.dosage?.[0]?.text?.trim(), past), provenance, organization: createdAtOrganization(statement).name, own, group: groupOf(meaning), ...(meaning.group === "current" ? { kind: meaning.kind, source: source.who } : {}), }; }; /** * The lists the page renders, and the counts above the first one. * * MedicationAdministration is deliberately absent: a dose given in the room belongs to the visit * that gave it, and the page says so in words rather than by quietly omitting it. */ export const buildMedicationLists = ( requests: readonly MedicationRequest[] | undefined, statements: readonly MedicationStatement[] | undefined, ): MedicationLists => { const rows = [ ...(requests ?? []).map(requestRow), ...(statements ?? []).map(statementRow), ].filter((row): row is MedicationRow => row !== null); const current = rows.filter((row) => row.group === "current"); const reported = current.filter((row) => row.kind === "reported-use"); return { current, other: rows.filter((row) => row.group === "other"), past: rows.filter((row) => row.group === "past"), prescribedCount: current.filter((row) => row.kind === "prescription").length, ownCount: reported.filter((row) => row.source === "patient").length, helperCount: reported.filter((row) => row.source === "helper").length, clinicianCount: reported.filter((row) => row.source === "clinician").length, unsourcedCount: reported.filter((row) => row.source === "unknown").length, }; }; /** "4 · 2 prescribed, 2 you added" - the count line beside the heading, by where each current row came from. */ export const currentSummary = ({ current, prescribedCount, ownCount, helperCount, clinicianCount, unsourcedCount }: MedicationLists): string => `${current.length} · ${prescribedCount} prescribed, ${ownCount} you added` + (helperCount > 0 ? `, ${helperCount} added by someone caring for you` : "") + (clinicianCount > 0 ? `, ${clinicianCount} recorded by a clinician` : "") + (unsourcedCount > 0 ? `, ${unsourcedCount} with no recorded source` : ""); /** * The fill deadline, read on the practice's calendar; nothing once it has passed. No renewal wording: * nothing we read records a renewal request, and `priorPrescription` marks a replacement. Only a current * prescription has one: a proposal, an unknown intent or an ended order with a validity period does not. */ export const fillByLine = ( row: Pick, today: string, zone: string, ): string | undefined => { if (row.group !== "current" || row.kind !== "prescription" || !row.validUntil) return undefined; const lastDay = fhirCalendarDate(row.validUntil, zone); if (!/^\d{4}-\d{2}-\d{2}$/.test(lastDay) || lastDay < today) return undefined; return `Prescription can be filled until ${formatDate(lastDay)}`; };