/** * Rank strength-bearing products above substances in the drug pickers. * * The drug fields bind the US Core medication set, which by definition includes ingredient-level * codes (`metformin`) alongside products (`metformin hydrochloride 500 MG Oral Tablet`). Snowstorm * orders matches shortest-first, so the vaguest term types always win the top of the list and a * clinician typing "metfor" sees substances first - the exact thing a prescription cannot use. * * THIS IS NOT A SUBSTITUTION AND DOES NOT BELONG IN compositeValueSetExpansion.ts. That table works * around value sets the server REFUSES and records each refusal verbatim; the medication set now * expands fine. The reason here is rank. The prescribable set is a strict subset of the parent, so * asking it first and the parent second, then deduping, yields the identical set of concepts - * reordered, with the products in front. * * THE SLOTS ARE RESERVED, NOT FIRST-COME. Prescribable alone answers "metfor" with 135 rows; left to * fill the whole page it would push the ingredient-level `metformin` off the screen entirely, which * is the opposite bug. A few rows are held back for the parent's own matches, so the dose-unknown * case stays one glance away. */ import { mergeExpansionContains } from './compositeValueSetExpansion.ts'; export const US_CORE_MEDICATION_CODES = 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1010.4'; export const PRESCRIBABLE_MEDICATIONS = 'http://akashichealthrecords.com/fhir/ValueSet/prescribable-medications'; export type SearchTiers = { /** Expanded first; its matches lead the list. */ readonly first: string; /** Rows the picker shows, and the `count` each tier is asked for. */ readonly limit: number; /** Rows held back for the parent's own matches when the lead tier could fill them all. */ readonly reserved: number; }; export const MEDICATION_SEARCH_TIERS: Readonly> = { [US_CORE_MEDICATION_CODES]: { first: PRESCRIBABLE_MEDICATIONS, limit: 20, reserved: 5 }, }; export const searchTiersFor = (valueSetUrl: string): SearchTiers | undefined => MEDICATION_SEARCH_TIERS[valueSetUrl]; type Concept = { system?: string; code?: string; display?: string }; const key = (c: Concept): string => `${c.system ?? ''}|${c.code ?? ''}`; /** * Merge the lead tier and the rest into one page. * * A tier that failed (`undefined`) or answered with nothing is a fallback trigger: the other tier * fills the page alone. When both answer, the lead takes up to `limit - reserved` rows and the * rest - minus anything the lead already holds - takes what is left; either side absorbs the * other's slack, so a rare drug with two products still fills the page. */ export const mergeSearchTiers = ( first: readonly Concept[] | undefined, rest: readonly Concept[] | undefined, { limit, reserved }: Pick, ): Concept[] => { const lead = mergeExpansionContains([first]); if (lead.length === 0) return mergeExpansionContains([rest]).slice(0, limit); const held = new Set(lead.map(key)); const remainder = mergeExpansionContains([rest]).filter((c) => !held.has(key(c))); const keep = Math.min(reserved, remainder.length); const leadTake = Math.min(lead.length, limit - keep); return [...lead.slice(0, leadTake), ...remainder.slice(0, limit - leadTake)]; };