Skip to content

Commit

Permalink
Offline Patient Registration: Interaction Support for Offline Registe…
Browse files Browse the repository at this point in the history
…red Patients | Alignments and Fixes (#86)

* Pre-merge master.

* Provide finalized patient info in sync queue.

* Translations.

* Revert package.json.

* Revert isOffline introduction.

* Leverage static patient UUIDs during creation.

* Moved FHIR mapping into control of patient-registration.

* Improve structure of patient registration sync item. Improve typings.

* @jsenv/file-size-impact

* Adapt code style.
  • Loading branch information
manuelroemer authored Jan 27, 2022
1 parent 3b8de04 commit 9f6c608
Show file tree
Hide file tree
Showing 12 changed files with 1,195 additions and 1,187 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@babel/preset-react": "^7.10.4",
"@babel/preset-typescript": "^7.10.4",
"@babel/runtime": "^7.12.13",
"@jsenv/file-size-impact": "^12.1.1",
"@jsenv/file-size-impact": "12.1.1",
"@openmrs/esm-framework": "next",
"@testing-library/dom": "^7.20.0",
"@testing-library/jest-dom": "^5.11.0",
Expand Down
62 changes: 33 additions & 29 deletions packages/esm-patient-registration-app/src/offline.resources.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import find from 'lodash-es/find';
import camelCase from 'lodash-es/camelCase';
import { FetchResponse, openmrsFetch, SessionUser } from '@openmrs/esm-framework';
import escapeRegExp from 'lodash-es/escapeRegExp';
import { FetchResponse, messageOmrsServiceWorker, openmrsFetch, SessionUser } from '@openmrs/esm-framework';
import { PatientIdentifierType, FetchedPatientIdentifierType } from './patient-registration/patient-registration-types';
import { mockAutoGenerationOptionsResult } from '../__mocks__/autogenerationoptions.mock';
import { cacheForOfflineHeaders } from './constants';
Expand All @@ -16,26 +17,20 @@ export interface Resources {
export const ResourcesContext = React.createContext<Resources>(null);

export async function fetchCurrentSession(abortController?: AbortController): Promise<FetchResponse<SessionUser>> {
const { data } = await openmrsFetch('/ws/rest/v1/session', {
signal: abortController?.signal,
headers: cacheForOfflineHeaders,
});
const { data } = await cacheAndFetch('/ws/rest/v1/session', abortController);
return data;
}

export async function fetchAddressTemplate(abortController?: AbortController) {
const { data } = await openmrsFetch('/ws/rest/v1/systemsetting?q=layout.address.format&v=custom:(value)', {
signal: abortController?.signal,
headers: cacheForOfflineHeaders,
});
const { data } = await cacheAndFetch(
'/ws/rest/v1/systemsetting?q=layout.address.format&v=custom:(value)',
abortController,
);
return data;
}

export async function fetchAllRelationshipTypes(abortController?: AbortController) {
const { data } = await openmrsFetch('/ws/rest/v1/relationshiptype?v=default', {
signal: abortController?.signal,
headers: cacheForOfflineHeaders,
});
const { data } = await cacheAndFetch('/ws/rest/v1/relationshiptype?v=default', abortController);
return data;
}

Expand Down Expand Up @@ -69,14 +64,14 @@ export async function fetchPatientIdentifierTypesWithSources(
}

async function fetchPrimaryIdentifierType(abortController: AbortController): Promise<FetchedPatientIdentifierType> {
const primaryIdentifierTypeResponse = await openmrsFetch(
const primaryIdentifierTypeResponse = await cacheAndFetch(
'/ws/rest/v1/metadatamapping/termmapping?v=full&code=emr.primaryIdentifierType',
{ signal: abortController?.signal, headers: cacheForOfflineHeaders },
abortController,
);

const { data: metadata } = await openmrsFetch(
const { data: metadata } = await cacheAndFetch(
`/ws/rest/v1/patientidentifiertype/${primaryIdentifierTypeResponse.data.results[0].metadataUuid}`,
{ signal: abortController?.signal, headers: cacheForOfflineHeaders },
abortController,
);

return {
Expand All @@ -92,25 +87,25 @@ async function fetchPrimaryIdentifierType(abortController: AbortController): Pro
async function fetchSecondaryIdentifierTypes(
abortController?: AbortController,
): Promise<Array<FetchedPatientIdentifierType>> {
const secondaryIdentifierTypeResponse = await openmrsFetch(
const secondaryIdentifierTypeResponse = await cacheAndFetch(
'/ws/rest/v1/metadatamapping/termmapping?v=full&code=emr.extraPatientIdentifierTypes',
{ signal: abortController?.signal, headers: cacheForOfflineHeaders },
abortController,
);

if (secondaryIdentifierTypeResponse.data.results) {
const extraIdentifierTypesSetUuid = secondaryIdentifierTypeResponse.data.results[0].metadataUuid;
const metadataResponse = await openmrsFetch(
const metadataResponse = await cacheAndFetch(
`/ws/rest/v1/metadatamapping/metadataset/${extraIdentifierTypesSetUuid}/members`,
{ signal: abortController?.signal, headers: cacheForOfflineHeaders },
abortController,
);

if (metadataResponse.data.results) {
return await Promise.all(
metadataResponse.data.results.map(async (setMember) => {
const type = await openmrsFetch(`/ws/rest/v1/patientidentifiertype/${setMember.metadataUuid}`, {
signal: abortController?.signal,
headers: cacheForOfflineHeaders,
});
const type = await cacheAndFetch(
`/ws/rest/v1/patientidentifiertype/${setMember.metadataUuid}`,
abortController,
);

return {
name: type.data.name,
Expand All @@ -129,10 +124,10 @@ async function fetchSecondaryIdentifierTypes(
}

async function fetchIdentifierSources(identifierType: string, abortController?: AbortController) {
return await openmrsFetch(`/ws/rest/v1/idgen/identifiersource?v=full&identifierType=${identifierType}`, {
signal: abortController?.signal,
headers: cacheForOfflineHeaders,
});
return await cacheAndFetch(
`/ws/rest/v1/idgen/identifiersource?v=full&identifierType=${identifierType}`,
abortController,
);
}

function fetchAutoGenerationOptions(identifierType: string, abortController?: AbortController) {
Expand All @@ -142,3 +137,12 @@ function fetchAutoGenerationOptions(identifierType: string, abortController?: Ab
// });‚
return Promise.resolve(mockAutoGenerationOptionsResult);
}

async function cacheAndFetch(url: string, abortController?: AbortController) {
await messageOmrsServiceWorker({
type: 'registerDynamicRoute',
pattern: escapeRegExp(url),
});

return await openmrsFetch(url, { headers: cacheForOfflineHeaders, signal: abortController?.signal });
}
18 changes: 9 additions & 9 deletions packages/esm-patient-registration-app/src/offline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ export async function syncPatientRegistration(
options: SyncProcessOptions<PatientRegistration>,
) {
await FormManager.savePatientFormOnline(
undefined,
queuedPatient.formValues,
queuedPatient.patientUuidMap,
queuedPatient.initialAddressFieldValues,
queuedPatient.identifierTypes,
queuedPatient.capturePhotoProps,
queuedPatient.patientPhotoConceptUuid,
queuedPatient.currentLocation,
queuedPatient.personAttributeSections,
queuedPatient._patientRegistrationData.isNewPatient,
queuedPatient._patientRegistrationData.formValues,
queuedPatient._patientRegistrationData.patientUuidMap,
queuedPatient._patientRegistrationData.initialAddressFieldValues,
queuedPatient._patientRegistrationData.identifierTypes,
queuedPatient._patientRegistrationData.capturePhotoProps,
queuedPatient._patientRegistrationData.patientPhotoConceptUuid,
queuedPatient._patientRegistrationData.currentLocation,
queuedPatient._patientRegistrationData.personAttributeSections,
options.abort,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
CapturePhotoProps,
PatientIdentifier,
PatientIdentifierValue,
PatientRegistration,
} from './patient-registration-types';
import {
addPatientIdentifier,
Expand All @@ -24,7 +25,7 @@ import {
import isEqual from 'lodash-es/isEqual';

export type SavePatientForm = (
patientUuid: string | undefined,
isNewPatient: boolean,
values: FormValues,
patientUuidMap: PatientUuidMapType,
initialAddressFieldValues: Record<string, any>,
Expand All @@ -38,7 +39,7 @@ export type SavePatientForm = (

export default class FormManager {
static async savePatientFormOffline(
patientUuid = v4(),
isNewPatient: boolean,
values: FormValues,
patientUuidMap: PatientUuidMapType,
initialAddressFieldValues: Record<string, any>,
Expand All @@ -48,10 +49,12 @@ export default class FormManager {
currentLocation: string,
personAttributeSections: any,
): Promise<null> {
await queueSynchronizationItem(
patientRegistration,
{
patientUuid,
const syncItem: PatientRegistration = {
fhirPatient: FormManager.mapPatientToFhirPatient(
FormManager.getPatientToCreate(values, personAttributeSections, patientUuidMap, initialAddressFieldValues, []),
),
_patientRegistrationData: {
isNewPatient,
formValues: values,
patientUuidMap,
initialAddressFieldValues,
Expand All @@ -61,18 +64,19 @@ export default class FormManager {
currentLocation,
personAttributeSections,
},
{
id: patientUuid,
displayName: 'Patient registration',
dependencies: [],
},
);
};

await queueSynchronizationItem(patientRegistration, syncItem, {
id: values.patientUuid,
displayName: 'Patient registration',
dependencies: [],
});

return null;
}

static async savePatientFormOnline(
patientUuid: string | undefined,
isNewPatient: boolean,
values: FormValues,
patientUuidMap: PatientUuidMapType,
initialAddressFieldValues: Record<string, any>,
Expand All @@ -90,9 +94,9 @@ export default class FormManager {
abortController,
);

if (patientUuid) {
if (!isNewPatient) {
await Promise.all(
FormManager.savePatientIdentifiers(patientUuid, patientIdentifiers, values.identifiers, abortController),
FormManager.savePatientIdentifiers(values.patientUuid, patientIdentifiers, values.identifiers, abortController),
);
}

Expand All @@ -104,11 +108,15 @@ export default class FormManager {
patientIdentifiers,
);

FormManager.getDeletedNames(patientUuidMap).forEach(async (name) => {
FormManager.getDeletedNames(values.patientUuid, patientUuidMap).forEach(async (name) => {
await deletePersonName(name.nameUuid, name.personUuid, abortController);
});

const savePatientResponse = await savePatient(abortController, createdPatient, patientUuidMap.patientUuid);
const savePatientResponse = await savePatient(
abortController,
createdPatient,
isNewPatient ? undefined : values.patientUuid,
);

if (savePatientResponse.ok) {
await Promise.all(
Expand Down Expand Up @@ -145,7 +153,7 @@ export default class FormManager {
}

static getPatientIdentifiersToCreate(
patientIdentifiers: PatientIdentifierValue[], // values.identifiers
patientIdentifiers: PatientIdentifierValue[],
identifierTypes: Array<PatientIdentifierType>,
location: string,
abortController: AbortController,
Expand Down Expand Up @@ -192,12 +200,12 @@ export default class FormManager {
return Promise.all(identifierTypeRequests);
}

static getDeletedNames(patientUuidMap: PatientUuidMapType) {
static getDeletedNames(patientUuid: string, patientUuidMap: PatientUuidMapType) {
if (patientUuidMap?.additionalNameUuid) {
return [
{
nameUuid: patientUuidMap.additionalNameUuid,
personUuid: patientUuidMap.patientUuid,
personUuid: patientUuid,
},
];
}
Expand All @@ -218,9 +226,9 @@ export default class FormManager {
}

return {
uuid: patientUuidMap['patientUuid'],
uuid: values.patientUuid,
person: {
uuid: patientUuidMap['patientUuid'],
uuid: values.patientUuid,
names: FormManager.getNames(values, patientUuidMap),
gender: values.gender.charAt(0),
birthdate: values.birthdate,
Expand Down Expand Up @@ -309,4 +317,45 @@ export default class FormManager {
}
});
}

static mapPatientToFhirPatient(patient: Partial<Patient>): fhir.Patient {
// Important:
// When changing this code, ideally assume that `patient` can be missing any attribute.
// The `fhir.Patient` provides us with the benefit that all properties are nullable and thus
// not required (technically, at least). -> Even if we cannot map some props here, we still
// provide a valid fhir.Patient object. The various patient chart modules should be able to handle
// such missing props correctly (and should be updated if they don't).

// Gender in the original object only uses a single letter. fhir.Patient expects a full string.
const genderMap = {
M: 'male',
F: 'female',
O: 'other',
U: 'unknown',
};

// Mapping inspired by:
// https://github.com/openmrs/openmrs-module-fhir/blob/669b3c52220bb9abc622f815f4dc0d8523687a57/api/src/main/java/org/openmrs/module/fhir/api/util/FHIRPatientUtil.java#L36
// https://github.com/openmrs/openmrs-esm-patient-management/blob/94e6f637fb37cf4984163c355c5981ea6b8ca38c/packages/esm-patient-search-app/src/patient-search-result/patient-search-result.component.tsx#L21
// Update as required.
return {
id: patient.uuid,
gender: genderMap[patient.person?.gender],
birthDate: patient.person?.birthdate,
deceasedBoolean: patient.person.dead,
deceasedDateTime: patient.person.deathDate,
name: patient.person?.names?.map((name) => ({
given: [name.givenName, name.middleName].filter(Boolean),
family: name.familyName,
})),
address: patient.person?.addresses.map((address) => ({
city: address.cityVillage,
country: address.country,
postalCode: address.postalCode,
state: address.stateProvince,
use: 'home',
})),
telecom: patient.person.attributes?.filter((attribute) => attribute.attributeType === 'Telephone Number'),
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { v4 } from 'uuid';
import { FormValues } from '../../patient-registration-types';
import styles from './../input.scss';

Expand All @@ -7,6 +8,7 @@ interface DummyDataInputProps {
}

export const dummyFormValues: FormValues = {
patientUuid: v4(),
givenName: 'John',
middleName: '',
familyName: 'Smith',
Expand Down
Loading

0 comments on commit 9f6c608

Please sign in to comment.