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

}, [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,
): Promise<{ [identifierFieldName: string]: PatientIdentifierValue }> {
const identifiers: FormValues['identifiers'] = {};
const promises: Promise<void>[] = [];

for (const identifier of patient.identifier) {
for (const config of identifierConfig) {
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] = {
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?

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,13 @@ 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);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialMPIFormValues, setInitialMPIFormValues]);
Copy link
Member

Choose a reason for hiding this comment

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

Is setInitialMPIFormValues a necessary dependency?


useEffect(() => {
exportedInitialFormValuesForTesting = initialFormValues;
}, [initialFormValues]);
Expand Down
Loading
Loading