Skip to content
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

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/esm-patient-registration-app/src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface RegistrationConfig {
month: number;
};
};
identifierMappings: [{ fhirIdentifierSystem: string; openmrsIdentifierTypeUuid: string }];
phone: {
personAttributeUuid: string;
validation?: {
Expand Down Expand Up @@ -351,6 +352,21 @@ export const esmPatientRegistrationSchema = {
},
},
},
identifierMappings: {
_type: Type.Array,
_elements: {
fhirIdentifierSystem: {
_type: Type.String,
_description: 'Identifier system from the fhir server',
},
openmrsIdentifierTypeUuid: {
_type: Type.String,
_default: null,
_description: 'Identifier type uuid of OpenMRS to map the identifier system',
},
},
_default: [],
},
phone: {
personAttributeUuid: {
_type: Type.UUID,
Expand Down
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
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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
Copy link
Member

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?

Copy link
Member

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.

}, [mpiPatient, isLoadingMpiPatient]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need isLoadingMpiPatient as a dependency or its ripple effect?


return [initialMPIFormValues, setInitialMPIFormValues];
}

export function useInitialAddressFieldValues(patientUuid: string, fallback = {}): [object, Dispatch<object>] {
const { isLoading, patient } = usePatient(patientUuid);
const [initialAddressFieldValues, setInitialAddressFieldValues] = useState<object>(fallback);
Expand Down
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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The 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>[] = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
const promises: Promise<void>[] = [];
const promises: Array<Promise<void>> = [];


for (const identifier of patient.identifier) {
for (const config of identifierConfig) {
Copy link
Member

Choose a reason for hiding this comment

The 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 identifierConfig once and create an object that keyed from fhirIdentifierSystem to the config object associated with it. That way, we loop through it once and the lookup from identifier -> identifierConfig is an O(1) operation.

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] = {
Copy link
Member

Choose a reason for hiding this comment

The 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 preferred : false is the correct behaviour.

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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
createErrorHandler,
interpolateUrl,
showSnackbar,
useAppContext,
useConfig,
usePatient,
usePatientPhoto,
Expand All @@ -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';
Expand All @@ -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');
Copy link
Member

Choose a reason for hiding this comment

The 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).

Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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;
Expand All @@ -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]);
Expand Down
Loading
Loading