@ampath/esm-patient-registration-app 6.0.1-pre.6

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 (176) hide show
  1. package/.turbo/turbo-build.log +41 -0
  2. package/README.md +7 -0
  3. package/dist/130.js +2 -0
  4. package/dist/130.js.LICENSE.txt +3 -0
  5. package/dist/130.js.map +1 -0
  6. package/dist/152.js +1 -0
  7. package/dist/152.js.map +1 -0
  8. package/dist/249.js +2 -0
  9. package/dist/249.js.LICENSE.txt +46 -0
  10. package/dist/249.js.map +1 -0
  11. package/dist/255.js +2 -0
  12. package/dist/255.js.LICENSE.txt +9 -0
  13. package/dist/255.js.map +1 -0
  14. package/dist/271.js +1 -0
  15. package/dist/303.js +1 -0
  16. package/dist/303.js.map +1 -0
  17. package/dist/319.js +1 -0
  18. package/dist/365.js +1 -0
  19. package/dist/365.js.map +1 -0
  20. package/dist/460.js +1 -0
  21. package/dist/525.js +1 -0
  22. package/dist/525.js.map +1 -0
  23. package/dist/537.js +1 -0
  24. package/dist/537.js.map +1 -0
  25. package/dist/574.js +1 -0
  26. package/dist/591.js +2 -0
  27. package/dist/591.js.LICENSE.txt +32 -0
  28. package/dist/591.js.map +1 -0
  29. package/dist/621.js +1 -0
  30. package/dist/621.js.map +1 -0
  31. package/dist/644.js +1 -0
  32. package/dist/729.js +1 -0
  33. package/dist/729.js.map +1 -0
  34. package/dist/735.js +1 -0
  35. package/dist/735.js.map +1 -0
  36. package/dist/757.js +1 -0
  37. package/dist/784.js +2 -0
  38. package/dist/784.js.LICENSE.txt +9 -0
  39. package/dist/784.js.map +1 -0
  40. package/dist/788.js +1 -0
  41. package/dist/807.js +1 -0
  42. package/dist/833.js +1 -0
  43. package/dist/879.js +1 -0
  44. package/dist/879.js.map +1 -0
  45. package/dist/ampath-esm-patient-registration-app.js +1 -0
  46. package/dist/ampath-esm-patient-registration-app.js.buildmanifest.json +649 -0
  47. package/dist/ampath-esm-patient-registration-app.js.map +1 -0
  48. package/dist/main.js +2 -0
  49. package/dist/main.js.LICENSE.txt +56 -0
  50. package/dist/main.js.map +1 -0
  51. package/dist/routes.json +1 -0
  52. package/docs/images/patient-registration-hierarchy.png +0 -0
  53. package/jest.config.js +3 -0
  54. package/package.json +61 -0
  55. package/src/add-patient-link.scss +3 -0
  56. package/src/add-patient-link.test.tsx +20 -0
  57. package/src/add-patient-link.tsx +21 -0
  58. package/src/config-schema.ts +410 -0
  59. package/src/constants.ts +14 -0
  60. package/src/declarations.d.ts +6 -0
  61. package/src/index.ts +71 -0
  62. package/src/nav-link.test.tsx +13 -0
  63. package/src/nav-link.tsx +10 -0
  64. package/src/offline.resources.ts +155 -0
  65. package/src/offline.ts +91 -0
  66. package/src/patient-registration/before-save-prompt.tsx +73 -0
  67. package/src/patient-registration/date-util.ts +52 -0
  68. package/src/patient-registration/field/__mocks__/field.resource.ts +60 -0
  69. package/src/patient-registration/field/address/address-field.component.tsx +153 -0
  70. package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +73 -0
  71. package/src/patient-registration/field/address/address-hierarchy.resource.tsx +157 -0
  72. package/src/patient-registration/field/address/address-search.component.tsx +85 -0
  73. package/src/patient-registration/field/address/address-search.scss +53 -0
  74. package/src/patient-registration/field/address/custom-address-field.component.tsx +31 -0
  75. package/src/patient-registration/field/address/tests/address-hierarchy.test.tsx +214 -0
  76. package/src/patient-registration/field/address/tests/address-search-component.test.tsx +135 -0
  77. package/src/patient-registration/field/custom-field.component.tsx +25 -0
  78. package/src/patient-registration/field/dob/dob.component.tsx +159 -0
  79. package/src/patient-registration/field/dob/dob.test.tsx +75 -0
  80. package/src/patient-registration/field/field.component.tsx +47 -0
  81. package/src/patient-registration/field/field.resource.ts +35 -0
  82. package/src/patient-registration/field/field.scss +127 -0
  83. package/src/patient-registration/field/field.test.tsx +294 -0
  84. package/src/patient-registration/field/gender/gender-field.component.tsx +49 -0
  85. package/src/patient-registration/field/gender/gender-field.test.tsx +59 -0
  86. package/src/patient-registration/field/id/id-field.component.tsx +144 -0
  87. package/src/patient-registration/field/id/id-field.test.tsx +107 -0
  88. package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +198 -0
  89. package/src/patient-registration/field/id/identifier-selection.scss +37 -0
  90. package/src/patient-registration/field/name/name-field.component.tsx +142 -0
  91. package/src/patient-registration/field/obs/obs-field.component.tsx +204 -0
  92. package/src/patient-registration/field/obs/obs-field.test.tsx +205 -0
  93. package/src/patient-registration/field/person-attributes/coded-attributes.component.tsx +60 -0
  94. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +116 -0
  95. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +127 -0
  96. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +88 -0
  97. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +187 -0
  98. package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +20 -0
  99. package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +58 -0
  100. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +88 -0
  101. package/src/patient-registration/field/phone/phone-field.component.tsx +16 -0
  102. package/src/patient-registration/form-manager.test.ts +67 -0
  103. package/src/patient-registration/form-manager.ts +414 -0
  104. package/src/patient-registration/input/basic-input/input/input.component.tsx +179 -0
  105. package/src/patient-registration/input/basic-input/input/input.test.tsx +72 -0
  106. package/src/patient-registration/input/basic-input/select/select-input.component.tsx +32 -0
  107. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
  108. package/src/patient-registration/input/combo-input/combo-input.component.tsx +128 -0
  109. package/src/patient-registration/input/combo-input/selection-tick.component.tsx +20 -0
  110. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +187 -0
  111. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +62 -0
  112. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +132 -0
  113. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +156 -0
  114. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +107 -0
  115. package/src/patient-registration/input/custom-input/identifier/utils.test.ts +81 -0
  116. package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
  117. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +53 -0
  118. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +43 -0
  119. package/src/patient-registration/input/input.scss +118 -0
  120. package/src/patient-registration/patient-registration-context.ts +24 -0
  121. package/src/patient-registration/patient-registration-hooks.ts +287 -0
  122. package/src/patient-registration/patient-registration-utils.ts +216 -0
  123. package/src/patient-registration/patient-registration.component.tsx +240 -0
  124. package/src/patient-registration/patient-registration.resource.test.tsx +26 -0
  125. package/src/patient-registration/patient-registration.resource.ts +250 -0
  126. package/src/patient-registration/patient-registration.scss +122 -0
  127. package/src/patient-registration/patient-registration.test.tsx +471 -0
  128. package/src/patient-registration/patient-registration.types.ts +318 -0
  129. package/src/patient-registration/section/death-info/death-info-section.component.tsx +31 -0
  130. package/src/patient-registration/section/death-info/death-info-section.test.tsx +64 -0
  131. package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
  132. package/src/patient-registration/section/demographics/demographics-section.test.tsx +83 -0
  133. package/src/patient-registration/section/generic-section.component.tsx +17 -0
  134. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +235 -0
  135. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +100 -0
  136. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +78 -0
  137. package/src/patient-registration/section/patient-relationships/relationships.scss +35 -0
  138. package/src/patient-registration/section/section-wrapper.component.tsx +40 -0
  139. package/src/patient-registration/section/section.component.tsx +23 -0
  140. package/src/patient-registration/section/section.scss +1 -0
  141. package/src/patient-registration/ui-components/overlay/overlay.component.tsx +51 -0
  142. package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
  143. package/src/patient-registration/validation/patient-registration-validation.test.tsx +157 -0
  144. package/src/patient-registration/validation/patient-registration-validation.tsx +60 -0
  145. package/src/patient-verification/client-registry-constants.ts +13 -0
  146. package/src/patient-verification/client-registry.component.tsx +66 -0
  147. package/src/patient-verification/client-registry.scss +1 -0
  148. package/src/patient-verification/utils.tsx +56 -0
  149. package/src/patient-verification/verification-modal.scss +20 -0
  150. package/src/patient-verification/verification.component.tsx +48 -0
  151. package/src/resource.ts +12 -0
  152. package/src/root.component.tsx +63 -0
  153. package/src/root.scss +7 -0
  154. package/src/root.test.tsx +32 -0
  155. package/src/routes.json +66 -0
  156. package/src/widgets/cancel-patient-edit.component.tsx +37 -0
  157. package/src/widgets/cancel-patient-edit.test.tsx +27 -0
  158. package/src/widgets/delete-identifier-confirmation-modal.test.tsx +34 -0
  159. package/src/widgets/delete-identifier-confirmation-modal.tsx +41 -0
  160. package/src/widgets/delete-identifier-modal.scss +34 -0
  161. package/src/widgets/display-photo.component.tsx +30 -0
  162. package/src/widgets/display-photo.test.tsx +37 -0
  163. package/src/widgets/edit-patient-details-button.component.tsx +34 -0
  164. package/src/widgets/edit-patient-details-button.scss +3 -0
  165. package/src/widgets/edit-patient-details-button.test.tsx +41 -0
  166. package/translations/am.json +97 -0
  167. package/translations/ar.json +97 -0
  168. package/translations/en.json +103 -0
  169. package/translations/es.json +97 -0
  170. package/translations/fr.json +97 -0
  171. package/translations/he.json +97 -0
  172. package/translations/km.json +97 -0
  173. package/translations/zh.json +89 -0
  174. package/translations/zh_CN.json +89 -0
  175. package/tsconfig.json +5 -0
  176. package/webpack.config.js +1 -0
@@ -0,0 +1,414 @@
1
+ import { type FetchResponse, openmrsFetch, queueSynchronizationItem, type Session } from '@openmrs/esm-framework';
2
+ import { patientRegistration } from '../constants';
3
+ import {
4
+ type FormValues,
5
+ type AttributeValue,
6
+ type PatientUuidMapType,
7
+ type Patient,
8
+ type CapturePhotoProps,
9
+ type PatientIdentifier,
10
+ type PatientRegistration,
11
+ type RelationshipValue,
12
+ type 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 { type RegistrationConfig } from '../config-schema';
28
+
29
+ export type SavePatientForm = (
30
+ isNewPatient: boolean,
31
+ values: FormValues,
32
+ patientUuidMap: PatientUuidMapType,
33
+ initialAddressFieldValues: Record<string, any>,
34
+ capturePhotoProps: CapturePhotoProps,
35
+ currentLocation: string,
36
+ initialIdentifierValues: FormValues['identifiers'],
37
+ currentUser: Session,
38
+ config: RegistrationConfig,
39
+ savePatientTransactionManager: SavePatientTransactionManager,
40
+ abortController?: AbortController,
41
+ ) => Promise<string | void>;
42
+
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(isNewPatient, 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
+ isNewPatient,
106
+ values,
107
+ patientUuidMap,
108
+ initialAddressFieldValues,
109
+ patientIdentifiers,
110
+ );
111
+
112
+ FormManager.getDeletedNames(values.patientUuid, patientUuidMap).forEach(async (name) => {
113
+ await deletePersonName(name.nameUuid, name.personUuid);
114
+ });
115
+
116
+ const savePatientResponse = await savePatient(
117
+ createdPatient,
118
+ isNewPatient && !savePatientTransactionManager.patientSaved ? undefined : values.patientUuid,
119
+ );
120
+
121
+ if (savePatientResponse.ok) {
122
+ savePatientTransactionManager.patientSaved = true;
123
+ await this.saveRelationships(values.relationships, savePatientResponse);
124
+
125
+ await this.saveObservations(values.obs, savePatientResponse, currentLocation, currentUser, config);
126
+
127
+ if (config.concepts.patientPhotoUuid && capturePhotoProps?.imageData) {
128
+ await savePatientPhoto(
129
+ savePatientResponse.data.uuid,
130
+ capturePhotoProps.imageData,
131
+ '/ws/rest/v1/obs',
132
+ capturePhotoProps.dateTime || new Date().toISOString(),
133
+ config.concepts.patientPhotoUuid,
134
+ );
135
+ }
136
+ }
137
+
138
+ return savePatientResponse.data.uuid;
139
+ };
140
+
141
+ static async saveRelationships(relationships: Array<RelationshipValue>, savePatientResponse: FetchResponse) {
142
+ return Promise.all(
143
+ relationships
144
+ .filter((m) => m.relationshipType)
145
+ .filter((relationship) => !!relationship.action)
146
+ .map(({ relatedPersonUuid, relationshipType, uuid: relationshipUuid, action }) => {
147
+ const [type, direction] = relationshipType.split('/');
148
+ const thisPatientUuid = savePatientResponse.data.uuid;
149
+ const isAToB = direction === 'aIsToB';
150
+ const relationshipToSave = {
151
+ personA: isAToB ? relatedPersonUuid : thisPatientUuid,
152
+ personB: isAToB ? thisPatientUuid : relatedPersonUuid,
153
+ relationshipType: type,
154
+ };
155
+
156
+ switch (action) {
157
+ case 'ADD':
158
+ return saveRelationship(relationshipToSave);
159
+ case 'UPDATE':
160
+ return updateRelationship(relationshipUuid, relationshipToSave);
161
+ case 'DELETE':
162
+ return deleteRelationship(relationshipUuid);
163
+ }
164
+ }),
165
+ );
166
+ }
167
+
168
+ static async saveObservations(
169
+ obss: { [conceptUuid: string]: string },
170
+ savePatientResponse: FetchResponse,
171
+ currentLocation: string,
172
+ currentUser: Session,
173
+ config: RegistrationConfig,
174
+ ) {
175
+ if (obss && Object.keys(obss).length > 0) {
176
+ if (!config.registrationObs.encounterTypeUuid) {
177
+ console.error(
178
+ 'The registration form has been configured to have obs fields, ' +
179
+ 'but no registration encounter type has been configured. Obs field values ' +
180
+ 'will not be saved.',
181
+ );
182
+ } else {
183
+ const encounterToSave: Encounter = {
184
+ encounterDatetime: new Date(),
185
+ patient: savePatientResponse.data.uuid,
186
+ encounterType: config.registrationObs.encounterTypeUuid,
187
+ location: currentLocation,
188
+ encounterProviders: [
189
+ {
190
+ provider: currentUser.currentProvider.uuid,
191
+ encounterRole: config.registrationObs.encounterProviderRoleUuid,
192
+ },
193
+ ],
194
+ form: config.registrationObs.registrationFormUuid,
195
+ obs: Object.entries(obss)
196
+ .filter(([, value]) => value !== '')
197
+ .map(([conceptUuid, value]) => ({ concept: conceptUuid, value })),
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
+ isNewPatient: boolean,
286
+ values: FormValues,
287
+ patientUuidMap: PatientUuidMapType,
288
+ initialAddressFieldValues: Record<string, any>,
289
+ identifiers: Array<PatientIdentifier>,
290
+ ): Patient {
291
+ let birthdate;
292
+ if (values.birthdate instanceof Date) {
293
+ birthdate = [values.birthdate.getFullYear(), values.birthdate.getMonth() + 1, values.birthdate.getDate()].join(
294
+ '-',
295
+ );
296
+ } else {
297
+ birthdate = values.birthdate;
298
+ }
299
+
300
+ return {
301
+ uuid: values.patientUuid,
302
+ person: {
303
+ uuid: values.patientUuid,
304
+ names: FormManager.getNames(values, patientUuidMap),
305
+ gender: values.gender.charAt(0).toUpperCase(),
306
+ birthdate,
307
+ birthdateEstimated: values.birthdateEstimated,
308
+ attributes: FormManager.getPatientAttributes(isNewPatient, values, patientUuidMap),
309
+ addresses: [values.address],
310
+ ...FormManager.getPatientDeathInfo(values),
311
+ },
312
+ identifiers,
313
+ };
314
+ }
315
+
316
+ static getNames(values: FormValues, patientUuidMap: PatientUuidMapType) {
317
+ const names = [
318
+ {
319
+ uuid: patientUuidMap.preferredNameUuid,
320
+ preferred: true,
321
+ givenName: values.givenName,
322
+ middleName: values.middleName,
323
+ familyName: values.familyName,
324
+ },
325
+ ];
326
+
327
+ if (values.addNameInLocalLanguage) {
328
+ names.push({
329
+ uuid: patientUuidMap.additionalNameUuid,
330
+ preferred: false,
331
+ givenName: values.additionalGivenName,
332
+ middleName: values.additionalMiddleName,
333
+ familyName: values.additionalFamilyName,
334
+ });
335
+ }
336
+
337
+ return names;
338
+ }
339
+
340
+ static getPatientAttributes(isNewPatient: boolean, values: FormValues, patientUuidMap: PatientUuidMapType) {
341
+ const attributes: Array<AttributeValue> = [];
342
+ if (values.attributes) {
343
+ Object.entries(values.attributes)
344
+ .filter(([, value]) => !!value)
345
+ .forEach(([key, value]) => {
346
+ attributes.push({
347
+ attributeType: key,
348
+ value,
349
+ });
350
+ });
351
+
352
+ if (!isNewPatient && values.patientUuid) {
353
+ Object.entries(values.attributes)
354
+ .filter(([, value]) => !value)
355
+ .forEach(async ([key]) => {
356
+ const attributeUuid = patientUuidMap[`attribute.${key}`];
357
+ await openmrsFetch(`/ws/rest/v1/person/${values.patientUuid}/attribute/${attributeUuid}`, {
358
+ method: 'DELETE',
359
+ }).catch((err) => {
360
+ console.error(err);
361
+ });
362
+ });
363
+ }
364
+ }
365
+
366
+ return attributes;
367
+ }
368
+
369
+ static getPatientDeathInfo(values: FormValues) {
370
+ const { isDead, deathDate, deathCause } = values;
371
+ return {
372
+ dead: isDead,
373
+ deathDate: isDead ? deathDate : undefined,
374
+ causeOfDeath: isDead ? deathCause : undefined,
375
+ };
376
+ }
377
+
378
+ static mapPatientToFhirPatient(patient: Partial<Patient>): fhir.Patient {
379
+ // Important:
380
+ // When changing this code, ideally assume that `patient` can be missing any attribute.
381
+ // The `fhir.Patient` provides us with the benefit that all properties are nullable and thus
382
+ // not required (technically, at least). -> Even if we cannot map some props here, we still
383
+ // provide a valid fhir.Patient object. The various patient chart modules should be able to handle
384
+ // such missing props correctly (and should be updated if they don't).
385
+
386
+ // Mapping inspired by:
387
+ // https://github.com/openmrs/openmrs-module-fhir/blob/669b3c52220bb9abc622f815f4dc0d8523687a57/api/src/main/java/org/openmrs/module/fhir/api/util/FHIRPatientUtil.java#L36
388
+ // 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
389
+ // Update as required.
390
+ return {
391
+ id: patient.uuid,
392
+ gender: patient.person?.gender,
393
+ birthDate: patient.person?.birthdate,
394
+ deceasedBoolean: patient.person.dead,
395
+ deceasedDateTime: patient.person.deathDate,
396
+ name: patient.person?.names?.map((name) => ({
397
+ given: [name.givenName, name.middleName].filter(Boolean),
398
+ family: name.familyName,
399
+ })),
400
+ address: patient.person?.addresses.map((address) => ({
401
+ city: address.cityVillage,
402
+ country: address.country,
403
+ postalCode: address.postalCode,
404
+ state: address.stateProvince,
405
+ use: 'home',
406
+ })),
407
+ telecom: patient.person.attributes?.filter((attribute) => attribute.attributeType === 'Telephone Number'),
408
+ };
409
+ }
410
+ }
411
+
412
+ export class SavePatientTransactionManager {
413
+ patientSaved = false;
414
+ }
@@ -0,0 +1,179 @@
1
+ import React, { useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Layer, TextInput } from '@carbon/react';
4
+ import { useField } from 'formik';
5
+
6
+ // FIXME Temporarily imported here
7
+ export interface TextInputProps
8
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'defaultValue' | 'id' | 'size' | 'value'> {
9
+ /**
10
+ * Specify an optional className to be applied to the `<input>` node
11
+ */
12
+ className?: string;
13
+
14
+ /**
15
+ * Optionally provide the default value of the `<input>`
16
+ */
17
+ defaultValue?: string | number;
18
+
19
+ /**
20
+ * Specify whether the `<input>` should be disabled
21
+ */
22
+ disabled?: boolean;
23
+
24
+ /**
25
+ * Specify whether to display the character counter
26
+ */
27
+ enableCounter?: boolean;
28
+
29
+ /**
30
+ * Provide text that is used alongside the control label for additional help
31
+ */
32
+ helperText?: React.ReactNode;
33
+
34
+ /**
35
+ * Specify whether you want the underlying label to be visually hidden
36
+ */
37
+ hideLabel?: boolean;
38
+
39
+ /**
40
+ * Specify a custom `id` for the `<input>`
41
+ */
42
+ id: string;
43
+
44
+ /**
45
+ * `true` to use the inline version.
46
+ */
47
+ inline?: boolean;
48
+
49
+ /**
50
+ * Specify whether the control is currently invalid
51
+ */
52
+ invalid?: boolean;
53
+
54
+ /**
55
+ * Provide the text that is displayed when the control is in an invalid state
56
+ */
57
+ invalidText?: React.ReactNode;
58
+
59
+ /**
60
+ * Provide the text that will be read by a screen reader when visiting this
61
+ * control
62
+ */
63
+ labelText: React.ReactNode;
64
+
65
+ /**
66
+ * `true` to use the light version. For use on $ui-01 backgrounds only.
67
+ * Don't use this to make tile background color same as container background color.
68
+ * 'The `light` prop for `TextInput` has ' +
69
+ 'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.'
70
+ */
71
+ light?: boolean;
72
+
73
+ /**
74
+ * Max character count allowed for the input. This is needed in order for enableCounter to display
75
+ */
76
+ maxCount?: number;
77
+
78
+ /**
79
+ * Optionally provide an `onChange` handler that is called whenever `<input>`
80
+ * is updated
81
+ */
82
+ onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
83
+
84
+ /**
85
+ * Optionally provide an `onClick` handler that is called whenever the
86
+ * `<input>` is clicked
87
+ */
88
+ onClick?: (event: React.MouseEvent<HTMLElement>) => void;
89
+
90
+ /**
91
+ * Specify the placeholder attribute for the `<input>`
92
+ */
93
+ placeholder?: string;
94
+
95
+ /**
96
+ * Whether the input should be read-only
97
+ */
98
+ readOnly?: boolean;
99
+
100
+ /**
101
+ * Specify the size of the Text Input. Currently supports the following:
102
+ */
103
+ size?: 'sm' | 'md' | 'lg' | 'xl';
104
+
105
+ /**
106
+ * Specify the type of the `<input>`
107
+ */
108
+ type?: string;
109
+
110
+ /**
111
+ * Specify the value of the `<input>`
112
+ */
113
+ value?: string | number | undefined;
114
+
115
+ /**
116
+ * Specify whether the control is currently in warning state
117
+ */
118
+ warn?: boolean;
119
+
120
+ /**
121
+ * Provide the text that is displayed when the control is in warning state
122
+ */
123
+ warnText?: React.ReactNode;
124
+ }
125
+
126
+ interface InputProps extends TextInputProps {
127
+ checkWarning?(value: string): string;
128
+ }
129
+
130
+ export const Input: React.FC<InputProps> = ({ checkWarning, ...props }) => {
131
+ const [field, meta] = useField(props.name);
132
+ const { t } = useTranslation();
133
+
134
+ /*
135
+ Do not remove these comments
136
+ t('givenNameRequired')
137
+ t('familyNameRequired')
138
+ t('genderUnspecified')
139
+ t('genderRequired')
140
+ t('birthdayRequired')
141
+ t('birthdayNotInTheFuture')
142
+ t('negativeYears')
143
+ t('negativeMonths')
144
+ t('deathdayNotInTheFuture')
145
+ t('invalidEmail')
146
+ t('numberInNameDubious')
147
+ t('yearsEstimateRequired')
148
+ */
149
+
150
+ const value = field.value || '';
151
+ const invalidText = meta.error && t(meta.error);
152
+ const warnText = useMemo(() => {
153
+ if (!invalidText && typeof checkWarning === 'function') {
154
+ const warning = checkWarning(value);
155
+ return warning && t(warning);
156
+ }
157
+
158
+ return undefined;
159
+ }, [checkWarning, invalidText, value, t]);
160
+
161
+ const labelText = props.required ? props.labelText : `${props.labelText} (${t('optional', 'optional')})`;
162
+
163
+ return (
164
+ <div style={{ marginBottom: '1rem' }}>
165
+ <Layer>
166
+ <TextInput
167
+ {...props}
168
+ {...field}
169
+ labelText={labelText}
170
+ invalid={!!(meta.touched && meta.error)}
171
+ invalidText={invalidText}
172
+ warn={!!warnText}
173
+ warnText={warnText}
174
+ value={value}
175
+ />
176
+ </Layer>
177
+ </div>
178
+ );
179
+ };
@@ -0,0 +1,72 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { Formik, Form } from 'formik';
5
+ import { Input } from './input.component';
6
+
7
+ describe('text input', () => {
8
+ const setupInput = async () => {
9
+ render(
10
+ <Formik initialValues={{ text: '' }} onSubmit={() => {}}>
11
+ <Form>
12
+ <Input
13
+ id="text"
14
+ labelText="Text"
15
+ name="text"
16
+ placeholder="Enter text"
17
+ required
18
+ checkWarning={(value) => {
19
+ if (value.length > 5) {
20
+ return 'name should be of 5 char';
21
+ }
22
+ }}
23
+ />
24
+ </Form>
25
+ </Formik>,
26
+ );
27
+ return screen.getByLabelText('Text') as HTMLInputElement;
28
+ };
29
+
30
+ it('exists', async () => {
31
+ const input = await setupInput();
32
+ expect(input.type).toEqual('text');
33
+ });
34
+
35
+ it('can input valid data without warning', async () => {
36
+ const user = userEvent.setup();
37
+
38
+ const input = await setupInput();
39
+ const userInput = 'text';
40
+
41
+ await user.type(input, userInput);
42
+ await user.tab();
43
+
44
+ expect(input.value).toEqual(userInput);
45
+ expect(screen.queryByText('name should be of 5 char')).not.toBeInTheDocument();
46
+ });
47
+
48
+ it('should show a warning when the invalid input is entered', async () => {
49
+ const user = userEvent.setup();
50
+
51
+ const input = await setupInput();
52
+ const userInput = 'Hello World';
53
+
54
+ await userEvent.clear(input);
55
+
56
+ await user.type(input, userInput);
57
+ await user.tab();
58
+
59
+ expect(screen.getByText('name should be of 5 char')).toBeInTheDocument();
60
+ });
61
+
62
+ it('should show the correct label text if the field is not required', () => {
63
+ render(
64
+ <Formik initialValues={{ text: '' }} onSubmit={() => {}}>
65
+ <Form>
66
+ <Input id="text" labelText="Text" name="text" placeholder="Enter text" />
67
+ </Form>
68
+ </Formik>,
69
+ );
70
+ expect(screen.getByLabelText('Text (optional)')).toBeInTheDocument();
71
+ });
72
+ });
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { Layer, Select, SelectItem } from '@carbon/react';
3
+ import { useField } from 'formik';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ interface SelectInputProps {
7
+ name: string;
8
+ options: Array<string>;
9
+ label: string;
10
+ required?: boolean;
11
+ }
12
+
13
+ export const SelectInput: React.FC<SelectInputProps> = ({ name, options, label, required }) => {
14
+ const [field] = useField(name);
15
+ const { t } = useTranslation();
16
+ const selectOptions = [
17
+ <SelectItem disabled hidden text={`Select ${label}`} key="" value="" />,
18
+ ...options.map((currentOption, index) => <SelectItem text={currentOption} value={currentOption} key={index} />),
19
+ ];
20
+
21
+ const labelText = required ? label : `${label} (${t('optional', 'optional')})`;
22
+
23
+ return (
24
+ <div style={{ marginBottom: '1rem' }}>
25
+ <Layer>
26
+ <Select id="identifier" {...field} labelText={labelText}>
27
+ {selectOptions}
28
+ </Select>
29
+ </Layer>
30
+ </div>
31
+ );
32
+ };