import { useState, useCallback, createContext, useContext, ReactNode, useEffect, useRef } from 'react'; import { overlayHeldBundle, writtenSince } from '../utils/writeThroughHold'; import { useCacheOwner } from '../utils/cacheOwner'; import { Bundle, Consent, Patient, CodeableConcept, Reference } 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'; const API_URL = environment.fhirFoundationApi; interface ExtendedConsentProvisionActor { role: IValueSet; reference: Reference; } interface ExtendedConsentProvision { type: 'deny' | 'permit'; period?: { start?: string; end?: string; }; actor: ExtendedConsentProvisionActor[]; action: IValueSet[]; purpose: IValueSet[]; dataPeriod?: { start?: string; end?: string; }; data: { meaning: 'instance' | 'related' | 'dependents' | 'authoredby'; reference: Reference; }[]; } export const useConsentService = () => { const { get, post, put, delete: del } = useHttpService(); const { transformToCodeableConcept, transformFromCodeableConcept, createReferenceObject, formatDate, addFhirPeriodIfApplicable, addPeriodControlsIfApplicable } = useFhirServiceContext(); const [consentsList, setConsentsList] = useState([]); const [shouldReload, setShouldReload] = useState(false); const [lastUpserted, setLastUpserted] = useState(null); const [unsaved, setUnsaved] = useState(false); const [consentsBundle, setConsentsBundle] = useState | null>(null); const consentsBundleRef = useRef | null>(null); const lastFetchTimeRef = useRef(0); const cacheOwner = useCacheOwner('Consent', () => { setLastUpserted(null); setConsentsBundle(null); setConsentsList([]); lastFetchTimeRef.current = 0; }); useEffect(() => { consentsBundleRef.current = consentsBundle; }, [consentsBundle]); const fetchConsents = useCallback(async (): Promise> => { // The list is the whole practice, 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}/Consent?${params.toString()}`); }).then((bundle) => overlayHeldBundle('Consent', bundle, () => true) as Bundle); }, [get]); const fetchConsentsWithPolling = useCallback(async (forceReload = false): Promise> => { const now = Date.now(); const cacheAge = now - lastFetchTimeRef.current; const cacheFresh = cacheAge < 5 * 60 * 1000; if (!forceReload && !shouldReload && !writtenSince('Consent', lastFetchTimeRef.current) && consentsBundleRef.current && cacheFresh && cacheOwner.isCurrentFor('viewer')) { return consentsBundleRef.current; } const operation = cacheOwner.begin(); let retries = 10; const delay = 3000; let found = false; let bundle: Bundle | null = null; do { bundle = await fetchConsents(); const consents = (bundle.entry ?? []).map(entry => entry.resource) as Consent[]; found = lastUpserted ? consents.some(q => q.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); setConsentsBundle(bundle); setConsentsList((bundle.entry ?? []).map(entry => entry.resource) as Consent[]); setLastUpserted(null); setShouldReload(false); lastFetchTimeRef.current = now; cacheOwner.claim('viewer', operation); return bundle; }, [fetchConsents, lastUpserted, shouldReload]); const fetchConsent = useCallback(async (id: string): Promise => { const cachedConsent = cacheOwner.isCurrent(cacheOwner.claimed) ? consentsList.find(consent => consent.id === id) : undefined; if (cachedConsent) { return cachedConsent; } return get(`${API_URL}/Consent/${id}`); }, [get, consentsList]); const fetchConsentHistory = useCallback(async (id: string): Promise> => { return get>(`${API_URL}/Consent/${id}/_history`); }, [get]); const createConsent = useCallback(async (consent: Consent): Promise => { return post(`${API_URL}/Consent`, consent); }, [post]); const updateConsent = useCallback(async (consent: Consent): Promise => { return put(`${API_URL}/Consent/${consent.id}`, consent); }, [put]); const deleteConsent = useCallback(async (consent: Consent): Promise => { return del(`${API_URL}/Consent/${consent.id}`); }, [del]); const transformToFhirConsent = useCallback((item: any, patient?: Patient): Consent => { const { status, scope, category, dateTime, policyRule, organization, sourceType, sourceAttachment, sourceReference, provision } = item; let consent: Consent = { resourceType: 'Consent', status: status, scope: transformToCodeableConcept(scope), // patient: createReferenceObject('Patient', patient.id as string), ...(category && category.length > 0 && { category: transformToCodeableConcept(category) }), ...(dateTime && { dateTime: formatDate(dateTime) }), ...(policyRule && { policyRule: transformToCodeableConcept(policyRule) }), ...(organization && organization.length !== 0 && { organization: organization.map((org: Reference) => ({ reference: org.reference, display: org.display })) }), ...(sourceType === 'attachment' && sourceAttachment && { sourceAttachment: { title: sourceAttachment.title, url: sourceAttachment.url } }), ...(sourceType === 'reference' && sourceReference && { sourceReference: { reference: sourceReference.reference, display: sourceReference.display } }) }; if (provision) { consent.provision = { type: provision.type, ...(provision.actor && provision.actor.length !== 0 && { actor: provision.actor.map((actor: ExtendedConsentProvisionActor) => ({ role: transformToCodeableConcept(actor.role), reference: { reference: actor.reference.reference, display: actor.reference.display } })) }), ...(provision.action && provision.action.length !== 0 && { action: transformToCodeableConcept(provision.action) as CodeableConcept[] }), ...(provision.purpose && provision.purpose.length !== 0 && { purpose: transformToCodeableConcept(provision.purpose) as CodeableConcept[] }), ...(provision.data && provision.data.length !== 0 && { data: provision.data.map((data: any) => ({ meaning: data.meaning, reference: { reference: data.reference.reference, display: data.reference.display } })) }) }; consent.provision = addFhirPeriodIfApplicable(consent.provision, provision, 'period'); consent.provision = addFhirPeriodIfApplicable(consent.provision, provision, 'dataPeriod'); } return consent; }, [createReferenceObject, transformToCodeableConcept, addFhirPeriodIfApplicable]); const transformFromFhirConsent = useCallback((consent: Consent): any => { const { status, scope, category, dateTime, policyRule, organization, sourceAttachment, sourceReference, provision } = consent; let sourceType = 'none'; if (sourceAttachment) sourceType = 'attachment'; if (sourceReference) sourceType = 'reference'; const formData = { status: status, scope: transformFromCodeableConcept(scope), category: transformFromCodeableConcept(category) ?? [], dateTime: dateTime ?? '', policyRule: transformFromCodeableConcept(policyRule) ?? null, organization: organization ?? [], sourceType: sourceType, sourceAttachment: sourceAttachment ?? { title: '', url: '' }, sourceReference: sourceReference ?? { reference: '', display: '' }, provision: { type: provision?.type ?? 'permit', actor: [], action: [], purpose: [], data: [] } }; if (provision) { if (provision.actor && provision.actor.length > 0) { formData.provision.actor = provision.actor.map(actor => ({ role: transformFromCodeableConcept(actor.role), reference: actor.reference })); } if (provision.action && provision.action.length > 0) { formData.provision.action = transformFromCodeableConcept(provision.action); } if (provision.purpose && provision.purpose.length > 0) { formData.provision.purpose = transformFromCodeableConcept(provision.purpose); } if (provision.data && provision.data.length > 0) { formData.provision.data = provision.data.map(data => ({ meaning: data.meaning, reference: data.reference })); } formData.provision = addPeriodControlsIfApplicable(formData.provision, provision, 'period'); formData.provision = addPeriodControlsIfApplicable(formData.provision, provision, 'dataPeriod'); } return formData; }, [transformFromCodeableConcept, addPeriodControlsIfApplicable]); return { consentsBundle, consentsList, setConsentsList, shouldReload, setShouldReload, lastUpserted, setLastUpserted, unsaved, setUnsaved, fetchConsents, fetchConsentsWithPolling, fetchConsent, fetchConsentHistory, createConsent, updateConsent, deleteConsent, transformToFhirConsent, transformFromFhirConsent, }; }; const ConsentServiceContext = createContext | undefined>(undefined); export const ConsentServiceProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const consentService = useConsentService(); return ( {children} ); }; export const useConsentServiceContext = () => { const context = useContext(ConsentServiceContext); if (context === undefined) { throw new Error('useConsentServiceContext must be used within a ConsentServiceProvider'); } return context; };