-
Notifications
You must be signed in to change notification settings - Fork 239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(feat) HIE-9: Add MPI workflows to OpenMRS frontend #1397
base: main
Are you sure you want to change the base?
Changes from all commits
f77c65c
f66ce2b
b11c671
686c252
70cbe50
cfeabb8
f571888
0a6610d
47ad201
5c7d78f
dae3d8d
0b18cee
a0691d9
2fa10df
75b09e4
5f2929b
9b4007f
10a800c
46b43d3
7db0c90
9573231
b25b124
70f3827
27b81f3
2487439
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { fhirBaseUrl, openmrsFetch } from '@openmrs/esm-framework'; | ||
import useSWR from 'swr'; | ||
|
||
export function useMpiPatient(patientId: string) { | ||
const url = `${fhirBaseUrl}/Patient/${patientId}/$cr`; | ||
|
||
const { | ||
data: patient, | ||
error: error, | ||
isLoading: isLoading, | ||
} = useSWR<{ data: fhir.Patient }, Error>(url, openmrsFetch); | ||
|
||
return { | ||
isLoading, | ||
patient, | ||
error: error, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,11 +25,13 @@ import { | |
import { | ||
getAddressFieldValuesFromFhirPatient, | ||
getFormValuesFromFhirPatient, | ||
getIdentifierFieldValuesFromFhirPatient, | ||
getPatientUuidMapFromFhirPatient, | ||
getPhonePersonAttributeValueFromFhirPatient, | ||
latestFirstEncounter, | ||
} from './patient-registration-utils'; | ||
import { useInitialPatientRelationships } from './section/patient-relationships/relationships.resource'; | ||
import { useMpiPatient } from './mpi/mpi-patient.resource'; | ||
|
||
interface DeathInfoResults { | ||
uuid: string; | ||
|
@@ -40,8 +42,8 @@ interface DeathInfoResults { | |
causeOfDeathNonCoded: string | null; | ||
} | ||
|
||
export function useInitialFormValues(patientUuid: string): [FormValues, Dispatch<FormValues>] { | ||
const { freeTextFieldConceptUuid } = useConfig<RegistrationConfig>(); | ||
export function useInitialFormValuesLocal(patientUuid: string): [FormValues, Dispatch<FormValues>] { | ||
const { freeTextFieldConceptUuid, fieldConfigurations } = useConfig<RegistrationConfig>(); | ||
const { isLoading: isLoadingPatientToEdit, patient: patientToEdit } = usePatient(patientUuid); | ||
const { data: deathInfo, isLoading: isLoadingDeathInfo } = useInitialPersonDeathInfo(patientUuid); | ||
const { data: attributes, isLoading: isLoadingAttributes } = useInitialPersonAttributes(patientUuid); | ||
|
@@ -90,7 +92,7 @@ export function useInitialFormValues(patientUuid: string): [FormValues, Dispatch | |
...initialFormValues, | ||
...getFormValuesFromFhirPatient(patientToEdit), | ||
address: getAddressFieldValuesFromFhirPatient(patientToEdit), | ||
...getPhonePersonAttributeValueFromFhirPatient(patientToEdit), | ||
...getPhonePersonAttributeValueFromFhirPatient(patientToEdit, fieldConfigurations.phone.personAttributeUuid), | ||
birthdateEstimated: !/^\d{4}-\d{2}-\d{2}$/.test(patientToEdit.birthDate), | ||
yearsEstimated, | ||
monthsEstimated, | ||
|
@@ -108,7 +110,13 @@ export function useInitialFormValues(patientUuid: string): [FormValues, Dispatch | |
setInitialFormValues(registration._patientRegistrationData.formValues); | ||
} | ||
})(); | ||
}, [initialFormValues, isLoadingPatientToEdit, patientToEdit, patientUuid]); | ||
}, [ | ||
initialFormValues, | ||
isLoadingPatientToEdit, | ||
patientToEdit, | ||
patientUuid, | ||
fieldConfigurations.phone.personAttributeUuid, | ||
]); | ||
|
||
// Set initial patient death info | ||
useEffect(() => { | ||
|
@@ -180,6 +188,64 @@ export function useInitialFormValues(patientUuid: string): [FormValues, Dispatch | |
return [initialFormValues, setInitialFormValues]; | ||
} | ||
|
||
export function useMpiInitialFormValues(patientUuid: string): [FormValues, Dispatch<FormValues>] { | ||
const { fieldConfigurations } = useConfig<RegistrationConfig>(); | ||
const { isLoading: isLoadingMpiPatient, patient: mpiPatient } = useMpiPatient(patientUuid); | ||
|
||
const [initialMPIFormValues, setInitialMPIFormValues] = useState<FormValues>({ | ||
patientUuid: v4(), | ||
givenName: '', | ||
middleName: '', | ||
familyName: '', | ||
additionalGivenName: '', | ||
additionalMiddleName: '', | ||
additionalFamilyName: '', | ||
addNameInLocalLanguage: false, | ||
gender: '', | ||
birthdate: null, | ||
yearsEstimated: 0, | ||
monthsEstimated: 0, | ||
birthdateEstimated: false, | ||
telephoneNumber: '', | ||
isDead: false, | ||
deathDate: undefined, | ||
deathTime: undefined, | ||
deathTimeFormat: 'AM', | ||
deathCause: '', | ||
nonCodedCauseOfDeath: '', | ||
relationships: [], | ||
identifiers: {}, | ||
address: {}, | ||
}); | ||
|
||
useEffect(() => { | ||
(async () => { | ||
if (mpiPatient?.data?.identifier) { | ||
const identifiers = await getIdentifierFieldValuesFromFhirPatient( | ||
mpiPatient.data, | ||
fieldConfigurations.identifierMappings, | ||
); | ||
|
||
const values = { | ||
...initialMPIFormValues, | ||
...getFormValuesFromFhirPatient(mpiPatient.data), | ||
address: getAddressFieldValuesFromFhirPatient(mpiPatient.data), | ||
identifiers, | ||
attributes: getPhonePersonAttributeValueFromFhirPatient( | ||
mpiPatient.data, | ||
fieldConfigurations.phone.personAttributeUuid, | ||
), | ||
}; | ||
setInitialMPIFormValues(values); | ||
} | ||
})(); | ||
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [mpiPatient, isLoadingMpiPatient]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need |
||
|
||
return [initialMPIFormValues, setInitialMPIFormValues]; | ||
} | ||
|
||
export function useInitialAddressFieldValues(patientUuid: string, fallback = {}): [object, Dispatch<object>] { | ||
const { isLoading, patient } = usePatient(patientUuid); | ||
const [initialAddressFieldValues, setInitialAddressFieldValues] = useState<object>(fallback); | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,6 +1,6 @@ | ||||||
import * as Yup from 'yup'; | ||||||
import camelCase from 'lodash-es/camelCase'; | ||||||
import { parseDate } from '@openmrs/esm-framework'; | ||||||
import { openmrsFetch, restBaseUrl, parseDate } from '@openmrs/esm-framework'; | ||||||
import { | ||||||
type AddressValidationSchemaType, | ||||||
type Encounter, | ||||||
|
@@ -116,9 +116,9 @@ export function getFormValuesFromFhirPatient(patient: fhir.Patient) { | |||||
result.middleName = patientName?.given[1]; | ||||||
result.familyName = patientName?.family; | ||||||
result.addNameInLocalLanguage = !!additionalPatientName ? true : undefined; | ||||||
result.additionalGivenName = additionalPatientName?.given[0]; | ||||||
result.additionalMiddleName = additionalPatientName?.given[1]; | ||||||
result.additionalFamilyName = additionalPatientName?.family; | ||||||
result.additionalGivenName = additionalPatientName?.given?.[0] ?? undefined; | ||||||
result.additionalMiddleName = additionalPatientName?.given?.[1] ?? undefined; | ||||||
result.additionalFamilyName = additionalPatientName?.family ?? undefined; | ||||||
|
||||||
result.gender = patient.gender; | ||||||
result.birthdate = patient.birthDate ? parseDate(patient.birthDate) : undefined; | ||||||
|
@@ -192,11 +192,59 @@ export function getPatientIdentifiersFromFhirPatient(patient: fhir.Patient): Arr | |||||
}); | ||||||
} | ||||||
|
||||||
export function getPhonePersonAttributeValueFromFhirPatient(patient: fhir.Patient) { | ||||||
export async function getIdentifierFieldValuesFromFhirPatient( | ||||||
patient: fhir.Patient, | ||||||
identifierConfig, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should have a type here of some sort right? |
||||||
): Promise<{ [identifierFieldName: string]: PatientIdentifierValue }> { | ||||||
const identifiers: FormValues['identifiers'] = {}; | ||||||
const promises: Promise<void>[] = []; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
|
||||||
|
||||||
for (const identifier of patient.identifier) { | ||||||
for (const config of identifierConfig) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than two nested for loops, it seems like it would make sense to loop through the |
||||||
if (config.fhirIdentifierSystem !== identifier.system) { | ||||||
continue; | ||||||
} | ||||||
|
||||||
const url = `${restBaseUrl}/patientidentifiertype/${config.openmrsIdentifierTypeUuid}`; | ||||||
|
||||||
promises.push( | ||||||
openmrsFetch(url) | ||||||
.then((response) => { | ||||||
if (!response.data?.name) { | ||||||
return; | ||||||
} | ||||||
identifiers[response.data.name] = { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the assumptions we're making here about the OpenMRS identifiers MPI identifiers correspond to need to be documented, i.e., I'm not always sure that |
||||||
identifierUuid: null, | ||||||
preferred: false, | ||||||
initialValue: identifier.value, | ||||||
identifierValue: identifier.value, | ||||||
identifierTypeUuid: config.identifierTypeUuid, | ||||||
identifierName: response.data.name, | ||||||
required: false, | ||||||
selectedSource: null, | ||||||
autoGeneration: false, | ||||||
}; | ||||||
}) | ||||||
.catch((error) => { | ||||||
console.error(`Error fetching identifier type for ${config.identifierTypeUuid}:`, error); | ||||||
}), | ||||||
); | ||||||
} | ||||||
} | ||||||
await Promise.all(promises); | ||||||
return identifiers; | ||||||
} | ||||||
|
||||||
export function getPhonePersonAttributeValueFromFhirPatient(patient: fhir.Patient, phoneUuid) { | ||||||
const result = {}; | ||||||
if (patient.telecom) { | ||||||
result['phone'] = patient.telecom[0].value; | ||||||
|
||||||
if (patient.telecom && Array.isArray(patient.telecom)) { | ||||||
const phoneEntry = patient.telecom.find((entry) => entry.system === 'phone'); | ||||||
if (phoneEntry) { | ||||||
result[phoneUuid] = phoneEntry.value; | ||||||
} | ||||||
} | ||||||
|
||||||
return result; | ||||||
} | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ import { | |
createErrorHandler, | ||
interpolateUrl, | ||
showSnackbar, | ||
useAppContext, | ||
useConfig, | ||
usePatient, | ||
usePatientPhoto, | ||
|
@@ -19,7 +20,12 @@ import { PatientRegistrationContext } from './patient-registration-context'; | |
import { type SavePatientForm, SavePatientTransactionManager } from './form-manager'; | ||
import { DummyDataInput } from './input/dummy-data/dummy-data-input.component'; | ||
import { cancelRegistration, filterOutUndefinedPatientIdentifiers, scrollIntoView } from './patient-registration-utils'; | ||
import { useInitialAddressFieldValues, useInitialFormValues, usePatientUuidMap } from './patient-registration-hooks'; | ||
import { | ||
useInitialAddressFieldValues, | ||
useMpiInitialFormValues, | ||
useInitialFormValuesLocal, | ||
usePatientUuidMap, | ||
} from './patient-registration-hooks'; | ||
import { ResourcesContext } from '../offline.resources'; | ||
import { builtInSections, type RegistrationConfig, type SectionDefinition } from '../config-schema'; | ||
import { SectionWrapper } from './section/section-wrapper.component'; | ||
|
@@ -39,10 +45,12 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa | |
const config = useConfig() as RegistrationConfig; | ||
const [target, setTarget] = useState<undefined | string>(); | ||
const { patientUuid: uuidOfPatientToEdit } = useParams(); | ||
const sourcePatientId = new URLSearchParams(search).get('sourceRecord'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this kind of data should be passed through the URL. It's a SPA, we can pass it through some kind of state variable (including the AppContext or something if necessary). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ibacher While we could use the AppContext API, wouldn’t a query parameter be a reasonable fit for passing a patient ID? Or is the concern about exposing identifiers, particularly those from external registries? If that’s not the case, creating a context for this might feel overkill. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @samuelmale I'm more concerned with the user winding up in a weird state when they, e.g., submit a patient registration and then click the Back button or something similar. But maybe that's silly? |
||
const { isLoading: isLoadingPatientToEdit, patient: patientToEdit } = usePatient(uuidOfPatientToEdit); | ||
const { t } = useTranslation(); | ||
const [capturePhotoProps, setCapturePhotoProps] = useState<CapturePhotoProps | null>(null); | ||
const [initialFormValues, setInitialFormValues] = useInitialFormValues(uuidOfPatientToEdit); | ||
const [initialFormValues, setInitialFormValues] = useInitialFormValuesLocal(uuidOfPatientToEdit); | ||
const [initialMPIFormValues, setInitialMPIFormValues] = useMpiInitialFormValues(sourcePatientId); | ||
const [initialAddressFieldValues] = useInitialAddressFieldValues(uuidOfPatientToEdit); | ||
const [patientUuidMap] = usePatientUuidMap(uuidOfPatientToEdit); | ||
const location = currentSession?.sessionLocation?.uuid; | ||
|
@@ -53,6 +61,12 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa | |
const fieldDefinition = config?.fieldDefinitions?.filter((def) => def.type === 'address'); | ||
const validationSchema = getValidationSchema(config); | ||
|
||
useEffect(() => { | ||
if (initialMPIFormValues) { | ||
setInitialFormValues(initialMPIFormValues); | ||
} | ||
}, [initialMPIFormValues, setInitialFormValues]); | ||
|
||
useEffect(() => { | ||
exportedInitialFormValuesForTesting = initialFormValues; | ||
}, [initialFormValues]); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would providing all the dependencies cause unnecessary rerenders?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Normally in situations where we use this, it's a good idea to add a comment explaining what dependencies we're explicitly excluding.