import { ReactNode, useCallback, createContext, useContext } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { formatFhirDateTimeForDisplay, isFhirDateOnly, toFhirDateTime } from '../utils/fhirDateTimeDisplay'; import { Address, Age, Bundle, BundleEntry, CodeableConcept, ContactPoint, Dosage, DosageDoseAndRate, Duration, FhirResource, HealthcareServiceAvailableTime, HumanName, Identifier, LocationHoursOfOperation, PractitionerRoleAvailableTime, PractitionerRoleNotAvailable, Reference } from 'fhir/r4'; import { IValueSet, useValueSetServiceContext } from './ValueSetService'; import { trimFhirAddress } from '../utils/fhirAddress'; interface ExtendedPractitionerRoleAvailableTime extends PractitionerRoleAvailableTime { available: boolean; [key: string]: any; } interface ExtendedPractitionerRoleNotAvailable extends PractitionerRoleNotAvailable { [key: string]: any; } /** * A CodeableConcept may carry a coding, free text, or both, and the form layer works in IValueSet, * which has nowhere to put text on its own. A text-only concept is therefore carried as its display * and written back out as text - never as a coding. Reading only coding[0] discarded the text and * then re-wrapped the empty result as a coding, which the server rejects outright. */ const codeableConceptToValueSet = (concept: CodeableConcept | undefined): IValueSet | null => { const coding = concept?.coding?.find((entry) => entry.code || entry.system || entry.display); if (coding) { return { ...coding } as IValueSet; } const text = concept?.text?.trim(); return text ? ({ display: text } as IValueSet) : null; }; /** * The inverse. A value the user picked out of a value set has a code and becomes a coding; a value * that only ever had a display came from free text and goes back as free text. Neither direction * may emit an empty object: FHIR refuses one, and the refusal is what protected the text until now. */ const valueSetToCodeableConcept = (valueSet: IValueSet | undefined): CodeableConcept | null => { if (valueSet?.code || valueSet?.system) { return { coding: [{ ...valueSet }], text: valueSet.display }; } const display = valueSet?.display?.trim(); return display ? { text: display } : null; }; const TIMING_REPEAT_NUMBERS = ['count', 'countMax', 'duration', 'durationMax', 'frequency', 'frequencyMax', 'period', 'periodMax', 'offset']; /** A Timing.repeat from the form: the form's own keys dropped, empty values left out, typed numbers made numbers. */ const timingRepeatFromForm = (repeat: unknown): Record | undefined => { if (!repeat || typeof repeat !== 'object') return undefined; const out: Record = {}; Object.entries(repeat as Record).forEach(([key, value]) => { if (key === 'repeatType' || value === '' || value === null || value === undefined) return; if (Array.isArray(value) && value.length === 0) return; out[key] = TIMING_REPEAT_NUMBERS.includes(key) && typeof value === 'string' ? Number(value) : value; }); return Object.keys(out).length ? out : undefined; }; export const useFhirService = () => { const { ageUnits, durationUnits } = useValueSetServiceContext(); const npiSystem = 'http://hl7.org/fhir/sid/us-npi'; const identifierTypeSystem = 'http://terminology.hl7.org/CodeSystem/v2-0203'; 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 formatDate = useCallback((date: string | Date): string => { if (!date) return ''; const d = new Date(date); return d.toISOString().split('T')[0]; }, []); const formatDateTime = useCallback((dateTime: string | Date): string => toFhirDateTime(dateTime), []); const formatDateTimeForInput = useCallback((dateTime: string | Date | undefined): string => { if (!dateTime) return ''; // A day is that day: read as an instant it is UTC midnight, the evening before west of Greenwich. if (isFhirDateOnly(dateTime)) return dateTime.trim(); const d = new Date(dateTime); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); const hours = String(d.getHours()).padStart(2, '0'); const minutes = String(d.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day}T${hours}:${minutes}`; }, []); const formatDateTimeForDisplay = useCallback((fhirDateTime: string): string => formatFhirDateTimeForDisplay(fhirDateTime), []); const addFhirPeriodIfApplicable = useCallback((entry: any, periodizable: any, key: string = 'period'): any => { if (periodizable[key]) { entry[key] = {}; if (periodizable[key].start) { entry[key].start = isDateTime(periodizable[key].start) ? formatDateTime(periodizable[key].start) : formatDate(periodizable[key].start); } if (periodizable[key].end) { entry[key].end = isDateTime(periodizable[key].end) ? formatDateTime(periodizable[key].end) : formatDate(periodizable[key].end); } return entry; } const { [key]: _, ...withoutPeriod } = entry; return withoutPeriod; }, [formatDate, formatDateTime]); const addPeriodControlsIfApplicable = useCallback((formValues: { [key: string]: any }, periodizable: any, key: string = 'period') => { if (periodizable[key]) { formValues[key] = {}; if (periodizable[key].start) { formValues[key].start = periodizable[key].start; } if (periodizable[key].end) { formValues[key].end = periodizable[key].end; } } return formValues; }, []); const isDateTime = useCallback((value: string | Date): boolean => { if (!value) return false; const stringValue = typeof value === 'string' ? value : value.toISOString(); return stringValue.includes('T') || stringValue.includes(':'); }, []); // DEFINED NEVER USED CURRENTLY // const addFhirPeriodIfApplicableWithType = useCallback(( // entry: any, // periodizable: any, // key: string = 'period', // type: 'date' | 'dateTime' = 'date' // ): any => { // if (periodizable[key]) { // entry[key] = {}; // const formatter = type === 'dateTime' ? formatDateTime: formatDate; // // if (periodizable[key].start) { // entry[key].start = formatter(periodizable[key].start); // } // // if (periodizable[key].end) { // entry[key].end = formatter(periodizable[key].end); // } // return entry; // } // // const { [key]: _,...withoutPeriod } = entry; // return withoutPeriod; // }, [formatDate, formatDateTime]); const transformToHumanName = useCallback((name: any[], _type?: 'practitioner' | 'patient' | 'related_person'): HumanName[] => { return name.map((name: any) => { const nameEntry: HumanName = { family: name.family, given: name.given, prefix: name.prefix || [], suffix: name.suffix || [], text: [ ...(name.prefix || []), ...name.given.map((g: string) => g), name.family, ...(name.suffix || []) ].filter(part => part).join(' ') }; if (!name.id) { nameEntry.id = uuidv4(); } if (name.use) { nameEntry.use = name.use; } return addFhirPeriodIfApplicable(nameEntry, name); }); }, [addFhirPeriodIfApplicable]); const transformFromHumanName = useCallback((resource: any): any[] => { return (resource.name ?? []).map((name: HumanName) => { const humanNameData = { id: name.id ?? '', family: name.family ?? '', given: name.given ?? [], prefix: name.prefix ?? [], suffix: name.suffix ?? [], use: name.use ?? '' }; return addPeriodControlsIfApplicable(humanNameData, name); }); }, []); const transformToCodeableConcept = useCallback((codeable: IValueSet | IValueSet[]): CodeableConcept | CodeableConcept[] => { if (Array.isArray(codeable)) { return codeable .map(valueSetToCodeableConcept) .filter((concept): concept is CodeableConcept => concept !== null); } return valueSetToCodeableConcept(codeable) ?? {}; }, []); const transformFromCodeableConcept = useCallback((codeableConcept: CodeableConcept | CodeableConcept[] | undefined): any => { if (!codeableConcept) { return null; } if (Array.isArray(codeableConcept)) { return codeableConcept .map(codeableConceptToValueSet) .filter((valueSet): valueSet is IValueSet => valueSet !== null); } return codeableConceptToValueSet(codeableConcept) ?? {}; }, []); const transformToIdentifier = useCallback((identifier: any[], system?: string): Identifier[] => { return (identifier ?? []).map((identifier: any) => { const identifierEntry: Identifier = { ...identifier, ...(identifier.type && { type: transformToCodeableConcept(identifier.type) }), ...(system && { system: system }) }; if (!identifier.id) { identifierEntry.id = uuidv4(); } if (!identifier.use) { delete identifierEntry.use; } if (!identifier.system && !system) { delete identifierEntry.system; } if (!identifier.type) { delete identifierEntry.type; } return addFhirPeriodIfApplicable(identifierEntry, identifier); }); }, [transformToCodeableConcept, addFhirPeriodIfApplicable]); const transformFromIdentifier = useCallback((identifier: Identifier[]): any[] => { if (!identifier) { return []; } return (identifier ?? []).map((id: Identifier) => { const identifierData = { id: id.id ?? '', system: id.system ?? '', value: id.value ?? '', use: id.use ?? '', ...(id.type && { type: transformFromCodeableConcept(id.type) }), }; return addPeriodControlsIfApplicable(identifierData, id); }); }, [transformFromCodeableConcept, addPeriodControlsIfApplicable]); const transformToAddress = useCallback((address: any[], _type?: 'practitioner' | 'patient' | 'RelatedPerson' | 'Organization' | 'Location'): Address[] => { if (!address) { return []; } return (Array.isArray(address) ? address : [address]).map((address: any) => { // WHAT REACHES THE WIRE IS TRIMMED, and it is done HERE because this is the one funnel every // resource's address goes through - patient, practitioner, organization and location. Trimming // in a caller fixes one form and leaves the identical hole on the next, which is exactly what // had happened: the location page trimmed for its duplicate CHECK and then saved the raw form // data, so the padding reached the wire on every route. const addressEntry: Address = trimFhirAddress(address); if (!addressEntry.id) { addressEntry.id = uuidv4(); } if (!address.type) { delete addressEntry.type; } if (!address.use) { delete addressEntry.use; } return addFhirPeriodIfApplicable(addressEntry, address); }); }, [addFhirPeriodIfApplicable]); const transformFromAddress = useCallback((resource: any): any[] => { if (!resource || !resource.address) { return []; } const address: Address[] = Array.isArray(resource.address) ? resource.address : [resource.address]; return (address || []).map((a: Address) => { const addressData = { id: a.id ?? '', use: a.use ?? '', type: a.type ?? '', line: a.line || [], city: a.city ?? '', state: a.state ?? '', postalCode: a.postalCode ?? '', country: a.country ?? '', }; return addPeriodControlsIfApplicable(addressData, a); }); }, [addPeriodControlsIfApplicable]); const transformToTelecom = useCallback((telecom: any[], _type?: 'practitioner' | 'patient' | 'RelatedPerson' | 'Organization'): ContactPoint[] => { return (telecom ?? []) .filter((telecom: any) => { if (!telecom) { return false; } return typeof telecom.value === 'string' ? telecom.value.trim() !== '' : !!telecom.value; }) .map((telecom: any) => { const telecomEntry: ContactPoint = { ...telecom, value: typeof telecom.value === 'string' ? telecom.value.trim() : telecom.value }; if (!telecom.id) { telecomEntry.id = uuidv4(); } if (!telecom.system) { delete telecomEntry.system; } if (!telecom.use) { delete telecomEntry.use; } return addFhirPeriodIfApplicable(telecomEntry, telecom); }); }, [addFhirPeriodIfApplicable]); const transformFromTelecom = useCallback((resource: any): any[] => { return (resource.telecom ?? []).map((telecom: ContactPoint) => { const telecomData = { id: telecom.id ?? '', system: telecom.system ?? '', value: telecom.value ?? '', use: telecom.use ?? '', rank: telecom.rank ?? 1 }; return addPeriodControlsIfApplicable(telecomData, telecom); }); }, [addPeriodControlsIfApplicable]); const transformToFhirAge = useCallback(({ value, unit }: { value: number, unit: string }): Age => { const unitValue: string = unit ? unit : 'a' const age: IValueSet | undefined = ageUnits.find((age: IValueSet) => age.code === unitValue) ?? ageUnits.find((age: IValueSet) => age.display === unitValue); return { system: age?.system, code: age?.code, unit: age?.display, value: value }; }, [ageUnits]); const transformFromFhirAge = useCallback((age: Age | undefined) => { if (!age) return undefined; return { value: age?.value ?? 1, unit: age?.unit ?? 'year' }; }, []); // TOLERATES BEING HANDED NOTHING, like every other transform beside it. It destructured its argument, // so `transformToFhirDuration(undefined)` threw `Cannot destructure property 'value' of 'undefined'` // before the guard on the next line could run - and its callers pass optional fields straight in. // A prescription with no dispense interval could not be filed at all, and the throw happened before // any request, so there was nothing on the wire to explain it. const transformToFhirDuration = useCallback((input?: { value: number, unit: string }): Duration | undefined => { const { value, unit } = input ?? {}; if (value === undefined || value === null) { return undefined; } const unitValue: string = unit ? unit : 'd' const duration: IValueSet | undefined = durationUnits.find((d: IValueSet) => d.code === unitValue); return { system: duration?.system, code: duration?.code, unit: unitValue, value: value }; }, []); const transformFromFhirDuration = useCallback((duration: Duration | undefined) => { return { value: duration?.value ?? undefined, unit: duration?.unit ?? 'd' }; }, []); const transformToOperationalHours = useCallback((operationalHours: ExtendedPractitionerRoleAvailableTime[], startKey: string = 'availableStartTime', endKey: string = 'availableEndTime'): PractitionerRoleAvailableTime[] => { return (operationalHours ?? []) .filter((entry: ExtendedPractitionerRoleAvailableTime) => entry.available) .map((practitionerRoleAvailableTime: ExtendedPractitionerRoleAvailableTime) => { const { daysOfWeek, allDay, [startKey]: start, [endKey]: end } = practitionerRoleAvailableTime; // Preserve valid hours so turning all-day off restores the previous window. const hasWindow = Boolean(start && end && start !== end); return { daysOfWeek: daysOfWeek, allDay: allDay, ...((allDay ? hasWindow : true) && { [startKey]: `${start}:00`, [endKey]: `${end}:00` }) }; }); }, []); // const transformToOperationalHoursWithIntervals = useCallback((availableTimes: PractitionerRoleAvailableTime[] = [], startKey: string = 'availableStartTime', endKey: string = 'availableEndTime'): any[] => { // const dayMap: Record = {}; // // daysOfWeek.forEach((day: IValueSet) => { // dayMap[day.code] = []; // }); // // availableTimes.forEach(time => { // if (time.daysOfWeek && time.daysOfWeek.length > 0) { // const dayCode = time.daysOfWeek[0]; // if (dayMap[dayCode]) { // dayMap[dayCode].push(time); // } // } // }); // // return daysOfWeek.map((day: IValueSet) => { // const dayTimes = dayMap[day.code]; // // if (dayTimes.length === 0) { // return { // daysOfWeek: [day.code], // available: false, // allDay: false, // [startKey]: '09:00', // [endKey]: '17:00', // timeIntervals: [{ start: '09:00', end: '17:00' }] // }; // } // // const hasAllDay = dayTimes.some(time => time.allDay); // // if (hasAllDay) { // return { // daysOfWeek: [day.code], // available: true, // allDay: true, // [startKey]: '00:00', // [endKey]: '23:59', // timeIntervals: [{ start: '00:00', end: '23:59' }] // }; // } // // const timeIntervals = dayTimes.map(time => ({ // start: time.availableStartTime || '09:00', // end: time.availableEndTime || '17:00' // })); // // if (timeIntervals.length === 0) { // timeIntervals.push({ start: '09:00', end: '17:00' }); // } // // return { // daysOfWeek: [day.code], // available: true, // allDay: false, // [startKey]: timeIntervals[0].start, // [endKey]: timeIntervals[0].end, // timeIntervals // }; // }); // }, [daysOfWeek]); const transformFromOperationalHours = useCallback((operationalHours: PractitionerRoleAvailableTime[] | LocationHoursOfOperation[] | HealthcareServiceAvailableTime[] | undefined, startKey: string = 'availableStartTime', endKey: string = 'availableEndTime'): any[] => { return daysOfWeek.map((dayOfWeek: IValueSet) => { // One stored entry may name several days (the first-clinician step saves mon-sun as one). const existing = (operationalHours ?? []).find((availableTime: PractitionerRoleAvailableTime) => (availableTime.daysOfWeek as string[] | undefined)?.includes(dayOfWeek.code)); // An unstored day turned on opens 09:00-17:00, never as a zero-length day. return { available: !!existing, daysOfWeek: [dayOfWeek.code], allDay: existing?.allDay ?? false, [startKey]: (existing as any)?.[startKey]?.split(':').slice(0, 2).join(':') ?? '09:00', [endKey]: (existing as any)?.[endKey]?.split(':').slice(0, 2).join(':') ?? '17:00' }; }); }, [daysOfWeek]); // const transformFromOperationalHoursWithIntervals = useCallback((formData: any, context: string = 'availableTime'): PractitionerRoleAvailableTime[] => { // const result: PractitionerRoleAvailableTime[] = []; // // formData[context]?.forEach((day: any) => { // if (!day.available) { // return; // } // // if (day.allDay) { // result.push({ // daysOfWeek: day.daysOfWeek, // allDay: true // }); // return; // } // // if (day.timeIntervals && day.timeIntervals.length > 0) { // day.timeIntervals.forEach((interval: any) => { // result.push({ // daysOfWeek: day.daysOfWeek, // availableStartTime: interval.start, // availableEndTime: interval.end, // allDay: false // }); // }); // } else { // result.push({ // daysOfWeek: day.daysOfWeek, // availableStartTime: day.availableStartTime, // availableEndTime: day.availableEndTime, // allDay: false // }); // } // }); // // return result; // }, []); const transformToOperationalHoursNotAvailable = useCallback((notAvailable: ExtendedPractitionerRoleNotAvailable[]) => { return (notAvailable ?? []).map((practitionerRoleNotAvailable: ExtendedPractitionerRoleNotAvailable) => { const notAvailableEntry: PractitionerRoleNotAvailable = { ...(practitionerRoleNotAvailable.description && { description: practitionerRoleNotAvailable.description as string }) } as PractitionerRoleNotAvailable; return addFhirPeriodIfApplicable(notAvailableEntry, practitionerRoleNotAvailable, 'during'); }); }, [addFhirPeriodIfApplicable]); const transformFromOperationalHoursNotAvailable = useCallback((notAvailable: PractitionerRoleNotAvailable[] | undefined): any[] => { return (notAvailable ?? []).map((notAvail: PractitionerRoleNotAvailable) => { const notAvailableFormGroup: { [key: string]: any } = { ...(notAvail.description && { description: notAvail.description }) }; return addPeriodControlsIfApplicable(notAvailableFormGroup, notAvail, 'during'); }); }, [addPeriodControlsIfApplicable]); const filterBundle = useCallback((bundle: Bundle, resourceType: string): BundleEntry[] => { return (bundle.entry ?? []) .filter((entry: BundleEntry) => entry.resource?.resourceType === resourceType); }, []); const filterAndUnpackBundle = useCallback((bundle: Bundle, resourceType: string): T[] => { if (!bundle) { return []; } return (bundle.entry ?? []) .filter((entry: BundleEntry) => (entry.resource as FhirResource | undefined)?.resourceType === resourceType) .map((entry: BundleEntry) => entry.resource as T); }, []); const unpackBundle = useCallback((bundle: Bundle): T[] => { return (bundle.entry ?? []).map((entry: BundleEntry) => entry.resource as T); }, []); const extractIdFromReference = useCallback((reference: string | undefined): string | undefined => { if (!reference) { return undefined; } const parts = reference.split('/'); return parts.length > 1 ? parts[parts.length - 1] : undefined; }, []); const extractIdFromReferenceObject = useCallback((reference: Reference | undefined): string | undefined => { if (!reference || !reference.reference) { return undefined; } return extractIdFromReference(reference.reference); }, [extractIdFromReference]); const extractIdsFromReferences = useCallback((references: Reference[] | undefined): string[] => { if (!references || !Array.isArray(references)) { return []; } return references .map(ref => extractIdFromReferenceObject(ref)) .filter((id): id is string => id !== undefined); }, [extractIdFromReferenceObject]); const extractReferenceId = useCallback((reference: Reference | Reference[] | undefined): string | string[] | undefined => { if (!reference) { return undefined; } if (Array.isArray(reference)) { return extractIdsFromReferences(reference); } return extractIdFromReferenceObject(reference); }, [extractIdsFromReferences, extractIdFromReferenceObject]); const extractReferenceType = useCallback((reference: Reference | undefined): string | undefined => { if (!reference || !reference.reference) { return undefined; } const parts = reference.reference.split('/'); return parts.length > 0 ? parts[0] : undefined; }, []); const createReference = useCallback((resourceType: string, id: string): string => { return `${resourceType}/${id}`; }, []); const createReferenceObject = useCallback((resourceType: string, id: string, display?: string): Reference => { return { reference: createReference(resourceType, id), ...(display && { display }) }; }, [createReference]); const getBadgeStyle = useCallback((status: string) => { switch (status) { case 'active': case 'completed': case 'routine': case 'accepted': return { backgroundColor: '#d4edda', color: '#155724', width: 100, justifyContent: 'center' }; case 'revoked': case 'entered-in-error': case 'urgent': case 'asap': case 'failed': case 'rejected': case 'canceled': case 'expired': return { backgroundColor: '#f8d7da', color: '#721c24', width: 100, justifyContent: 'center' }; default: return { backgroundColor: '#fff3cd', color: '#856404', width: 100, justifyContent: 'center' }; } }, []); const formatToFhirInstant = useCallback((dateTime: string): string => { const date = new Date(dateTime); return date.toISOString(); }, []); const transformToFhirQuantity = useCallback((quantityData: any): any => { if (!quantityData) { return undefined; } const quantity: any = {}; if (quantityData.value !== undefined && quantityData.value !== null && quantityData.value !== '') { quantity.value = Number(quantityData.value); } if (quantityData.unit) { if (typeof quantityData.unit === 'object' && quantityData.unit.code) { quantity.code = quantityData.unit.code; quantity.unit = quantityData.unit.display || quantityData.unit.code; quantity.system = 'http://unitsofmeasure.org'; } else if (typeof quantityData.unit === 'string') { quantity.code = quantityData.unit; quantity.unit = quantityData.unit; quantity.system = 'http://unitsofmeasure.org'; } } if (quantityData.comparator) { quantity.comparator = quantityData.comparator; } return Object.keys(quantity).length > 0 ? quantity : undefined; }, []); const transformFromFhirQuantity = useCallback((quantity: any): any => { if (!quantity) { return { value: '', unit: null, comparator: '' }; } return { value: quantity.value ?? '', // A bench result stores its unit as a display string with no code, so reading only // `code` dropped the scale on load, and the save then wrote that emptiness back. unit: (quantity.code ?? quantity.unit) ? { code: quantity.code ?? quantity.unit, display: quantity.unit || quantity.code, system: quantity.system || 'http://unitsofmeasure.org' } : null, comparator: quantity.comparator ?? '' }; }, []); const transformToFhirRatio = useCallback((ratioData: any): any => { if (!ratioData) { return undefined; } const ratio: any = {}; if (ratioData.numerator) { const numerator = transformToFhirQuantity(ratioData.numerator); if (numerator) { ratio.numerator = numerator; } } if (ratioData.denominator) { const denominator = transformToFhirQuantity(ratioData.denominator); if (denominator) { ratio.denominator = denominator; } } return Object.keys(ratio).length > 0 ? ratio : undefined; }, [transformToFhirQuantity]); const transformFromFhirRatio = useCallback((ratio: any): any => { if (!ratio) { return { numerator: { value: '', unit: null, comparator: '' }, denominator: { value: '', unit: null, comparator: '' } }; } return { numerator: transformFromFhirQuantity(ratio.numerator), denominator: transformFromFhirQuantity(ratio.denominator) }; }, [transformFromFhirQuantity]); // The whole schedule goes through the form, not only its name: a save from a form that shows just the name must // not erase how often, on which days or when it happens. const transformToFhirTiming = useCallback((timingData: any): any => { if (!timingData) { return undefined; } const timing: any = {}; if (timingData.code) { timing.code = transformToCodeableConcept(timingData.code); } const event = (Array.isArray(timingData.event) ? timingData.event : []).filter(Boolean); if (event.length) timing.event = event; const repeat = timingRepeatFromForm(timingData.repeat); if (repeat) timing.repeat = repeat; return Object.keys(timing).length !== 0 ? timing : undefined; }, [transformToCodeableConcept]); const transformFromFhirTiming = useCallback((timing: any): any => { if (!timing) { return { code: null }; } return { code: transformFromCodeableConcept(timing.code), ...(timing.event ? { event: [...timing.event] } : {}), ...(timing.repeat ? { repeat: JSON.parse(JSON.stringify(timing.repeat)) } : {}), }; }, [transformFromCodeableConcept]); const transformToReferenceCollection = useCallback((referenceCollection: any[]): Reference[] => { if (!referenceCollection || !Array.isArray(referenceCollection)) { return []; } return referenceCollection .filter(item => item.reference && item.referenceType) .map(item => item.reference); }, []); const transformFromReferenceCollection = useCallback((references: Reference[] | undefined): any[] => { if (!references || !Array.isArray(references)) { return []; } return references.map(reference => ({ referenceType: extractReferenceType(reference), reference: reference })); }, [extractReferenceType]); const transformToFhirDosage = useCallback((dosageData: any): Dosage | Dosage[] | undefined => { if (!dosageData) { return undefined; } const transformSingleDosage = (d: any): Dosage | null => { if (!d) return null; const transformedMaxDosePerAdministration = transformToFhirQuantity(d.maxDosePerAdministration); const transformedMaxDosePerLifetime = transformToFhirQuantity(d.maxDosePerLifetime); const transformedMaxDosePerPeriod = transformToFhirRatio(d.maxDosePerPeriod); // A DOSAGE NEED NOT CARRY A DOSE. FHIR makes `doseAndRate` 0..*, and the commonest sig of // all - a line of free text like "1 tablet by mouth twice daily with food" - has none. This // was the only unguarded field in the object beside a dozen that check first, and it threw // rather than omitting: a prescription written that way could not be filed at all. const transformedDoseAndRate: DosageDoseAndRate[] = (d.doseAndRate ?? []).map((doseAndRate: any) => { const doseAndRateItem: DosageDoseAndRate = { ...(doseAndRate.type && { type: transformToCodeableConcept(doseAndRate.type) }), ...(doseAndRate.doseType === 'range' && doseAndRate.doseRange && { doseRange: doseAndRate.doseRange }), ...(doseAndRate.doseType === 'quantity' && doseAndRate.doseQuantity && { doseQuantity: transformToFhirQuantity(doseAndRate.doseQuantity) }), ...(doseAndRate.rateType === 'range' && doseAndRate.rateRange && { rateRange: doseAndRate.rateRange }), ...(doseAndRate.rateType === 'quantity' && doseAndRate.rateQuantity && { rateQuantity: transformToFhirQuantity(doseAndRate.rateQuantity) }), ...(doseAndRate.rateType === 'ratio' && doseAndRate.rateRatio && { rateRatio: transformToFhirRatio(doseAndRate.rateRatio) }) }; if (doseAndRate.doseType === 'range') { delete doseAndRateItem.doseQuantity; } if (doseAndRate.doseType === 'quantity') { delete doseAndRateItem.doseRange; } if (doseAndRate.rateType === 'range') { delete doseAndRateItem.rateQuantity; delete doseAndRateItem.rateRatio; } if (doseAndRate.rateType === 'quantity') { delete doseAndRateItem.doseRange; delete doseAndRateItem.rateRatio; } if (doseAndRate.rateType === 'ratio') { delete doseAndRateItem.doseRange; delete doseAndRateItem.rateQuantity; } return Object.keys(doseAndRateItem).length !== 0 ? doseAndRateItem : null; }).filter(Boolean); const dosageItem: Dosage = { ...(d.sequence && { sequence: d.sequence }), ...(d.text && { text: d.text }), ...(d.patientInstruction && { patientInstruction: d.patientInstruction }), ...(d.additionalInstruction && d.additionalInstruction.length !== 0 && { additionalInstruction: transformToCodeableConcept(d.additionalInstruction) }), ...(d.site && { site: transformToCodeableConcept(d.site) }), ...(d.route && { route: transformToCodeableConcept(d.route) }), ...(d.method && { method: transformToCodeableConcept(d.method) }), ...(d.timing && { timing: transformToFhirTiming(d.timing) }), ...(d.asNeededBoolean && !d.asNeededCodeableConcept && { asNeededBoolean: d.asNeededBoolean }), ...(d.asNeededCodeableConcept && { asNeededCodeableConcept: transformToCodeableConcept(d.asNeededCodeableConcept) }), ...(transformedMaxDosePerAdministration && { maxDosePerAdministration: transformedMaxDosePerAdministration }), ...(transformedMaxDosePerLifetime && { maxDosePerLifetime: transformedMaxDosePerLifetime }), ...(transformedMaxDosePerPeriod && { maxDosePerPeriod: transformedMaxDosePerPeriod }), ...(transformedDoseAndRate && transformedDoseAndRate.length !== 0 && { doseAndRate: transformedDoseAndRate }) }; return Object.keys(dosageItem).length !== 0 ? dosageItem : null; }; if (Array.isArray(dosageData)) { const transformedDosages = dosageData .map(transformSingleDosage) .filter(Boolean) as Dosage[]; return transformedDosages.length !== 0 ? transformedDosages : undefined; } return transformSingleDosage(dosageData) || undefined; }, [transformToCodeableConcept, transformToFhirQuantity, transformToFhirRatio, transformToFhirTiming]); const transformFromFhirDosage = useCallback((dosageData: any): any => { if (!dosageData) { return []; } const transformSingleDosage = (d: any): any => { if (!d) return null; const extendedDosage = { sequence: d.sequence ?? 1, text: d.text ?? '', patientInstruction: d.patientInstruction ?? '', additionalInstruction: transformFromCodeableConcept(d.additionalInstruction), site: transformFromCodeableConcept(d.site), route: transformFromCodeableConcept(d.route), method: transformFromCodeableConcept(d.method), doseAndRate: (d.doseAndRate ?? []).map((doseAndRate: DosageDoseAndRate) => ({ type: transformFromCodeableConcept(doseAndRate.type), doseType: doseAndRate.doseQuantity ? 'quantity' : doseAndRate.doseRange ? 'range' : '', rateType: doseAndRate.rateQuantity ? 'quantity' : doseAndRate.rateRange ? 'range' : doseAndRate.rateRatio ? 'ratio' : '', doseRange: doseAndRate.doseRange, doseQuantity: transformFromFhirQuantity(doseAndRate.doseQuantity), rateRange: doseAndRate.rateRange, rateQuantity: transformFromFhirQuantity(doseAndRate.rateQuantity), rateRatio: transformFromFhirRatio(doseAndRate.rateRatio) })), timing: transformFromFhirTiming(d.timing), asNeededBoolean: d.asNeededBoolean || d.asNeededCodeableConcept, asNeededCodeableConcept: transformFromCodeableConcept(d.asNeededCodeableConcept), maxDosePerAdministration: transformFromFhirQuantity(d.maxDosePerAdministration), maxDosePerLifetime: transformFromFhirQuantity(d.maxDosePerLifetime), maxDosePerPeriod: transformFromFhirRatio(d.maxDosePerPeriod) }; return extendedDosage; }; if (Array.isArray(dosageData)) { return dosageData.map(transformSingleDosage).filter(Boolean); } const result = transformSingleDosage(dosageData); return result ? [result] : []; }, [transformFromCodeableConcept, transformFromFhirQuantity, transformFromFhirRatio, transformFromFhirTiming]); return { npiSystem, identifierTypeSystem, daysOfWeek, addFhirPeriodIfApplicable, addPeriodControlsIfApplicable, formatDate, formatDateTime, formatDateTimeForInput, formatDateTimeForDisplay, filterBundle, filterAndUnpackBundle, unpackBundle, getBadgeStyle, extractReferenceId, extractReferenceType, createReferenceObject, transformToHumanName, transformToIdentifier, transformToAddress, transformToTelecom, transformToCodeableConcept, transformToOperationalHours, transformToOperationalHoursNotAvailable, transformToFhirAge, transformFromFhirAge, transformFromHumanName, transformFromIdentifier, transformFromAddress, transformFromTelecom, transformFromCodeableConcept, transformFromOperationalHours, transformFromOperationalHoursNotAvailable, formatToFhirInstant, transformToFhirQuantity, transformFromFhirQuantity, transformToFhirRatio, transformFromFhirRatio, transformToFhirTiming, transformFromFhirTiming, transformToReferenceCollection, transformFromReferenceCollection, transformToFhirDosage, transformFromFhirDosage, transformToFhirDuration, transformFromFhirDuration }; }; const FhirServiceContext = createContext | undefined>(undefined); export const FhirServiceProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const fhirService = useFhirService(); return ( {children} ); }; export const useFhirServiceContext = () => { const context = useContext(FhirServiceContext); if (context === undefined) { throw new Error('useFhirServiceContext must be used within a FhirServiceProvider'); } return context; };