import { ReactNode, useCallback, createContext, useContext, useRef } from 'react'; import { useHttpService } from './HttpService.tsx'; import { environment } from '../environments/environment.ts'; import { Questionnaire } from 'fhir/r4'; import { isSubstitutedValueSet, mergeExpansionContains, resolveExpansionUrls, } from '../utils/terminology/compositeValueSetExpansion.ts'; import { mergeSearchTiers, searchTiersFor } from '../utils/terminology/medicationSearchTiers.ts'; import { sortTimezoneOptions } from '../utils/timezoneOptions.ts'; import { hideUncomputedTotals } from '../utils/loincUncomputedTotals.ts'; export interface IValueSet { system: string; code: string; display: string; } const valueSetUrls: { [key: string]: string } = { // Questionnaire publicationStatus: 'http://hl7.org/fhir/ValueSet/publication-status', resourceTypes: 'http://hl7.org/fhir/ValueSet/resource-types', itemType: 'http://hl7.org/fhir/ValueSet/item-type', questionnaireQuestions: 'http://hl7.org/fhir/ValueSet/questionnaire-questions', questionnaireEnableOperator: 'http://hl7.org/fhir/ValueSet/questionnaire-enable-operator', questionnaireItemControl: 'http://hl7.org/fhir/ValueSet/questionnaire-item-control', questionnaireEnableBehavior: 'http://hl7.org/fhir/ValueSet/questionnaire-enable-behavior', choiceListOrientation: 'http://hl7.org/fhir/ValueSet/choice-list-orientation', questionnaireDisplayCategory: 'http://hl7.org/fhir/ValueSet/questionnaire-display-category', questionnaireUsageMode: 'http://hl7.org/fhir/ValueSet/questionnaire-usage-mode', signatureType: 'http://hl7.org/fhir/ValueSet/signature-type', // Patient birthsex: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1021.24', sex: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1240.3', genderIdentity: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1021.32', // US Core MOVED these, it did not merely rename them. The four // hl7.org/fhir/us/core/ValueSet/{omb-,detailed-}{race,ethnicity} URLs used here existed in // US Core 4 and 6 and are ABSENT from 7, 8 and 9 - on a current server they resolve to // nothing at all. Below are the canonical URLs the us-core-race and us-core-ethnicity // extensions bind to in 9.0.0, read out of the StructureDefinitions rather than guessed. // // Following the publisher's own rebinding is not the same as re-binding a field ourselves: // which concepts a clinician may record is US Core's decision here, not ours. Verified // expanding on the terminology server 2026-09-04 - 9, 924, 5 and 48 concepts respectively. ombRaceCategory: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.4.642.2.575', detailedRace: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.25', ombEthnicityCategory: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.4.642.40.2.48.3', detailedEthnicity: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.4.642.40.2.48.1', tribalEntityUS: 'http://terminology.hl7.org/ValueSet/v3-TribalEntityUS', // Locations daysOfWeek: 'http://hl7.org/fhir/ValueSet/days-of-week', locationStatus: 'http://hl7.org/fhir/ValueSet/location-status', bedStatus: 'http://terminology.hl7.org/ValueSet/v2-0116', locationMode: 'http://hl7.org/fhir/ValueSet/location-mode', serviceDeliveryLocationRoleType: 'http://terminology.hl7.org/ValueSet/v3-ServiceDeliveryLocationRoleType', usCoreHealthcareServiceLocation: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.31', cmsPlaceOfServiceCodes: 'http://terminology.hl7.org/ValueSet/CMSPlaceOfServiceCodes', locationPhysicalType: 'http://terminology.hl7.org/ValueSet/location-physical-type', // HealthcareService serviceCategory: 'http://hl7.org/fhir/ValueSet/service-category', serviceType: 'http://hl7.org/fhir/ValueSet/service-type', // search practiceCodes: 'http://hl7.org/fhir/ValueSet/c80-practice-codes', serviceProvisionConditions: 'http://hl7.org/fhir/ValueSet/service-provision-conditions', program: 'http://hl7.org/fhir/ValueSet/program', languages: 'http://hl7.org/fhir/ValueSet/languages', serviceReferralMethod: 'http://hl7.org/fhir/ValueSet/service-referral-method', // Task taskStatus: 'http://hl7.org/fhir/ValueSet/task-status', taskIntent: 'http://hl7.org/fhir/ValueSet/task-intent', requestPriority: 'http://hl7.org/fhir/ValueSet/request-priority', taskCode: 'http://hl7.org/fhir/ValueSet/task-code|4.0.1', performerRole: 'http://hl7.org/fhir/ValueSet/performer-role', // search // PractitionerRole careTeamMemberFunction: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1099.30', practitionerSpecialty: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.114222.4.11.1066', usCoreProviderRole: 'http://hl7.org/fhir/us/core/ValueSet/us-core-provider-role', // HumanName nameUse: 'http://hl7.org/fhir/ValueSet/name-use', // Address addressUse: 'http://hl7.org/fhir/ValueSet/address-use', addressType: 'http://hl7.org/fhir/ValueSet/address-type', // uspsState: 'http://hl7.org/fhir/us/core/ValueSet/us-core-usps-state', uspsState: 'http://terminology.hl7.org/ValueSet/USPS-State', // ContactPoint contactPointSystem: 'http://hl7.org/fhir/ValueSet/contact-point-system', contactPointUse: 'http://hl7.org/fhir/ValueSet/contact-point-use', // Identifier identifierType: 'http://hl7.org/fhir/ValueSet/identifier-type', identifierUse: 'http://hl7.org/fhir/ValueSet/identifier-use', // Practitioner administrativeGender: 'http://hl7.org/fhir/ValueSet/administrative-gender', qualifications: 'http://terminology.hl7.org/ValueSet/v2-0360', maritalStatus: 'http://hl7.org/fhir/ValueSet/marital-status', // RelatedPerson relatedPersonRelationshipType: 'http://hl7.org/fhir/ValueSet/relatedperson-relationshiptype', // Organization organizationType: 'http://hl7.org/fhir/ValueSet/organization-type', // CommunicationRequest requestStatus: 'http://hl7.org/fhir/ValueSet/request-status', communicationCategory: 'http://hl7.org/fhir/ValueSet/communication-category', participationMode: 'http://terminology.hl7.org/ValueSet/v3-ParticipationMode', actReason: 'http://terminology.hl7.org/ValueSet/v3-ActReason', eventStatus: 'http://hl7.org/fhir/ValueSet/event-status', // Communication communicationNotDoneReason: 'http://hl7.org/fhir/ValueSet/communication-not-done-reason', communicationTopic: 'http://hl7.org/fhir/ValueSet/communication-topic', clinicalFindings: 'http://hl7.org/fhir/ValueSet/clinical-findings', // search // QuestionnaireResponse questionnaireresponseMode: 'http://hl7.org/fhir/ValueSet/questionnaireresponse-mode', questionnaireAnswersStatus: 'http://hl7.org/fhir/ValueSet/questionnaire-answers-status', historyStatus: 'http://hl7.org/fhir/ValueSet/history-status', historyAbsentReason: 'http://hl7.org/fhir/ValueSet/history-absent-reason', familyMember: 'http://terminology.hl7.org/ValueSet/v3-FamilyMember', conditionOutcome: 'http://hl7.org/fhir/ValueSet/condition-outcome', medicationCategory: 'http://hl7.org/fhir/ValueSet/medication-statement-category', medicationForm: 'http://hl7.org/fhir/ValueSet/medication-form-codes', route: 'http://hl7.org/fhir/ValueSet/route-codes', doseUnit: 'http://hl7.org/fhir/ValueSet/ucum-units', frequency: 'http://hl7.org/fhir/ValueSet/timing-abbreviation', medicationStatusReason: 'http://hl7.org/fhir/ValueSet/reason-medication-status-codes', // BodyStructure bodystructureCode: 'http://hl7.org/fhir/ValueSet/bodystructure-code', // search bodySite: 'http://hl7.org/fhir/ValueSet/body-site', // search bodystructureRelativeLocation: 'http://hl7.org/fhir/ValueSet/bodystructure-relative-location', // AllergyIntolerance allergyIntoleranceClinical: 'http://hl7.org/fhir/ValueSet/allergyintolerance-clinical', allergyIntoleranceVerification: 'http://hl7.org/fhir/ValueSet/allergyintolerance-verification', allergyIntoleranceType: 'http://hl7.org/fhir/ValueSet/allergy-intolerance-type', allergyIntoleranceCategory: 'http://hl7.org/fhir/ValueSet/allergy-intolerance-category', allergyIntoleranceCriticality: 'http://hl7.org/fhir/ValueSet/allergy-intolerance-criticality', allergyIntoleranceCode: 'http://hl7.org/fhir/ValueSet/allergyintolerance-code', // search usCoreAllergyIntoleranceCode: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1186.8', // search substanceCode: 'http://hl7.org/fhir/ValueSet/substance-code', // search reactionEventSeverity: 'http://hl7.org/fhir/ValueSet/reaction-event-severity', routeCodes: 'http://hl7.org/fhir/ValueSet/route-codes', // search // Flag flagStatus: 'http://hl7.org/fhir/ValueSet/flag-status', flagCategory: 'http://hl7.org/fhir/ValueSet/flag-category', flagCode: 'http://hl7.org/fhir/ValueSet/flag-code', // Condition conditionClinical: 'http://hl7.org/fhir/ValueSet/condition-clinical', conditionVerStatus: 'http://hl7.org/fhir/ValueSet/condition-ver-status', conditionCategory: 'http://hl7.org/fhir/ValueSet/condition-category', conditionSeverity: 'http://hl7.org/fhir/ValueSet/condition-severity', conditionCode: 'http://hl7.org/fhir/ValueSet/condition-code', // search conditionStage: 'http://hl7.org/fhir/ValueSet/condition-stage', // search conditionStageType: 'http://hl7.org/fhir/ValueSet/condition-stage-type', // search manifestationOrSymptom: 'http://hl7.org/fhir/ValueSet/manifestation-or-symptom', // search usCoreProblemOrHealthConcern: 'http://hl7.org/fhir/us/core/ValueSet/us-core-problem-or-health-concern', // Procedure procedureNotPerformedReason: 'http://hl7.org/fhir/ValueSet/procedure-not-performed-reason', // search procedureCategory: 'http://hl7.org/fhir/ValueSet/procedure-category', procedureCode: 'http://hl7.org/fhir/ValueSet/procedure-code', // search usCoreProcedureCode: 'http://hl7.org/fhir/us/core/ValueSet/us-core-procedure-code', usCoreConditionCode: 'http://hl7.org/fhir/us/core/ValueSet/us-core-condition-code', // The diagnosis on a lab or imaging order. ICD-10-CM's valid codes, as the implicit all-codes // set - no publisher ships a ValueSet for it. The server holds the billable codes only, so a // category header (E11) is never offered. icd10cmDiagnosis: 'http://hl7.org/fhir/sid/icd-10-cm?fhir_vs', // search procedureReason: 'http://hl7.org/fhir/ValueSet/procedure-reason', // search procedureOutcome: 'http://hl7.org/fhir/ValueSet/procedure-outcome', procedureFollowup: 'http://hl7.org/fhir/ValueSet/procedure-followup', deviceAction: 'http://hl7.org/fhir/ValueSet/device-action', // search deviceKind: 'http://terminology.hl7.org/ValueSet/device-kind', // search // AdverseEvent adverseEventActuality: 'http://hl7.org/fhir/ValueSet/adverse-event-actuality', adverseEventCategory: 'http://hl7.org/fhir/ValueSet/adverse-event-category', adverseEventType: 'http://hl7.org/fhir/ValueSet/adverse-event-type', // search adverseEventSeriousness: 'http://hl7.org/fhir/ValueSet/adverse-event-seriousness', adverseEventSeverity: 'http://hl7.org/fhir/ValueSet/adverse-event-severity', adverseEventOutcome: 'http://hl7.org/fhir/ValueSet/adverse-event-outcome', adverseEventCausalityAssess: 'http://hl7.org/fhir/ValueSet/adverse-event-causality-assess', adverseEventCausalityMethod: 'http://hl7.org/fhir/ValueSet/adverse-event-causality-method', // ClinicalImpression clinicalimpressionStatus: 'http://hl7.org/fhir/ValueSet/clinicalimpression-status', investigationSets: 'http://hl7.org/fhir/ValueSet/investigation-sets', clinicalimpressionPrognosis: 'http://hl7.org/fhir/ValueSet/clinicalimpression-prognosis', // DetectedIssue observationStatus: 'http://hl7.org/fhir/ValueSet/observation-status', detectedissueCategory: 'http://hl7.org/fhir/ValueSet/detectedissue-category', detectedissueSeverity: 'http://hl7.org/fhir/ValueSet/detectedissue-severity', detectedissueMitigationAction: 'http://hl7.org/fhir/ValueSet/detectedissue-mitigation-action', // Immunization immunizationStatus: 'http://hl7.org/fhir/ValueSet/immunization-status', immunizationStatusReason: 'http://hl7.org/fhir/ValueSet/immunization-status-reason', // CDC vaccine manufacturers. Immunization.manufacturer is a Reference in R4, so the chosen // code is stored as manufacturer.identifier, not as a CodeableConcept. mvxManufacturer: 'http://hl7.org/fhir/sid/mvx?fhir_vs', // search vaccineCode: 'http://hl7.org/fhir/ValueSet/vaccine-code', usCoreVaccineCode: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1010.6', immunizationOrigin: 'http://hl7.org/fhir/ValueSet/immunization-origin', immunizationSite: 'http://hl7.org/fhir/ValueSet/immunization-site', immunizationRoute: 'http://hl7.org/fhir/ValueSet/immunization-route', immunizationFunction: 'http://hl7.org/fhir/ValueSet/immunization-function', immunizationReason: 'http://hl7.org/fhir/ValueSet/immunization-reason', immunizationSubpotentReason: 'http://hl7.org/fhir/ValueSet/immunization-subpotent-reason', immunizationProgramEligibility: 'http://hl7.org/fhir/ValueSet/immunization-program-eligibility', immunizationFundingSource: 'http://hl7.org/fhir/ValueSet/immunization-funding-source', immunizationTargetDisease: 'http://hl7.org/fhir/ValueSet/immunization-target-disease', // ImmunizationEvaluation immunizationEvaluationStatus: 'http://hl7.org/fhir/ValueSet/immunization-evaluation-status', immunizationEvaluationTargetDisease: 'http://hl7.org/fhir/ValueSet/immunization-evaluation-target-disease', immunizationEvaluationDoseStatus: 'http://hl7.org/fhir/ValueSet/immunization-evaluation-dose-status', immunizationEvaluationDoseStatusReason: 'http://hl7.org/fhir/ValueSet/immunization-evaluation-dose-status-reason', // ImmunizationRecommendation immunizationRecommendationTargetDisease: 'http://hl7.org/fhir/ValueSet/immunization-recommendation-target-disease', immunizationRecommendationStatus: 'http://hl7.org/fhir/ValueSet/immunization-recommendation-status', immunizationRecommendationReason: 'http://hl7.org/fhir/ValueSet/immunization-recommendation-reason', immunizationRecommendationDateCriterion: 'http://hl7.org/fhir/ValueSet/immunization-recommendation-date-criterion', // MedicationStatement medicationStatementStatus: 'http://hl7.org/fhir/ValueSet/medication-statement-status', reasonMedicationStatusCodes: 'http://hl7.org/fhir/ValueSet/reason-medication-status-codes', medicationStatementCategory: 'http://hl7.org/fhir/ValueSet/medication-statement-category', // US Core binds MedicationRequest, MedicationDispense and Medication to the RxNorm set below, // extensibly; the SNOMED medication-codes set it used to point at carries no strength. // Historical SNOMED codes still resolve through $lookup for display. Same URL as // usCoreMedicationCodes on purpose, so every drug field converges on one definition. medicationCodes: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1010.4', // search // Dosage additionalInstructionCodes: 'http://hl7.org/fhir/ValueSet/additional-instruction-codes', medicationAsNeededReason: 'http://hl7.org/fhir/ValueSet/medication-as-needed-reason', // search approachSiteCodes: 'http://hl7.org/fhir/ValueSet/approach-site-codes', // search administrationMethodCodes: 'http://hl7.org/fhir/ValueSet/administration-method-codes', doseRateType: 'http://hl7.org/fhir/ValueSet/dose-rate-type', // Timing timingAbbreviation: 'http://hl7.org/fhir/ValueSet/timing-abbreviation', eventTiming: 'http://hl7.org/fhir/ValueSet/event-timing', // Medication medicationStatus: 'http://hl7.org/fhir/ValueSet/medication-status', medicationFormCodes: 'http://hl7.org/fhir/ValueSet/medication-form-codes', usCoreMedicationCodes: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1010.4', // search // Strength-bearing RxNorm products only; asked FIRST for the set above so products lead the // list. Listed here so the conformance probe proves it expands. See medicationSearchTiers.ts. prescribableMedications: 'http://akashichealthrecords.com/fhir/ValueSet/prescribable-medications', // search tier // Encounter encounterStatus: 'http://hl7.org/fhir/ValueSet/encounter-status', actEncounterCode: 'http://terminology.hl7.org/ValueSet/v3-ActEncounterCode', encounterType: 'http://hl7.org/fhir/ValueSet/encounter-type', usCoreEncounterType: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.23', actPriority: 'http://terminology.hl7.org/ValueSet/v3-ActPriority', diagnosisRole: 'http://hl7.org/fhir/ValueSet/diagnosis-role', encounterAdmitSource: 'http://hl7.org/fhir/ValueSet/encounter-admit-source', reAdmissionIndicator: 'http://terminology.hl7.org/ValueSet/v2-0092', encounterDiet: 'http://hl7.org/fhir/ValueSet/encounter-diet', encounterSpecialCourtesy: 'http://hl7.org/fhir/ValueSet/encounter-special-courtesy', encounterSpecialArrangements: 'http://hl7.org/fhir/ValueSet/encounter-special-arrangements', encounterDischargeDisposition: 'http://hl7.org/fhir/ValueSet/encounter-discharge-disposition', encounterLocationStatus: 'http://hl7.org/fhir/ValueSet/encounter-location-status', interpreterRequired: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.16', // Consent consentStateCodes :'http://hl7.org/fhir/ValueSet/consent-state-codes', consentScope: 'http://hl7.org/fhir/ValueSet/consent-scope', consentCategory: 'http://hl7.org/fhir/ValueSet/consent-category', consentPolicy: 'http://hl7.org/fhir/ValueSet/consent-policy', consentProvisionType: 'http://hl7.org/fhir/ValueSet/consent-provision-type', securityRoleType: 'http://hl7.org/fhir/ValueSet/security-role-type', consentAction: 'http://hl7.org/fhir/ValueSet/consent-action', securityLabels: 'http://hl7.org/fhir/ValueSet/security-labels', purposeOfUse: 'http://terminology.hl7.org/ValueSet/v3-PurposeOfUse', consentContentClass: 'http://hl7.org/fhir/ValueSet/consent-content-class', // search consentContentCode: 'http://hl7.org/fhir/ValueSet/consent-content-code', // search consentDataMeaning: 'http://hl7.org/fhir/ValueSet/consent-data-meaning', // Slot slotstatus: 'http://hl7.org/fhir/ValueSet/slotstatus', // Appointment appointmentstatus: 'http://hl7.org/fhir/ValueSet/appointmentstatus', appointmentCancellationReason: 'http://hl7.org/fhir/ValueSet/appointment-cancellation-reason', appointmentReasonCode: 'http://terminology.hl7.org/ValueSet/v2-0276', encounterReason: 'http://hl7.org/fhir/ValueSet/encounter-reason', // search encounterParticipantType: 'http://hl7.org/fhir/ValueSet/encounter-participant-type', participantrequired: 'http://hl7.org/fhir/ValueSet/participantrequired', participationstatus: 'http://hl7.org/fhir/ValueSet/participationstatus', // EpisodeOfCare episodeOfCareStatus: 'http://hl7.org/fhir/ValueSet/episode-of-care-status', episodeofcareType: 'http://hl7.org/fhir/ValueSet/episodeofcare-type', // Observation observationCategory: 'http://hl7.org/fhir/ValueSet/observation-category', observationCodes: 'http://hl7.org/fhir/ValueSet/observation-codes', // search usCoreVitalSignsCodes: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.88.12.80.62', // search dataAbsentReason: 'http://hl7.org/fhir/ValueSet/data-absent-reason', observationInterpretation: 'http://hl7.org/fhir/ValueSet/observation-interpretation', observationMethods: 'http://hl7.org/fhir/ValueSet/observation-methods', // search referencerangeMeaning: 'http://hl7.org/fhir/ValueSet/referencerange-meaning', referencerangeAppliesto: 'http://hl7.org/fhir/ValueSet/referencerange-appliesto', // search ucumVitalsCommon: 'http://hl7.org/fhir/ValueSet/ucum-vitals-common', ucumBodylength: 'http://hl7.org/fhir/ValueSet/ucum-bodylength', ucumBodytemp: 'http://hl7.org/fhir/ValueSet/ucum-bodytemp', ucumBodyweight: 'http://hl7.org/fhir/ValueSet/ucum-bodyweight', usCoreObservationAdiDocumentationOptions: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.16', usCoreClinicalResultObservationCategory: 'http://hl7.org/fhir/us/core/ValueSet/us-core-clinical-result-observation-category', usCoreObservationOccupationOptions2: 'http://phinvads.cdc.gov/fhir/ValueSet/2.16.840.1.114222.4.11.7901', usCoreObservationPregnancyintentOptions: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1166.22', usCoreObservationPregnancystatusOptions: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1240.12', usCoreScreeningAssessmentObservationCategory: 'http://hl7.org/fhir/us/core/ValueSet/us-core-screening-assessment-observation-category', usCoreObservationScreeningAssessmentCodes: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.13', usCoreObservationSexualOrientationOptions: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1240.11', usCoreSimpleObservationCategory: 'http://hl7.org/fhir/us/core/ValueSet/us-core-simple-observation-category', usCoreSmokingstatusCodes: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.6', unitsOfMeasureCaseSensitive: 'http://terminology.hl7.org/ValueSet/v3-UnitsOfMeasureCaseSensitive', usCoreSmokingstatusOptions: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.3', // DiagnosticReport diagnosticReportStatus: 'http://hl7.org/fhir/ValueSet/diagnostic-report-status', diagnosticServiceSections: 'http://hl7.org/fhir/ValueSet/diagnostic-service-sections', reportCodes: 'http://hl7.org/fhir/ValueSet/report-codes', // search usCoreLaboratoryTestCodes: 'http://hl7.org/fhir/us/core/ValueSet/us-core-laboratory-test-codes', // search usCorediagnosticreportReportAndNoteCodes: 'http://hl7.org/fhir/us/core/ValueSet/us-core-diagnosticreport-report-and-note-codes', // search // MedicationRequest medicationrequestStatus: 'http://hl7.org/fhir/ValueSet/medicationrequest-status', medicationrequestStatusReason: 'http://hl7.org/fhir/ValueSet/medicationrequest-status-reason', medicationrequestIntent: 'http://hl7.org/fhir/ValueSet/medicationrequest-intent', medicationrequestCategory: 'http://hl7.org/fhir/ValueSet/medicationrequest-category', medicationrequestCourseOfTherapy: 'http://hl7.org/fhir/ValueSet/medicationrequest-course-of-therapy', actSubstanceAdminSubstitutionCode: 'http://terminology.hl7.org/ValueSet/v3-ActSubstanceAdminSubstitutionCode', substanceAdminSubstitutionReason: 'http://terminology.hl7.org/ValueSet/v3-SubstanceAdminSubstitutionReason', // MedicationAdministration medicationAdminStatus: 'http://hl7.org/fhir/ValueSet/medication-admin-status', reasonMedicationNotGivenCodes: 'http://hl7.org/fhir/ValueSet/reason-medication-not-given-codes', medicationAdminCategory: 'http://hl7.org/fhir/ValueSet/medication-admin-category', medAdminPerformFunction: 'http://hl7.org/fhir/ValueSet/med-admin-perform-function', reasonMedicationGivenCodes: 'http://hl7.org/fhir/ValueSet/reason-medication-given-codes', // MedicationDispense medicationdispenseStatus: 'http://hl7.org/fhir/ValueSet/medicationdispense-status', medicationdispenseStatusReason: 'http://hl7.org/fhir/ValueSet/medicationdispense-status-reason', medicationdispenseCategory: 'http://hl7.org/fhir/ValueSet/medicationdispense-category', medicationdispensePerformerFunction: 'http://hl7.org/fhir/ValueSet/medicationdispense-performer-function', actPharmacySupplyType: 'http://terminology.hl7.org/ValueSet/v3-ActPharmacySupplyType', ucumUnits: 'http://hl7.org/fhir/ValueSet/ucum-units', // search usCoreRouteCodes: 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.22', // search // Device udiEntryType: 'http://hl7.org/fhir/ValueSet/udi-entry-type', deviceStatus: 'http://hl7.org/fhir/ValueSet/device-status', deviceStatusReason: 'http://hl7.org/fhir/ValueSet/device-status-reason', deviceNametype: 'http://hl7.org/fhir/ValueSet/device-nametype', deviceType: 'http://hl7.org/fhir/ValueSet/device-type', // search // DeviceUseStatement deviceStatementStatus: 'http://hl7.org/fhir/ValueSet/device-statement-status', // DeviceRequest requestIntent: 'http://hl7.org/fhir/ValueSet/request-intent', participantRole: 'http://hl7.org/fhir/ValueSet/participant-role', // search // VisionPrescription fmStatus: 'http://hl7.org/fhir/ValueSet/fm-status', visionProduct: 'http://hl7.org/fhir/ValueSet/vision-product', visionEyeCodes: 'http://hl7.org/fhir/ValueSet/vision-eye-codes', visionBaseCodes: 'http://hl7.org/fhir/ValueSet/vision-base-codes', // RiskAssessment riskProbability: 'http://hl7.org/fhir/ValueSet/risk-probability', // NutritionOrder foodType: 'http://hl7.org/fhir/ValueSet/food-type', // search dietType: 'http://hl7.org/fhir/ValueSet/diet-type', nutrientCode: 'http://hl7.org/fhir/ValueSet/nutrient-code', textureCode: 'http://hl7.org/fhir/ValueSet/texture-code', modifiedFoodtype: 'http://hl7.org/fhir/ValueSet/modified-foodtype', consistencyType: 'http://hl7.org/fhir/ValueSet/consistency-type', supplementType: 'http://hl7.org/fhir/ValueSet/supplement-type', entformulaType: 'http://hl7.org/fhir/ValueSet/entformula-type', entformulaAdditive: 'http://hl7.org/fhir/ValueSet/entformula-additive', enteralRoute: 'http://hl7.org/fhir/ValueSet/enteral-route', // Goal goalStatus: 'http://hl7.org/fhir/ValueSet/goal-status', goalAchievement: 'http://hl7.org/fhir/ValueSet/goal-achievement', goalCategory: 'http://hl7.org/fhir/ValueSet/goal-category', goalPriority: 'http://hl7.org/fhir/ValueSet/goal-priority', goalStartEvent: 'http://hl7.org/fhir/ValueSet/goal-start-event', usCoreGoalDescription: 'http://hl7.org/fhir/us/core/ValueSet/us-core-goal-description', // CarePlan carePlanIntent: 'http://hl7.org/fhir/ValueSet/care-plan-intent', carePlanCategory: 'http://hl7.org/fhir/ValueSet/care-plan-category', carePlanActivityOutcome: 'http://hl7.org/fhir/ValueSet/care-plan-activity-outcome', carePlanActivityKind: 'http://hl7.org/fhir/ValueSet/care-plan-activity-kind', carePlanActivityStatus: 'http://hl7.org/fhir/ValueSet/care-plan-activity-status', // CareTeam careTeamStatus: 'http://hl7.org/fhir/ValueSet/care-team-status', careTeamCategory: 'http://hl7.org/fhir/ValueSet/care-team-category', // DocumentReference documentReferenceStatus: 'http://hl7.org/fhir/ValueSet/document-reference-status', compositionStatus: 'http://hl7.org/fhir/ValueSet/composition-status', c80DocTypecodes: 'http://hl7.org/fhir/ValueSet/c80-doc-typecodes', // search documentClasscodes: 'http://hl7.org/fhir/ValueSet/document-classcodes', documentRelationshipType: 'http://hl7.org/fhir/ValueSet/document-relationship-type', formatcodes: 'http://hl7.org/fhir/ValueSet/formatcodes', v3ActCode: 'http://terminology.hl7.org/ValueSet/v3-ActCode', // search c80Facilitycodes: 'http://hl7.org/fhir/ValueSet/c80-facilitycodes', c80PracticeCodes: 'http://hl7.org/fhir/ValueSet/c80-practice-codes', mediaStatus: 'http://hl7.org/fhir/ValueSet/event-status', mediaType: 'http://hl7.org/fhir/ValueSet/media-type', mediaView: 'http://hl7.org/fhir/ValueSet/media-view', mediaModality: 'http://hl7.org/fhir/ValueSet/media-modality', // misc timezones: 'http://hl7.org/fhir/ValueSet/timezones' }; const API_URL = environment.apiUrl; /** One LOINC question as the catalogue route returns it: answers already in their LOINC order. */ export interface LoincCatalogueQuestion { code: string; text: string; datatype: string | null; answers: { code: string | null; display: string; sequence: number | null; score: number | null }[]; units: { unit: string }[]; } /** One LOINC form or panel as the catalogue route returns it. */ export interface LoincCatalogueForm { code: string; display: string; } // A catalogue answer without its list is a failed read, not an empty one. const catalogueResults = (response: { results?: unknown } | undefined): T[] => { if (!Array.isArray(response?.results)) throw new Error('The LOINC catalogue answered without results.'); return response.results as T[]; }; // An absent expansion is a failed read, except when the server states the total is zero. const readExpansion = (response: any): { outcome: 'ok'; contains: any[] } | { outcome: 'unavailable' } => { const expansion = response?.expansion; if (!expansion) return { outcome: 'unavailable' }; if (Array.isArray(expansion.contains)) return { outcome: 'ok', contains: expansion.contains }; if (expansion.contains === undefined && expansion.total === 0) return { outcome: 'ok', contains: [] }; return { outcome: 'unavailable' }; }; export const useValueSetService = () => { const { get } = useHttpService(); const cacheRef = useRef>({}); // FROM THE ENVIRONMENT, NOT HARDCODED. This was the only endpoint in the plugin written as a // literal, so when the old terminology host was retired every coded field went blank and // nothing in a stack switch would have caught it. const terminologyServerUrl = environment.terminologyApi; // const fhirUrl = 'https://lforms-fhir.nlm.nih.gov/baseR4/Questionnaire'; const ucumSystemUrl = 'http://unitsofmeasure.org'; const daysOfWeek: IValueSet[] = [ { code: 'mon', display: 'Monday', system: 'http://hl7.org/fhir/days-of-week' }, { code: 'tue', display: 'Tuesday', system: 'http://hl7.org/fhir/days-of-week' }, { code: 'wed', display: 'Wednesday', system: 'http://hl7.org/fhir/days-of-week' }, { code: 'thu', display: 'Thursday', system: 'http://hl7.org/fhir/days-of-week' }, { code: 'fri', display: 'Friday', system: 'http://hl7.org/fhir/days-of-week' }, { code: 'sat', display: 'Saturday', system: 'http://hl7.org/fhir/days-of-week' }, { code: 'sun', display: 'Sunday', system: 'http://hl7.org/fhir/days-of-week' } ]; const ageUnits: IValueSet[] = [ { code: 'min', display: 'minutes', system: ucumSystemUrl }, { code: 'h', display: 'hours', system: ucumSystemUrl }, { code: 'd', display: 'days', system: ucumSystemUrl }, { code: 'wk', display: 'weeks', system: ucumSystemUrl }, { code: 'mo', display: 'months', system: ucumSystemUrl }, { code: 'a', display: 'years', system: ucumSystemUrl } ]; const quantityComparators: IValueSet[] = [ { code: '<', display: 'Less than', system: 'http://hl7.org/fhir/quantity-comparator'}, { code: '<=', display: 'Less or Equal to', system: 'http://hl7.org/fhir/quantity-comparator'}, { code: '>=', display: 'Greater or Equal to', system: 'http://hl7.org/fhir/quantity-comparator'}, { code: '>', display: 'Greater than', system: 'http://hl7.org/fhir/quantity-comparator'}, ]; const durationUnits: IValueSet[] = [ { code: 'ms', display: 'milliseconds', system: ucumSystemUrl }, { code: 's', display: 'seconds', system: ucumSystemUrl }, ...ageUnits ]; const unitsOfTime: IValueSet[] = [ { code: 's', display: 'seconds', system: ucumSystemUrl }, { code: 'min', display: 'minutes', system: ucumSystemUrl }, { code: 'h', display: 'hours', system: ucumSystemUrl }, { code: 'd', display: 'days', system: ucumSystemUrl}, { code: 'wk', display: 'weeks', system: ucumSystemUrl }, { code: 'mo', display: 'months', system: ucumSystemUrl }, { code: 'a', display: 'years', system: ucumSystemUrl } ]; const processValueSet = useCallback((expansion: any): IValueSet[] => { const flattenValueSet = (items: any[]): IValueSet[] => { return items.flatMap(item => { if (item.abstract || item.contains) { return flattenValueSet(item.contains || []); } return [{ code: item.code, display: item.display, system: item.system }]; }); }; return flattenValueSet(expansion.contains || []); }, []); const groupValueSet = useCallback((expansion: any): Record => { const groupValueSet = (items: any[]): Record => { return items.reduce((acc, item) => { if (item.abstract) { // Group child items under the abstract item's code acc[item.code] = (item.contains || []).map((child: any) => ({ code: child.code, display: child.display, system: child.system })); } else { // Add ungrouped items directly under their system or default group const groupKey = item.system || 'Ungrouped'; if (!acc[groupKey]) { acc[groupKey] = []; } acc[groupKey].push({ code: item.code, display: item.display, system: item.system }); } return acc; }, {} as Record); }; return groupValueSet(expansion.contains || []); }, []); const fetchValueSetInternalWithStatus = useCallback(async (valueSetUrl: string, grouped: boolean = false, filter?: string): Promise<{ outcome: 'ok'; results: IValueSet[] | Record } | { outcome: 'unavailable' }> => { if (!valueSetUrl) { console.error('fetchValueSet called with no value set URL - look for a `valueSetUrls.` that is not defined in the map.'); return { outcome: 'unavailable' }; } const [baseUrl, version] = valueSetUrl.split('|'); const isTaskCode = baseUrl === valueSetUrls.taskCode.split('|')[0]; let expandUrl = `${terminologyServerUrl}/ValueSet/$expand?url=${encodeURIComponent(baseUrl)}`; if (filter) { expandUrl += `&filter=${encodeURIComponent(filter)}`; } if (version) { expandUrl += `&version=${encodeURIComponent(version)}`; // For task-code, we need to explicitly specify the code system version if (isTaskCode) { expandUrl += `&system-version=http://hl7.org/fhir/CodeSystem/task-code|${encodeURIComponent(version)}`; } } expandUrl += `&incomplete-ok=true`; try { // - two value sets this server REFUSES to expand, for two different reasons, are // asked for as the pieces they are composed of and merged. Both pieces expand 200 on their // own; the refusals are about the parents, not the content. Nothing is re-bound - the // substitution reproduces the published definition utils/terminology. // The `|version` suffix is deliberately NOT carried into a substitution: the children are // different value sets with their own versions. Neither substituted set is requested with // one today, and a caller that starts to would be asking a question this cannot answer. // Drug search: products first, then the rest of the published set. Each tier is asked // for the page size; a failed products tier falls back to the parent, but a failed // parent is unavailable. Unfiltered fetches fall through - there is nothing to rank. const tiers = filter ? searchTiersFor(baseUrl) : undefined; if (tiers && filter) { const expandTier = (tierUrl: string) => get( `${terminologyServerUrl}/ValueSet/$expand?url=${encodeURIComponent(tierUrl)}` + `&filter=${encodeURIComponent(filter)}&count=${tiers.limit}&incomplete-ok=true`, { bypassAuth: true, bypassOrganization: true }, ) .then((part) => { const read = readExpansion(part); return read.outcome === 'ok' ? read.contains as IValueSet[] : undefined; }) .catch((error) => { console.error(`Tier ${tierUrl} failed:`, error); return undefined; }); const [first, rest] = await Promise.all([expandTier(tiers.first), expandTier(baseUrl)]); if (rest === undefined) return { outcome: 'unavailable' }; const contains = rest.length === 0 ? [] : mergeSearchTiers(first, rest, tiers); const expansion = { contains }; return { outcome: 'ok', results: grouped ? groupValueSet(expansion) : processValueSet(expansion) }; } if (isSubstitutedValueSet(baseUrl)) { const parts = await Promise.all( resolveExpansionUrls(baseUrl).map((partUrl) => { let url = `${terminologyServerUrl}/ValueSet/$expand?url=${encodeURIComponent(partUrl)}`; if (filter) url += `&filter=${encodeURIComponent(filter)}`; url += `&incomplete-ok=true`; return get(url, { bypassAuth: true, bypassOrganization: true }) .then((part) => readExpansion(part)); }), ); if (parts.some((part) => part.outcome === 'unavailable')) { return { outcome: 'unavailable' }; } const contains = mergeExpansionContains(parts.map((part) => part.outcome === 'ok' ? part.contains : undefined)); if (contains.length !== 0) { const expansion = { contains }; return { outcome: 'ok', results: grouped ? groupValueSet(expansion) : processValueSet(expansion) }; } return { outcome: 'ok', results: grouped ? {} : [] }; } const response = await get(expandUrl, { bypassAuth: true, bypassOrganization: true }); const read = readExpansion(response); if (read.outcome === 'ok') { const expansion = { contains: read.contains }; return { outcome: 'ok', results: grouped ? groupValueSet(expansion) : processValueSet(expansion) }; } if (isTaskCode && version) { const codeSystemUrl = `${terminologyServerUrl}/CodeSystem/task-code?version=${encodeURIComponent(version)}`; const codeSystemResponse = await get(codeSystemUrl, { bypassAuth: true, bypassOrganization: true }); if (Array.isArray(codeSystemResponse?.concept)) { const valueSetItems: IValueSet[] = codeSystemResponse.concept.map((concept: any) => ({ code: concept.code, display: concept.display || concept.code, system: 'http://hl7.org/fhir/CodeSystem/task-code' })); return { outcome: 'ok', results: valueSetItems }; } } return { outcome: 'unavailable' }; } catch (error) { console.error('Error fetching value set:', error); // For task-code, if the first approach fails, try a hardcoded fallback if (isTaskCode && version === '4.0.1') { console.log('Using fallback for task-code 4.0.1'); return { outcome: 'ok', results: [ { code: 'approve', display: 'Approve', system: 'http://hl7.org/fhir/CodeSystem/task-code' }, { code: 'fulfill', display: 'Fulfill', system: 'http://hl7.org/fhir/CodeSystem/task-code' }, { code: 'abort', display: 'Abort', system: 'http://hl7.org/fhir/CodeSystem/task-code' }, { code: 'replace', display: 'Replace', system: 'http://hl7.org/fhir/CodeSystem/task-code' }, { code: 'change', display: 'Change', system: 'http://hl7.org/fhir/CodeSystem/task-code' }, { code: 'suspend', display: 'Suspend', system: 'http://hl7.org/fhir/CodeSystem/task-code' } ] }; } return { outcome: 'unavailable' }; } }, [get, processValueSet, groupValueSet, terminologyServerUrl]); const fetchValueSetInternal = useCallback(async (valueSetUrl: string, grouped: boolean = false, filter?: string): Promise> => { const fetched = await fetchValueSetInternalWithStatus(valueSetUrl, grouped, filter); if (fetched.outcome === 'unavailable') { return grouped ? {} : []; } return fetched.results; }, [fetchValueSetInternalWithStatus]); const fetchValueSet = useCallback(async (url: string, isRecord = false): Promise => { if (cacheRef.current[url]) { return cacheRef.current[url]; } const fetched = await fetchValueSetInternalWithStatus(url, isRecord); if (fetched.outcome === 'unavailable') { return isRecord ? {} : []; } cacheRef.current[url] = fetched.results; return fetched.results; }, [fetchValueSetInternalWithStatus]); /** * The IANA time zones, sorted for reading. `utils/timezoneOptions.ts` carries the reasoning, and * the reason it is fetched through one function rather than sorted in each form: three screens offer * this field and only one of them had the sort. */ const fetchTimezoneOptions = useCallback(async (): Promise => ( sortTimezoneOptions(await fetchValueSet(valueSetUrls.timezones)) ), [fetchValueSet]); const clearCache = useCallback(() => { cacheRef.current = {}; }, []); const fetchFromLoinc = useCallback(async (code: string): Promise => { const params = new URLSearchParams(); params.append('code', code); // A scored instrument's total is hidden on the way in: Akashic does not compute it. return hideUncomputedTotals(await get(`${API_URL}/LOINC?${params.toString()}`)); }, [get]); const fetchUnit = useCallback((unit: string) => { // const url = `${loincBaseUrl}/api/ucum/v3/search?df=cs_code,name,guidance&maxList=500&terms=${encodeURIComponent(searchTerm)}`; // return get(url); const params = new URLSearchParams(); params.append('unit', unit); return get<{ total: number; results: IValueSet[]; }>(`${API_URL}/UCUM?${params.toString()}`); }, [get]); // The questionnaire builder catalogue, read through our own route. Unlike searchLoincCodes these throw on a failed // read, so the builder can say the catalogue was unavailable instead of showing no matches. const searchQuestions = useCallback(async (searchTerm: string): Promise => { const params = new URLSearchParams({ catalogue: 'question', terms: searchTerm.trim() }); return catalogueResults(await get<{ results?: unknown }>(`${API_URL}/LOINC?${params.toString()}`)); }, [get]); const getLoincPanel = useCallback(async (code: string): Promise => { const params = new URLSearchParams({ catalogue: 'panel', code }); const response = await get<{ definition?: unknown }>(`${API_URL}/LOINC?${params.toString()}`); if (!response?.definition || typeof response.definition !== 'object') throw new Error('The LOINC panel definition could not be read.'); return response.definition; }, [get]); const searchQuestionnaires = useCallback(async (searchTerm: string): Promise => { const params = new URLSearchParams({ catalogue: 'form', terms: searchTerm.trim() }); return catalogueResults(await get<{ results?: unknown }>(`${API_URL}/LOINC?${params.toString()}`)); }, [get]); /** * THROUGH OUR OWN ENDPOINT, not straight to NIH. Spencer's ruling, 2026-08-29: "wrap it". * * This called clinicaltables.nlm.nih.gov directly from the browser - no wrapper, no fallback, an * availability dependency on a public endpoint sitting in the middle of recording a clinical * result. `/R4/LOINC` already existed for the code lookup, already holds the licensed credentials, * and already falls back to NIH; it now answers `terms` as well, so the credentials and the * fallback stay in one place rather than being copied to a second clinical field. * * The row-shape unpacking that used to live here moved to the handler with it. Nothing in the * browser knows what NIH's positional array looks like any more, which is the point. */ const searchLoincCodes = useCallback(async (searchTerm: string): Promise => { const trimmed = searchTerm.trim(); if (trimmed.length < 2) return []; const params = new URLSearchParams(); params.append('terms', trimmed); try { const response = await get<{ source: string; results: IValueSet[] }>( `${API_URL}/LOINC?${params.toString()}`, ); return Array.isArray(response?.results) ? response.results : []; } catch (error) { console.error('LOINC search failed:', error); return []; } }, [get]); /** * The same search, saying WHETHER IT WORKED. * * `searchLoincCodes` above must keep returning `[]` on failure -- that is a deliberate ruling * (fail soft, never throw into a form somebody is filling in) and it is pinned inside its catch. * But an empty array cannot say which of two things happened, and its own comment promises "the * field says so", which the field has no way to do. * * THAT AMBIGUITY IS NOT HYPOTHETICAL. The terminology server answers an $expand of the US Core * diagnostic value sets with HTTP 200 and ZERO codes, no warning -- measured 2026-08-28. A * Diagnostic Report type-ahead returned nothing for `hemoglobin A1c` and nothing for `blood`, * read as "your search matched nothing", and went unexplained for weeks. * * So a caller that must tell the difference uses this one. Same request, same wrapped endpoint, * same soft failure -- it just reports which happened instead of erasing the distinction. */ /** * A live value-set search that says WHETHER IT WORKED. * * `fetchValueSetInternal` above must keep returning `[]` on failure - it feeds every coded control * in the product and failing soft into a form somebody is filling in is the right ruling. But an * empty array cannot say which of two things happened, and a caller that must tell the difference * has nothing to read. This is `searchLoincCodesWithStatus`'s twin, for the other terminology. * * THAT AMBIGUITY IS NOT HYPOTHETICAL, AND IT WAS MEASURED IN THE PRODUCT. On 2026-09-04 the * terminology server stopped accepting connections during a rebuild. A clinician searching the * record for `metformin` got six lab tests and no drugs at all, with nothing on screen saying the * drug lookup was down - which reads as "there is no such drug" and sends them somewhere else. * The surface HAD a notice for exactly that and could never show it, because the failure arrived * as an empty list. * * IT IS THE SEARCH CASE ONLY, deliberately. No version pinning, no grouping, no compose-part walk, * no task-code fallback - all of which belong to the general fetch and none of which a type-ahead * uses. So this is not a second answer to "how do we expand a value set"; it is the one shape that * needs to report a failure. */ // EVERY CALL TO THE TERMINOLOGY SERVER GOES UNAUTHENTICATED, AND THAT IS NOT AN OPTIMISATION. // It is a different host from our API and it takes no token; its CORS preflight answers // `Access-Control-Allow-Headers: Content-Type, Accept, Accept-Language, Prefer`. So a request // that carries Authorization, DPoP or the organization headers is refused by the BROWSER before // it is sent, and arrives as `TypeError: Failed to fetch` with a CORS message - which reads as // the server being down rather than as our own header. // // Every other $expand in this file passes these; the type-ahead did not, so the medication and // diagnosis searches answered `unavailable` on every keystroke while the same value sets, asked // for by the resource forms one panel away, answered 200. const searchValueSetWithStatus = useCallback(async ( valueSetUrl: string, filter: string, ): Promise<{ outcome: 'ok'; results: IValueSet[] } | { outcome: 'unavailable' }> => { const trimmed = filter.trim(); if (!valueSetUrl || trimmed.length < 2) return { outcome: 'ok', results: [] }; const [baseUrl] = valueSetUrl.split('|'); // The type-ahead is the surface the drug ranking exists for, so the tiers apply here too - // without them this path leads with substances again and asks Snowstorm for its default page // on every keystroke. Availability is judged on the PARENT alone: the prescribable tier is an // addition for rank, the published set is the answer, and a prescribable tier that failed is // passed on as `undefined`, which merges to the parent alone. const tiers = searchTiersFor(baseUrl); if (tiers) { const expandTier = (tierUrl: string) => get<{ expansion?: { contains?: IValueSet[] } }>( `${terminologyServerUrl}/ValueSet/$expand?url=${encodeURIComponent(tierUrl)}` + `&filter=${encodeURIComponent(trimmed)}&count=${tiers.limit}&incomplete-ok=true`, { bypassAuth: true, bypassOrganization: true }, ) .then((response) => { const contains = response?.expansion?.contains; return Array.isArray(contains) ? contains : undefined; }) .catch((error) => { console.error('Value set search failed:', tierUrl, error); return undefined; }); const [first, parent] = await Promise.all([expandTier(tiers.first), expandTier(baseUrl)]); if (parent === undefined) return { outcome: 'unavailable' }; return { outcome: 'ok', results: mergeSearchTiers(first, parent, tiers) as IValueSet[] }; } const url = `${terminologyServerUrl}/ValueSet/$expand` + `?url=${encodeURIComponent(baseUrl)}` + `&filter=${encodeURIComponent(trimmed)}` + '&incomplete-ok=true'; try { const response = await get<{ expansion?: { contains?: IValueSet[] } }>(url, { bypassAuth: true, bypassOrganization: true, }); // A response with no expansion at all is not an empty expansion - it is a shape we do not // understand, and calling it "no matches" is the same erasure one layer along. const contains = response?.expansion?.contains; if (contains === undefined) return { outcome: 'unavailable' }; if (!Array.isArray(contains)) return { outcome: 'unavailable' }; return { outcome: 'ok', results: contains }; } catch (error) { console.error('Value set search failed:', valueSetUrl, error); return { outcome: 'unavailable' }; } }, [get, terminologyServerUrl]); const searchLoincCodesWithStatus = useCallback(async ( searchTerm: string, kind?: string, termsOnly?: boolean, ): Promise<{ outcome: 'ok'; results: IValueSet[]; filteredByKind: boolean } | { outcome: 'unavailable' }> => { const trimmed = searchTerm.trim(); if (trimmed.length < 2) return { outcome: 'ok', results: [], filteredByKind: true }; const params = new URLSearchParams(); params.append('terms', trimmed); if (kind) params.append('kind', kind); if (termsOnly) params.append('termsOnly', 'true'); try { const response = await get<{ source: string; results: IValueSet[]; filteredByKind?: boolean }>( `${API_URL}/LOINC?${params.toString()}`, ); // A response that is not a list is not an empty result -- it is a shape we do not // understand, and calling it "no matches" is the same erasure one layer along. if (!Array.isArray(response?.results)) return { outcome: 'unavailable' }; // Absent means unfiltered. A caller that asked for one kind and is told nothing must not // assume it got one, or the mixing this parameter exists to stop returns silently. return { outcome: 'ok', results: response.results, filteredByKind: response.filteredByKind === true }; } catch (error) { console.error('LOINC search failed:', error); return { outcome: 'unavailable' }; } }, [get]); const lookup = useCallback(async (code: string) => { const url = `${terminologyServerUrl}/CodeSystem/$lookup?_format=json&system=http://loinc.org&code=${code}`; return get(url, { bypassAuth: true, bypassOrganization: true }); }, [get, terminologyServerUrl]); const findValueSetItemByCode = useCallback((valueSet: IValueSet[], code: string): IValueSet | undefined => { return valueSet.find(item => item.code === code); }, []); const getDisplayForCode = useCallback((valueSet: IValueSet[], code: string): string => { const item = findValueSetItemByCode(valueSet, code); return item ? item.display : code; }, [findValueSetItemByCode]); // Live SNOMED search scoped by an ECL expression, via the terminology server's $expand (design ยง3). // Reuses the same $expand plumbing as fetchValueSetInternal, pointed at SNOMED's implicit ECL value set. const searchSnomed = useCallback(async (ecl: string, filter: string): Promise => { // The terminology server requires at least 3 characters for term search (HAPI-0389). if (filter.trim().length < 3) return []; const vsUrl = `http://snomed.info/sct?fhir_vs=ecl/${encodeURIComponent(ecl)}`; const expandUrl = `${terminologyServerUrl}/ValueSet/$expand?url=${encodeURIComponent(vsUrl)}` + `&filter=${encodeURIComponent(filter)}&count=20&incomplete-ok=true`; try { const response = await get<{ expansion?: { contains?: unknown[] } }>(expandUrl, { bypassAuth: true, bypassOrganization: true, }); return response.expansion?.contains ? processValueSet(response.expansion) : []; } catch (error) { console.error('SNOMED search failed:', error); return []; } }, [get, processValueSet, terminologyServerUrl]); return { ucumSystemUrl, daysOfWeek, ageUnits, durationUnits, unitsOfTime, quantityComparators, findValueSetItemByCode, getDisplayForCode, valueSetUrls, fetchValueSet, fetchTimezoneOptions, fetchValueSetInternal, fetchValueSetInternalWithStatus, clearCache, fetchFromLoinc, fetchUnit, searchQuestions, getLoincPanel, searchQuestionnaires, lookup, processValueSet, groupValueSet, searchSnomed, searchLoincCodes, searchLoincCodesWithStatus, searchValueSetWithStatus }; }; const ValueSetServiceContext = createContext | undefined>(undefined); export const ValueSetServiceProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const valueSetService = useValueSetService(); return ( {children} ); }; export const useValueSetServiceContext = () => { const context = useContext(ValueSetServiceContext); if (context === undefined) { throw new Error('useValueSetServiceContext must be used within a ValueSetServiceProvider'); } return context; };