import { useState, useCallback, createContext, useContext, ReactNode, useRef } from 'react'; import { overlayHeldBundle, writtenSince } from '../utils/writeThroughHold'; import { useCacheOwner } from '../utils/cacheOwner'; import { Bundle, Medication, CodeableConcept } from 'fhir/r4'; import { environment } from '../environments/environment.ts'; import { useFhirServiceContext } from './FhirService.tsx'; import { IValueSet } from './ValueSetService.tsx'; import { useHttpService } from './HttpService.tsx'; import { fetchAllPagesAsBundle } from '../utils/fetchAllPagesAsBundle'; import { searchPageSize } from '../utils/pageSizeOverride'; // - WAS `environment.healthLakeEndpoint`, A KEY THAT DOES NOT EXIST ON `environment`. // Every URL began with the literal string "undefined", became relative, resolved against the WordPress // origin and 404'd. The drug catalogue has never loaded. // DO NOT restore that key: it meant "talk to HealthLake directly from the browser", bypassing the // authorizer entirely. /R4/Medication now exists behind the gateway. // `fhirClinicalApi` ends in `/R4` with NO trailing slash, so the call sites take a `/` separator. const API_URL = environment.fhirClinicalApi; interface ExtendedMedication extends Medication { code: IValueSet; form: IValueSet; ingredient: { itemCodeableConcept: IValueSet; isActive: boolean; strength: { numerator: { value: number; unit: string; system: string; code: string; }; denominator: { value: number; unit: string; system: string; code: string; }; }; }[]; } export const useMedicationService = () => { const { get, post, put, delete: del } = useHttpService(); const { transformToCodeableConcept, transformFromCodeableConcept, unpackBundle } = useFhirServiceContext(); const [medicationsList, setMedicationsList] = useState([]); const [shouldReload, setShouldReload] = useState(false); const [lastUpserted, setLastUpserted] = useState(null); const [unsaved, setUnsaved] = useState(false); const [medicationsBundle, setMedicationsBundle] = useState | null>(null); // When the catalogue now held was asked for: set only by a published read, so a write since it reloads. const lastFetchTimeRef = useRef(0); // The catalogue as published, read by the cache check: the state a callback closed over may be an older one. const publishedRef = useRef | null>(null); const cacheOwner = useCacheOwner('Medication', () => { setLastUpserted(null); setMedicationsBundle(null); setMedicationsList([]); publishedRef.current = null; lastFetchTimeRef.current = 0; }); const fetchMedications = useCallback(async (): Promise> => { // The whole Medication catalogue with no bound stated at all. This is the read a prescriber's // picker is built from, so a page boundary here presents as "we don't carry that drug". // The list is the whole practice catalogue, and the hold is emptied when the practice changes. return fetchAllPagesAsBundle((pageParam) => { const params = new URLSearchParams(); params.append('_count', searchPageSize('100')); if (pageParam) params.set('page', pageParam); return get>(`${API_URL}/Medication?${params.toString()}`); }).then((bundle) => overlayHeldBundle('Medication', bundle, () => true) as Bundle); }, [get]); const fetchMedicationsWithPolling = useCallback(async (forceReload = false): Promise> => { const now = Date.now(); const cacheAge = now - lastFetchTimeRef.current; const cacheFresh = cacheAge < 5 * 60 * 1000; const held = publishedRef.current; if (!forceReload && !shouldReload && !writtenSince('Medication', lastFetchTimeRef.current) && held && cacheFresh && cacheOwner.isCurrentFor('viewer')) { return held; } const operation = cacheOwner.begin(); let retries = 10; const delay = 3000; let found = false; let bundle: Bundle | null = null; let entries: Medication[]; do { bundle = await fetchMedications(); entries = unpackBundle(bundle); found = lastUpserted ? entries.some(e => e.id === lastUpserted) : true; if (!found) { await new Promise(resolve => setTimeout(resolve, delay)); } retries--; } while (!found && retries > 0 && cacheOwner.publishes(operation)); if (!cacheOwner.publishes(operation)) return cacheOwner.answerFor(operation, bundle); publishedRef.current = bundle; setMedicationsBundle(bundle); setMedicationsList(entries); setLastUpserted(null); setShouldReload(false); lastFetchTimeRef.current = now; cacheOwner.claim('viewer', operation); return bundle; }, [fetchMedications, unpackBundle, lastUpserted, shouldReload]); const fetchMedication = useCallback(async (id: string): Promise => { const cachedMedication = cacheOwner.isCurrent(cacheOwner.claimed) ? medicationsList.find(medication => medication.id === id) : undefined; if (cachedMedication) { return cachedMedication; } return get(`${API_URL}/Medication/${id}`); }, [get, medicationsList]); const fetchMedicationHistory = useCallback(async (id: string): Promise> => { return get>(`${API_URL}/Medication/${id}/_history`); }, [get]); const createMedication = useCallback(async (medication: Medication): Promise => { return post(`${API_URL}/Medication`, medication); }, [post]); const updateMedication = useCallback(async (medication: Medication): Promise => { return put(`${API_URL}/Medication/${medication.id}`, medication); }, [put]); const deleteMedication = useCallback(async (medication: Medication): Promise => { return del(`${API_URL}/Medication/${medication.id}`); }, [del]); const transformToFhirMedication = useCallback((item: any): Medication => { const { status, code, manufacturer, form, amount, ingredient, batch, meta } = item; const usCoreMedicationProfile: string = 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-medication'; const medication: Medication = { resourceType: 'Medication', meta: { ...meta, profile: [usCoreMedicationProfile] }, ...(status && { status: status }), ...(code && { code: transformToCodeableConcept(code) as CodeableConcept }), //...(manufacturer && { manufacturer: manufacturer }), ...(form && { form: transformToCodeableConcept(form) as CodeableConcept }), ...(amount && { amount: amount }), ...(ingredient && ingredient.length !== 0 && { ingredient: ingredient.map((ing: any) => ({ ...(ing.itemCodeableConcept && { itemCodeableConcept: transformToCodeableConcept(ing.itemCodeableConcept) as CodeableConcept }), ...(ing.isActive !== undefined && { isActive: ing.isActive }), ...(ing.strength && { strength: ing.strength }) })) }), ...(batch && { batch: batch }) }; return medication; }, [transformToCodeableConcept]); const transformFromFhirMedication = useCallback((medication: Medication): any => { const { status, code, manufacturer, form, amount, ingredient, batch, meta } = medication; const formData = { meta: meta, status: status || 'active', code: transformFromCodeableConcept(code), // manufacturer: manufacturer || null, form: transformFromCodeableConcept(form), amount: amount || null, ingredient: (ingredient || []).map(ing => ({ itemCodeableConcept: transformFromCodeableConcept(ing.itemCodeableConcept), isActive: ing.isActive !== undefined ? ing.isActive : true, strength: ing.strength || null })), batch: batch || null, }; return formData; }, [transformFromCodeableConcept]); return { medicationsBundle, medicationsList, setMedicationsList, shouldReload, setShouldReload, lastUpserted, setLastUpserted, unsaved, setUnsaved, fetchMedications, fetchMedicationsWithPolling, fetchMedication, fetchMedicationHistory, createMedication, updateMedication, deleteMedication, transformToFhirMedication, transformFromFhirMedication }; }; const MedicationServiceContext = createContext | undefined>(undefined); export const MedicationServiceProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const medicationService = useMedicationService(); return ( {children} ); }; export const useMedicationServiceContext = () => { const context = useContext(MedicationServiceContext); if (context === undefined) { throw new Error('useMedicationServiceContext must be used within a MedicationServiceProvider'); } return context; };