@kenyaemr/esm-patient-registration-app 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +7 -0
  2. package/__mocks__/autogenerationoptions.mock.ts +34 -0
  3. package/__mocks__/react-i18next.js +49 -0
  4. package/dist/144.js +2 -0
  5. package/dist/144.js.LICENSE.txt +27 -0
  6. package/dist/144.js.map +1 -0
  7. package/dist/207.js +1 -0
  8. package/dist/207.js.map +1 -0
  9. package/dist/317.js +2 -0
  10. package/dist/317.js.LICENSE.txt +6 -0
  11. package/dist/317.js.map +1 -0
  12. package/dist/330.js +1 -0
  13. package/dist/330.js.map +1 -0
  14. package/dist/574.js +1 -0
  15. package/dist/59.js +1 -0
  16. package/dist/59.js.map +1 -0
  17. package/dist/591.js +2 -0
  18. package/dist/591.js.LICENSE.txt +32 -0
  19. package/dist/591.js.map +1 -0
  20. package/dist/62.js +1 -0
  21. package/dist/62.js.map +1 -0
  22. package/dist/635.js +1 -0
  23. package/dist/635.js.map +1 -0
  24. package/dist/68.js +1 -0
  25. package/dist/68.js.map +1 -0
  26. package/dist/735.js +1 -0
  27. package/dist/735.js.map +1 -0
  28. package/dist/757.js +1 -0
  29. package/dist/784.js +2 -0
  30. package/dist/784.js.LICENSE.txt +9 -0
  31. package/dist/784.js.map +1 -0
  32. package/dist/805.js +1 -0
  33. package/dist/805.js.map +1 -0
  34. package/dist/807.js +1 -0
  35. package/dist/821.js +1 -0
  36. package/dist/821.js.map +1 -0
  37. package/dist/822.js +1 -0
  38. package/dist/822.js.map +1 -0
  39. package/dist/858.js +2 -0
  40. package/dist/858.js.LICENSE.txt +3 -0
  41. package/dist/858.js.map +1 -0
  42. package/dist/887.js +1 -0
  43. package/dist/887.js.map +1 -0
  44. package/dist/9.js +2 -0
  45. package/dist/9.js.LICENSE.txt +9 -0
  46. package/dist/9.js.map +1 -0
  47. package/dist/975.js +1 -0
  48. package/dist/975.js.map +1 -0
  49. package/dist/main.js +2 -0
  50. package/dist/main.js.LICENSE.txt +9 -0
  51. package/dist/main.js.map +1 -0
  52. package/dist/openmrs-esm-patient-registration-app.js +1 -0
  53. package/dist/openmrs-esm-patient-registration-app.js.buildmanifest.json +623 -0
  54. package/dist/openmrs-esm-patient-registration-app.js.map +1 -0
  55. package/dist/openmrs-esm-patient-registration-app.old +1 -0
  56. package/docs/images/patient-registration-hierarchy.png +0 -0
  57. package/package.json +55 -0
  58. package/src/add-patient-link.scss +3 -0
  59. package/src/add-patient-link.tsx +21 -0
  60. package/src/config-schema.ts +405 -0
  61. package/src/constants.ts +14 -0
  62. package/src/declarations.d.tsx +4 -0
  63. package/src/index.ts +131 -0
  64. package/src/nav-link.tsx +10 -0
  65. package/src/offline.resources.ts +109 -0
  66. package/src/offline.ts +90 -0
  67. package/src/patient-registration/before-save-prompt.tsx +72 -0
  68. package/src/patient-registration/date-util.ts +52 -0
  69. package/src/patient-registration/field/__mocks__/field.resource.ts +60 -0
  70. package/src/patient-registration/field/address/address-field.component.tsx +31 -0
  71. package/src/patient-registration/field/address/address-hierarchy.component.tsx +143 -0
  72. package/src/patient-registration/field/address/address-hierarchy.test.tsx +181 -0
  73. package/src/patient-registration/field/address/address-search.component.tsx +98 -0
  74. package/src/patient-registration/field/address/address-search.scss +53 -0
  75. package/src/patient-registration/field/custom-field.component.tsx +25 -0
  76. package/src/patient-registration/field/dob/dob.component.tsx +143 -0
  77. package/src/patient-registration/field/dob/dob.test.tsx +73 -0
  78. package/src/patient-registration/field/field.component.tsx +44 -0
  79. package/src/patient-registration/field/field.resource.ts +35 -0
  80. package/src/patient-registration/field/field.scss +127 -0
  81. package/src/patient-registration/field/gender/gender-field.component.tsx +49 -0
  82. package/src/patient-registration/field/gender/gender-field.test.tsx +66 -0
  83. package/src/patient-registration/field/id/id-field.component.tsx +142 -0
  84. package/src/patient-registration/field/id/identifier-selection-overlay.tsx +194 -0
  85. package/src/patient-registration/field/id/identifier-selection.scss +37 -0
  86. package/src/patient-registration/field/name/name-field.component.tsx +109 -0
  87. package/src/patient-registration/field/obs/obs-field.component.tsx +185 -0
  88. package/src/patient-registration/field/obs/obs-field.test.tsx +127 -0
  89. package/src/patient-registration/field/person-attributes/coded-attributes.component.tsx +59 -0
  90. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +68 -0
  91. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +81 -0
  92. package/src/patient-registration/field/person-attributes/person-attributes.resource.tsx +20 -0
  93. package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +57 -0
  94. package/src/patient-registration/form-manager.test.ts +68 -0
  95. package/src/patient-registration/form-manager.ts +413 -0
  96. package/src/patient-registration/input/basic-input/input/input.component.tsx +59 -0
  97. package/src/patient-registration/input/basic-input/input/input.test.tsx +170 -0
  98. package/src/patient-registration/input/basic-input/select/select-input.component.tsx +32 -0
  99. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +32 -0
  100. package/src/patient-registration/input/combo-input/combo-input.component.tsx +76 -0
  101. package/src/patient-registration/input/combo-input/combo-input.test.tsx +43 -0
  102. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +84 -0
  103. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +53 -0
  104. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +109 -0
  105. package/src/patient-registration/input/custom-input/estimated-age/estimated-age-input.component.tsx +32 -0
  106. package/src/patient-registration/input/custom-input/estimated-age/estimated-age-input.test.tsx +36 -0
  107. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +156 -0
  108. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +110 -0
  109. package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
  110. package/src/patient-registration/input/custom-input/unidentified-patient/unidentified-patient-input.component.tsx +24 -0
  111. package/src/patient-registration/input/custom-input/unidentified-patient/unidentified-patient-input.test.tsx +39 -0
  112. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +53 -0
  113. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +43 -0
  114. package/src/patient-registration/input/input.scss +108 -0
  115. package/src/patient-registration/patient-registration-context.ts +24 -0
  116. package/src/patient-registration/patient-registration-hooks.ts +320 -0
  117. package/src/patient-registration/patient-registration-types.tsx +271 -0
  118. package/src/patient-registration/patient-registration-utils.ts +219 -0
  119. package/src/patient-registration/patient-registration.component.tsx +250 -0
  120. package/src/patient-registration/patient-registration.resource.test.tsx +26 -0
  121. package/src/patient-registration/patient-registration.resource.tsx +296 -0
  122. package/src/patient-registration/patient-registration.scss +94 -0
  123. package/src/patient-registration/patient-registration.test.tsx +436 -0
  124. package/src/patient-registration/section/death-info/death-info-section.component.tsx +30 -0
  125. package/src/patient-registration/section/death-info/death-info-section.test.tsx +73 -0
  126. package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
  127. package/src/patient-registration/section/demographics/demographics-section.test.tsx +84 -0
  128. package/src/patient-registration/section/generic-section.component.tsx +17 -0
  129. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +226 -0
  130. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +78 -0
  131. package/src/patient-registration/section/patient-relationships/relationships.scss +35 -0
  132. package/src/patient-registration/section/section-wrapper.component.tsx +40 -0
  133. package/src/patient-registration/section/section.component.tsx +23 -0
  134. package/src/patient-registration/section/section.scss +1 -0
  135. package/src/patient-registration/ui-components/overlay/index.tsx +51 -0
  136. package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
  137. package/src/patient-registration/validation/patient-registration-validation.test.tsx +129 -0
  138. package/src/patient-registration/validation/patient-registration-validation.tsx +46 -0
  139. package/src/patient-verification/assets/counties.json +236 -0
  140. package/src/patient-verification/assets/verification-assets.ts +11 -0
  141. package/src/patient-verification/patient-verification-hook.tsx +156 -0
  142. package/src/patient-verification/patient-verification-utils.ts +173 -0
  143. package/src/patient-verification/patient-verification.component.tsx +118 -0
  144. package/src/patient-verification/patient-verification.scss +30 -0
  145. package/src/patient-verification/verification-modal/confirm-prompt.component.tsx +69 -0
  146. package/src/patient-verification/verification-modal/empty-prompt.component.tsx +35 -0
  147. package/src/patient-verification/verification-types.ts +50 -0
  148. package/src/resource.ts +12 -0
  149. package/src/root.component.tsx +66 -0
  150. package/src/root.scss +7 -0
  151. package/src/root.test.tsx +32 -0
  152. package/src/widgets/cancel-patient-edit.component.tsx +37 -0
  153. package/src/widgets/delete-identifier-confirmation-modal.tsx +41 -0
  154. package/src/widgets/delete-identifier-modal.scss +34 -0
  155. package/src/widgets/display-photo.component.tsx +30 -0
  156. package/src/widgets/edit-patient-details-button.component.tsx +34 -0
  157. package/src/widgets/edit-patient-details-button.scss +3 -0
  158. package/translations/en.json +108 -0
  159. package/translations/fr.json +89 -0
  160. package/translations/km.json +89 -0
  161. package/tsconfig.json +5 -0
  162. package/webpack.config.js +1 -0
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import styles from './../field.scss';
3
+ import { Input } from '../../input/basic-input/input/input.component';
4
+ import { Field } from 'formik';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { PersonAttributeTypeResponse } from '../../patient-registration-types';
7
+
8
+ export interface TextPersonAttributeFieldProps {
9
+ id: string;
10
+ personAttributeType: PersonAttributeTypeResponse;
11
+ validationRegex?: string;
12
+ label?: string;
13
+ required?: boolean;
14
+ }
15
+
16
+ export function TextPersonAttributeField({
17
+ id,
18
+ personAttributeType,
19
+ validationRegex,
20
+ label,
21
+ required,
22
+ }: TextPersonAttributeFieldProps) {
23
+ const { t } = useTranslation();
24
+
25
+ const validateInput = (value: string) => {
26
+ if (!value || !validationRegex || validationRegex === '' || typeof validationRegex !== 'string' || value === '') {
27
+ return;
28
+ }
29
+ const regex = new RegExp(validationRegex);
30
+ if (regex.test(value)) {
31
+ return;
32
+ } else {
33
+ return t('invalidInput', 'Invalid Input');
34
+ }
35
+ };
36
+
37
+ const fieldName = `attributes.${personAttributeType.uuid}`;
38
+
39
+ return (
40
+ <div className={`${styles.customField} ${styles.halfWidthInDesktopView}`}>
41
+ <Field name={fieldName} validate={validateInput}>
42
+ {({ field, form: { touched, errors }, meta }) => {
43
+ return (
44
+ <Input
45
+ id={id}
46
+ name={`person-attribute-${personAttributeType.uuid}`}
47
+ labelText={label ?? personAttributeType?.display}
48
+ invalid={errors[fieldName] && touched[fieldName]}
49
+ {...field}
50
+ required={required}
51
+ />
52
+ );
53
+ }}
54
+ </Field>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,68 @@
1
+ import { FormManager } from './form-manager';
2
+ import { FormValues } from './patient-registration-types';
3
+
4
+ jest.mock('./patient-registration.resource');
5
+
6
+ const formValues: FormValues = {
7
+ patientUuid: '',
8
+ givenName: '',
9
+ middleName: '',
10
+ familyName: '',
11
+ unidentifiedPatient: false,
12
+ additionalGivenName: '',
13
+ additionalMiddleName: '',
14
+ additionalFamilyName: '',
15
+ addNameInLocalLanguage: false,
16
+ gender: '',
17
+ birthdate: '',
18
+ yearsEstimated: 1000,
19
+ monthsEstimated: 11,
20
+ birthdateEstimated: false,
21
+ telephoneNumber: '',
22
+ isDead: false,
23
+ deathDate: 'string',
24
+ deathCause: 'string',
25
+ relationships: [],
26
+ address: {
27
+ address1: '',
28
+ address2: '',
29
+ cityVillage: '',
30
+ stateProvince: 'New York',
31
+ country: 'string',
32
+ postalCode: 'string',
33
+ },
34
+ identifiers: {
35
+ foo: {
36
+ identifierUuid: 'aUuid',
37
+ identifierName: 'Foo',
38
+ required: false,
39
+ initialValue: 'foo',
40
+ identifierValue: 'foo',
41
+ identifierTypeUuid: 'identifierType',
42
+ preferred: true,
43
+ autoGeneration: false,
44
+ selectedSource: {
45
+ uuid: 'some-uuid',
46
+ name: 'unique',
47
+ autoGenerationOption: { manualEntryEnabled: true, automaticGenerationEnabled: false },
48
+ },
49
+ },
50
+ },
51
+ };
52
+
53
+ describe('FormManager', () => {
54
+ describe('createIdentifiers', () => {
55
+ it('uses the uuid of a field name if it exists', async () => {
56
+ const result = await FormManager.savePatientIdentifiers(true, undefined, formValues.identifiers, {}, 'Nyc');
57
+ expect(result).toEqual([
58
+ {
59
+ uuid: 'aUuid',
60
+ identifier: 'foo',
61
+ identifierType: 'identifierType',
62
+ location: 'Nyc',
63
+ preferred: true,
64
+ },
65
+ ]);
66
+ });
67
+ });
68
+ });
@@ -0,0 +1,413 @@
1
+ import { FetchResponse, queueSynchronizationItem, Session, toOmrsIsoString } from '@openmrs/esm-framework';
2
+ import { patientRegistration } from '../constants';
3
+ import {
4
+ FormValues,
5
+ AttributeValue,
6
+ PatientUuidMapType,
7
+ Patient,
8
+ CapturePhotoProps,
9
+ PatientIdentifier,
10
+ PatientRegistration,
11
+ RelationshipValue,
12
+ Encounter,
13
+ } from './patient-registration-types';
14
+ import {
15
+ addPatientIdentifier,
16
+ deletePatientIdentifier,
17
+ deletePersonName,
18
+ deleteRelationship,
19
+ generateIdentifier,
20
+ savePatient,
21
+ savePatientPhoto,
22
+ saveRelationship,
23
+ updateRelationship,
24
+ updatePatientIdentifier,
25
+ saveEncounter,
26
+ } from './patient-registration.resource';
27
+ import isEqual from 'lodash-es/isEqual';
28
+ import { RegistrationConfig } from '../config-schema';
29
+
30
+ export type SavePatientForm = (
31
+ isNewPatient: boolean,
32
+ values: FormValues,
33
+ patientUuidMap: PatientUuidMapType,
34
+ initialAddressFieldValues: Record<string, any>,
35
+ capturePhotoProps: CapturePhotoProps,
36
+ currentLocation: string,
37
+ initialIdentifierValues: FormValues['identifiers'],
38
+ currentUser: Session,
39
+ config: RegistrationConfig,
40
+ savePatientTransactionManager: SavePatientTransactionManager,
41
+ abortController?: AbortController,
42
+ ) => Promise<string | void>;
43
+ export class FormManager {
44
+ static savePatientFormOffline: SavePatientForm = async (
45
+ isNewPatient,
46
+ values,
47
+ patientUuidMap,
48
+ initialAddressFieldValues,
49
+ capturePhotoProps,
50
+ currentLocation,
51
+ initialIdentifierValues,
52
+ currentUser,
53
+ config,
54
+ ) => {
55
+ const syncItem: PatientRegistration = {
56
+ fhirPatient: FormManager.mapPatientToFhirPatient(
57
+ FormManager.getPatientToCreate(values, patientUuidMap, initialAddressFieldValues, []),
58
+ ),
59
+ _patientRegistrationData: {
60
+ isNewPatient,
61
+ formValues: values,
62
+ patientUuidMap,
63
+ initialAddressFieldValues,
64
+ capturePhotoProps,
65
+ currentLocation,
66
+ initialIdentifierValues,
67
+ currentUser,
68
+ config,
69
+ savePatientTransactionManager: new SavePatientTransactionManager(),
70
+ },
71
+ };
72
+
73
+ await queueSynchronizationItem(patientRegistration, syncItem, {
74
+ id: values.patientUuid,
75
+ displayName: 'Patient registration',
76
+ patientUuid: syncItem.fhirPatient.id,
77
+ dependencies: [],
78
+ });
79
+
80
+ return null;
81
+ };
82
+
83
+ static savePatientFormOnline: SavePatientForm = async (
84
+ isNewPatient,
85
+ values,
86
+ patientUuidMap,
87
+ initialAddressFieldValues,
88
+ capturePhotoProps,
89
+ currentLocation,
90
+ initialIdentifierValues,
91
+ currentUser,
92
+ config,
93
+ savePatientTransactionManager,
94
+ abortController,
95
+ ) => {
96
+ const patientIdentifiers: Array<PatientIdentifier> = await FormManager.savePatientIdentifiers(
97
+ isNewPatient,
98
+ values.patientUuid,
99
+ values.identifiers,
100
+ initialIdentifierValues,
101
+ currentLocation,
102
+ );
103
+
104
+ const createdPatient = FormManager.getPatientToCreate(
105
+ values,
106
+ patientUuidMap,
107
+ initialAddressFieldValues,
108
+ patientIdentifiers,
109
+ );
110
+
111
+ FormManager.getDeletedNames(values.patientUuid, patientUuidMap).forEach(async (name) => {
112
+ await deletePersonName(name.nameUuid, name.personUuid);
113
+ });
114
+
115
+ const savePatientResponse = await savePatient(
116
+ createdPatient,
117
+ isNewPatient && !savePatientTransactionManager.patientSaved ? undefined : values.patientUuid,
118
+ );
119
+
120
+ if (savePatientResponse.ok) {
121
+ savePatientTransactionManager.patientSaved = true;
122
+ await this.saveRelationships(values.relationships, savePatientResponse);
123
+
124
+ await this.saveObservations(values.obs, savePatientResponse, currentLocation, currentUser, config);
125
+
126
+ if (config.concepts.patientPhotoUuid && capturePhotoProps?.imageData) {
127
+ await savePatientPhoto(
128
+ savePatientResponse.data.uuid,
129
+ capturePhotoProps.imageData,
130
+ '/ws/rest/v1/obs',
131
+ capturePhotoProps.dateTime || new Date().toISOString(),
132
+ config.concepts.patientPhotoUuid,
133
+ );
134
+ }
135
+ }
136
+
137
+ return savePatientResponse.data.uuid;
138
+ };
139
+
140
+ static async saveRelationships(relationships: Array<RelationshipValue>, savePatientResponse: FetchResponse) {
141
+ return Promise.all(
142
+ relationships
143
+ .filter((m) => m.relationshipType)
144
+ .filter((relationship) => !!relationship.action)
145
+ .map(({ relatedPersonUuid, relationshipType, uuid: relationshipUuid, action }) => {
146
+ const [type, direction] = relationshipType.split('/');
147
+ const thisPatientUuid = savePatientResponse.data.uuid;
148
+ const isAToB = direction === 'aIsToB';
149
+ const relationshipToSave = {
150
+ personA: isAToB ? relatedPersonUuid : thisPatientUuid,
151
+ personB: isAToB ? thisPatientUuid : relatedPersonUuid,
152
+ relationshipType: type,
153
+ };
154
+
155
+ switch (action) {
156
+ case 'ADD':
157
+ return saveRelationship(relationshipToSave);
158
+ case 'UPDATE':
159
+ return updateRelationship(relationshipUuid, relationshipToSave);
160
+ case 'DELETE':
161
+ return deleteRelationship(relationshipUuid);
162
+ }
163
+ }),
164
+ );
165
+ }
166
+
167
+ static async saveObservations(
168
+ obss: { [conceptUuid: string]: string },
169
+ savePatientResponse: FetchResponse,
170
+ currentLocation: string,
171
+ currentUser: Session,
172
+ config: RegistrationConfig,
173
+ ) {
174
+ if (obss && Object.keys(obss).length > 0) {
175
+ if (!config.registrationObs.encounterTypeUuid) {
176
+ console.error(
177
+ 'The registration form has been configured to have obs fields, ' +
178
+ 'but no registration encounter type has been configured. Obs field values ' +
179
+ 'will not be saved.',
180
+ );
181
+ } else {
182
+ const encounterToSave: Encounter = {
183
+ encounterDatetime: toOmrsIsoString(new Date()),
184
+ patient: savePatientResponse.data.uuid,
185
+ encounterType: config.registrationObs.encounterTypeUuid,
186
+ location: currentLocation,
187
+ encounterProviders: [
188
+ {
189
+ provider: currentUser.currentProvider.uuid,
190
+ encounterRole: config.registrationObs.encounterProviderRoleUuid,
191
+ },
192
+ ],
193
+ form: config.registrationObs.registrationFormUuid,
194
+ obs: Object.keys(obss).map((conceptUuid) => ({
195
+ concept: conceptUuid,
196
+ value: obss[conceptUuid],
197
+ })),
198
+ };
199
+ return saveEncounter(encounterToSave);
200
+ }
201
+ }
202
+ }
203
+
204
+ static async savePatientIdentifiers(
205
+ isNewPatient: boolean,
206
+ patientUuid: string,
207
+ patientIdentifiers: FormValues['identifiers'], // values.identifiers
208
+ initialIdentifierValues: FormValues['identifiers'], // Initial identifiers assigned to the patient
209
+ location: string,
210
+ ): Promise<Array<PatientIdentifier>> {
211
+ let identifierTypeRequests = Object.values(patientIdentifiers)
212
+ /* Since default identifier-types will be present on the form and are also in the not-required state,
213
+ therefore we might be running into situations when there's no value and no source associated,
214
+ hence filtering these fields out.
215
+ */
216
+ .filter(
217
+ ({ identifierValue, autoGeneration, selectedSource }) => identifierValue || (autoGeneration && selectedSource),
218
+ )
219
+ .map(async (patientIdentifier) => {
220
+ const {
221
+ identifierTypeUuid,
222
+ identifierValue,
223
+ identifierUuid,
224
+ selectedSource,
225
+ preferred,
226
+ autoGeneration,
227
+ initialValue,
228
+ } = patientIdentifier;
229
+
230
+ const identifier = !autoGeneration
231
+ ? identifierValue
232
+ : await (
233
+ await generateIdentifier(selectedSource.uuid)
234
+ ).data.identifier;
235
+ const identifierToCreate = {
236
+ uuid: identifierUuid,
237
+ identifier,
238
+ identifierType: identifierTypeUuid,
239
+ location,
240
+ preferred,
241
+ };
242
+
243
+ if (!isNewPatient) {
244
+ if (!initialValue) {
245
+ await addPatientIdentifier(patientUuid, identifierToCreate);
246
+ } else if (initialValue !== identifier) {
247
+ await updatePatientIdentifier(patientUuid, identifierUuid, identifierToCreate.identifier);
248
+ }
249
+ }
250
+
251
+ return identifierToCreate;
252
+ });
253
+
254
+ /*
255
+ If there was initially an identifier assigned to the patient,
256
+ which is now not present in the patientIdentifiers(values.identifiers),
257
+ this means that the identifier is meant to be deleted, hence we need
258
+ to delete the respective identifiers.
259
+ */
260
+
261
+ if (patientUuid) {
262
+ Object.keys(initialIdentifierValues)
263
+ .filter((identifierFieldName) => !patientIdentifiers[identifierFieldName])
264
+ .forEach(async (identifierFieldName) => {
265
+ await deletePatientIdentifier(patientUuid, initialIdentifierValues[identifierFieldName].identifierUuid);
266
+ });
267
+ }
268
+
269
+ return Promise.all(identifierTypeRequests);
270
+ }
271
+
272
+ static getDeletedNames(patientUuid: string, patientUuidMap: PatientUuidMapType) {
273
+ if (patientUuidMap?.additionalNameUuid) {
274
+ return [
275
+ {
276
+ nameUuid: patientUuidMap.additionalNameUuid,
277
+ personUuid: patientUuid,
278
+ },
279
+ ];
280
+ }
281
+ return [];
282
+ }
283
+
284
+ static getPatientToCreate(
285
+ values: FormValues,
286
+ patientUuidMap: PatientUuidMapType,
287
+ initialAddressFieldValues: Record<string, any>,
288
+ identifiers: Array<PatientIdentifier>,
289
+ ): Patient {
290
+ let birthdate;
291
+ if (values.birthdate instanceof Date) {
292
+ birthdate = [values.birthdate.getFullYear(), values.birthdate.getMonth() + 1, values.birthdate.getDate()].join(
293
+ '-',
294
+ );
295
+ } else {
296
+ birthdate = values.birthdate;
297
+ }
298
+
299
+ return {
300
+ uuid: values.patientUuid,
301
+ person: {
302
+ uuid: values.patientUuid,
303
+ names: FormManager.getNames(values, patientUuidMap),
304
+ gender: values.gender.charAt(0),
305
+ birthdate,
306
+ birthdateEstimated: values.birthdateEstimated,
307
+ attributes: FormManager.getPatientAttributes(values),
308
+ addresses: [values.address],
309
+ ...FormManager.getPatientDeathInfo(values),
310
+ },
311
+ identifiers,
312
+ };
313
+ }
314
+
315
+ static getNames(values: FormValues, patientUuidMap: PatientUuidMapType) {
316
+ const names = [
317
+ {
318
+ uuid: patientUuidMap.preferredNameUuid,
319
+ preferred: true,
320
+ givenName: values.givenName,
321
+ middleName: values.middleName,
322
+ familyName: values.familyName,
323
+ },
324
+ ];
325
+
326
+ if (values.addNameInLocalLanguage) {
327
+ names.push({
328
+ uuid: patientUuidMap.additionalNameUuid,
329
+ preferred: false,
330
+ givenName: values.additionalGivenName,
331
+ middleName: values.additionalMiddleName,
332
+ familyName: values.additionalFamilyName,
333
+ });
334
+ }
335
+
336
+ return names;
337
+ }
338
+
339
+ static getPatientAttributes(values: FormValues) {
340
+ const attributes: Array<AttributeValue> = [];
341
+ if (values.attributes) {
342
+ for (const [key, value] of Object.entries(values.attributes)) {
343
+ attributes.push({
344
+ attributeType: key,
345
+ value,
346
+ });
347
+ }
348
+ }
349
+ if (values.unidentifiedPatient) {
350
+ attributes.push({
351
+ // The UUID of the 'Unknown Patient' attribute-type will always be static across all implementations of OpenMRS
352
+ attributeType: '8b56eac7-5c76-4b9c-8c6f-1deab8d3fc47',
353
+ value: 'true',
354
+ });
355
+ }
356
+
357
+ return attributes;
358
+ }
359
+
360
+ static getPatientDeathInfo(values: FormValues) {
361
+ const { isDead, deathDate, deathCause } = values;
362
+ return {
363
+ dead: isDead,
364
+ deathDate: isDead ? deathDate : undefined,
365
+ causeOfDeath: isDead ? deathCause : undefined,
366
+ };
367
+ }
368
+
369
+ static mapPatientToFhirPatient(patient: Partial<Patient>): fhir.Patient {
370
+ // Important:
371
+ // When changing this code, ideally assume that `patient` can be missing any attribute.
372
+ // The `fhir.Patient` provides us with the benefit that all properties are nullable and thus
373
+ // not required (technically, at least). -> Even if we cannot map some props here, we still
374
+ // provide a valid fhir.Patient object. The various patient chart modules should be able to handle
375
+ // such missing props correctly (and should be updated if they don't).
376
+
377
+ // Gender in the original object only uses a single letter. fhir.Patient expects a full string.
378
+ const genderMap = {
379
+ M: 'male',
380
+ F: 'female',
381
+ O: 'other',
382
+ U: 'unknown',
383
+ };
384
+
385
+ // Mapping inspired by:
386
+ // https://github.com/openmrs/openmrs-module-fhir/blob/669b3c52220bb9abc622f815f4dc0d8523687a57/api/src/main/java/org/openmrs/module/fhir/api/util/FHIRPatientUtil.java#L36
387
+ // 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
388
+ // Update as required.
389
+ return {
390
+ id: patient.uuid,
391
+ gender: genderMap[patient.person?.gender],
392
+ birthDate: patient.person?.birthdate,
393
+ deceasedBoolean: patient.person.dead,
394
+ deceasedDateTime: patient.person.deathDate,
395
+ name: patient.person?.names?.map((name) => ({
396
+ given: [name.givenName, name.middleName].filter(Boolean),
397
+ family: name.familyName,
398
+ })),
399
+ address: patient.person?.addresses.map((address) => ({
400
+ city: address.cityVillage,
401
+ country: address.country,
402
+ postalCode: address.postalCode,
403
+ state: address.stateProvince,
404
+ use: 'home',
405
+ })),
406
+ telecom: patient.person.attributes?.filter((attribute) => attribute.attributeType === 'Telephone Number'),
407
+ };
408
+ }
409
+ }
410
+
411
+ export class SavePatientTransactionManager {
412
+ patientSaved = false;
413
+ }
@@ -0,0 +1,59 @@
1
+ import React, { useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Layer, TextInput, TextInputProps } from '@carbon/react';
4
+ import { useField } from 'formik';
5
+
6
+ interface InputProps extends TextInputProps {
7
+ checkWarning?(value: string): string;
8
+ }
9
+
10
+ export const Input: React.FC<any> = ({ checkWarning, ...props }) => {
11
+ const [field, meta] = useField(props.name);
12
+ const { t } = useTranslation();
13
+
14
+ /*
15
+ Do not remove these comments
16
+ t('givenNameRequired')
17
+ t('familyNameRequired')
18
+ t('genderUnspecified')
19
+ t('genderRequired')
20
+ t('birthdayRequired')
21
+ t('birthdayNotInTheFuture')
22
+ t('negativeYears')
23
+ t('negativeMonths')
24
+ t('deathdayNotInTheFuture')
25
+ t('invalidEmail')
26
+ t('numberInNameDubious')
27
+ t('yearsEstimateRequired')
28
+ */
29
+
30
+ const value = field.value || '';
31
+ const invalidText = meta.error && t(meta.error);
32
+ const warnText = useMemo(() => {
33
+ if (!invalidText && typeof checkWarning === 'function') {
34
+ const warning = checkWarning(value);
35
+ return warning && t(warning);
36
+ }
37
+
38
+ return undefined;
39
+ }, [checkWarning, invalidText, value, t]);
40
+
41
+ const labelText = props.required ? props.labelText : `${props.labelText} (${t('optional', 'optional')})`;
42
+
43
+ return (
44
+ <div style={{ marginBottom: '1rem' }}>
45
+ <Layer>
46
+ <TextInput
47
+ {...props}
48
+ {...field}
49
+ labelText={labelText}
50
+ invalid={!!(meta.touched && meta.error)}
51
+ invalidText={invalidText}
52
+ warn={!!warnText}
53
+ warnText={warnText}
54
+ value={value}
55
+ />
56
+ </Layer>
57
+ </div>
58
+ );
59
+ };