@olaboot/esm-patient-registration-app 9.2.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 (250) hide show
  1. package/ADDRESS_CONFIGURATION.md +152 -0
  2. package/IDENTIFIER_CONFIGURATION.md +142 -0
  3. package/IMPLEMENTATION_SUMMARY.md +111 -0
  4. package/QUICK_START.md +95 -0
  5. package/README.md +7 -0
  6. package/address-required-fields-config.json +26 -0
  7. package/dist/126.js +1 -0
  8. package/dist/15.js +1 -0
  9. package/dist/1564.js +1 -0
  10. package/dist/1567.js +1 -0
  11. package/dist/1845.js +1 -0
  12. package/dist/1953.js +1 -0
  13. package/dist/200.js +1 -0
  14. package/dist/200.js.map +1 -0
  15. package/dist/215.js +1 -0
  16. package/dist/2178.js +1 -0
  17. package/dist/250.js +1 -0
  18. package/dist/250.js.map +1 -0
  19. package/dist/2523.js +1 -0
  20. package/dist/2523.js.map +1 -0
  21. package/dist/2566.js +1 -0
  22. package/dist/2586.js +1 -0
  23. package/dist/2586.js.map +1 -0
  24. package/dist/2716.js +1 -0
  25. package/dist/2716.js.map +1 -0
  26. package/dist/2759.js +1 -0
  27. package/dist/2821.js +6 -0
  28. package/dist/2821.js.map +1 -0
  29. package/dist/3089.js +1 -0
  30. package/dist/3089.js.map +1 -0
  31. package/dist/3230.js +1 -0
  32. package/dist/3441.js +1 -0
  33. package/dist/3565.js +1 -0
  34. package/dist/3571.js +1 -0
  35. package/dist/3571.js.map +1 -0
  36. package/dist/3746.js +1 -0
  37. package/dist/3925.js +1 -0
  38. package/dist/3946.js +1 -0
  39. package/dist/4024.js +1 -0
  40. package/dist/4024.js.map +1 -0
  41. package/dist/4744.js +1 -0
  42. package/dist/4744.js.map +1 -0
  43. package/dist/4809.js +1 -0
  44. package/dist/4894.js +1 -0
  45. package/dist/4970.js +1 -0
  46. package/dist/4970.js.map +1 -0
  47. package/dist/5130.js +1 -0
  48. package/dist/5187.js +1 -0
  49. package/dist/5491.js +1 -0
  50. package/dist/5491.js.map +1 -0
  51. package/dist/5595.js +1 -0
  52. package/dist/5961.js +1 -0
  53. package/dist/6133.js +1 -0
  54. package/dist/634.js +1 -0
  55. package/dist/634.js.map +1 -0
  56. package/dist/6456.js +1 -0
  57. package/dist/6466.js +1 -0
  58. package/dist/6613.js +1 -0
  59. package/dist/6783.js +1 -0
  60. package/dist/7073.js +38 -0
  61. package/dist/7073.js.map +1 -0
  62. package/dist/7154.js +1 -0
  63. package/dist/7154.js.map +1 -0
  64. package/dist/7348.js +1 -0
  65. package/dist/7439.js +1 -0
  66. package/dist/7439.js.map +1 -0
  67. package/dist/7543.js +1 -0
  68. package/dist/7607.js +1 -0
  69. package/dist/772.js +1 -0
  70. package/dist/7984.js +1 -0
  71. package/dist/7984.js.map +1 -0
  72. package/dist/8538.js +1 -0
  73. package/dist/8538.js.map +1 -0
  74. package/dist/8599.js +1 -0
  75. package/dist/8727.js +1 -0
  76. package/dist/8847.js +1 -0
  77. package/dist/9015.js +1 -0
  78. package/dist/906.js +1 -0
  79. package/dist/9065.js +1 -0
  80. package/dist/9182.js +1 -0
  81. package/dist/9339.js +1 -0
  82. package/dist/9453.js +1 -0
  83. package/dist/9833.js +1 -0
  84. package/dist/9833.js.map +1 -0
  85. package/dist/9856.js +1 -0
  86. package/dist/9856.js.map +1 -0
  87. package/dist/9920.js +1 -0
  88. package/dist/9938.js +1 -0
  89. package/dist/9943.js +1 -0
  90. package/dist/9943.js.map +1 -0
  91. package/dist/main.js +10 -0
  92. package/dist/main.js.map +1 -0
  93. package/dist/olaboot-esm-patient-registration-app.js +5 -0
  94. package/dist/olaboot-esm-patient-registration-app.js.buildmanifest.json +1627 -0
  95. package/dist/olaboot-esm-patient-registration-app.js.map +1 -0
  96. package/dist/routes.json +1 -0
  97. package/docs/images/patient-registration-hierarchy.png +0 -0
  98. package/example-config.json +14 -0
  99. package/jest.config.js +3 -0
  100. package/package.json +60 -0
  101. package/rspack.config.js +1 -0
  102. package/src/add-patient-link.extension.tsx +21 -0
  103. package/src/add-patient-link.scss +3 -0
  104. package/src/add-patient-link.test.tsx +16 -0
  105. package/src/config-schema.ts +507 -0
  106. package/src/constants.ts +14 -0
  107. package/src/declarations.d.ts +4 -0
  108. package/src/index.ts +59 -0
  109. package/src/nav-link.test.tsx +13 -0
  110. package/src/nav-link.tsx +10 -0
  111. package/src/offline.resources.ts +157 -0
  112. package/src/offline.ts +93 -0
  113. package/src/patient-photo.extension.tsx +11 -0
  114. package/src/patient-registration/before-save-prompt.component.tsx +72 -0
  115. package/src/patient-registration/field/__mocks__/field.resource.ts +60 -0
  116. package/src/patient-registration/field/address/address-field.component.tsx +186 -0
  117. package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +71 -0
  118. package/src/patient-registration/field/address/address-hierarchy.resource.tsx +157 -0
  119. package/src/patient-registration/field/address/address-hierarchy.test.tsx +296 -0
  120. package/src/patient-registration/field/address/address-search.component.tsx +87 -0
  121. package/src/patient-registration/field/address/address-search.scss +53 -0
  122. package/src/patient-registration/field/address/address-search.test.tsx +141 -0
  123. package/src/patient-registration/field/address/custom-address-field.component.tsx +32 -0
  124. package/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx +98 -0
  125. package/src/patient-registration/field/custom-field.component.tsx +25 -0
  126. package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx +79 -0
  127. package/src/patient-registration/field/dob/dob.component.tsx +167 -0
  128. package/src/patient-registration/field/dob/dob.test.tsx +90 -0
  129. package/src/patient-registration/field/field.component.tsx +53 -0
  130. package/src/patient-registration/field/field.resource.ts +42 -0
  131. package/src/patient-registration/field/field.scss +171 -0
  132. package/src/patient-registration/field/field.test.tsx +330 -0
  133. package/src/patient-registration/field/gender/gender-field.component.tsx +54 -0
  134. package/src/patient-registration/field/gender/gender-field.test.tsx +99 -0
  135. package/src/patient-registration/field/id/id-field.component.tsx +136 -0
  136. package/src/patient-registration/field/id/id-field.test.tsx +121 -0
  137. package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +200 -0
  138. package/src/patient-registration/field/id/identifier-selection.scss +41 -0
  139. package/src/patient-registration/field/name/name-field.component.tsx +148 -0
  140. package/src/patient-registration/field/obs/obs-field.component.tsx +261 -0
  141. package/src/patient-registration/field/obs/obs-field.test.tsx +299 -0
  142. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +120 -0
  143. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +141 -0
  144. package/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx +105 -0
  145. package/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx +48 -0
  146. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +100 -0
  147. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +193 -0
  148. package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +20 -0
  149. package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +58 -0
  150. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +90 -0
  151. package/src/patient-registration/field/phone/phone-field.component.tsx +17 -0
  152. package/src/patient-registration/form-manager.test.ts +91 -0
  153. package/src/patient-registration/form-manager.ts +443 -0
  154. package/src/patient-registration/input/basic-input/input/input.component.tsx +183 -0
  155. package/src/patient-registration/input/basic-input/input/input.test.tsx +72 -0
  156. package/src/patient-registration/input/basic-input/select/select-input.component.tsx +32 -0
  157. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
  158. package/src/patient-registration/input/combo-input/combo-input.component.tsx +130 -0
  159. package/src/patient-registration/input/combo-input/selection-tick.component.tsx +20 -0
  160. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +187 -0
  161. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +62 -0
  162. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +164 -0
  163. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +193 -0
  164. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +335 -0
  165. package/src/patient-registration/input/custom-input/identifier/utils.test.ts +81 -0
  166. package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
  167. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +56 -0
  168. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +34 -0
  169. package/src/patient-registration/input/input.scss +122 -0
  170. package/src/patient-registration/patient-registration-context.ts +35 -0
  171. package/src/patient-registration/patient-registration-hooks.ts +376 -0
  172. package/src/patient-registration/patient-registration-utils.test.ts +33 -0
  173. package/src/patient-registration/patient-registration-utils.ts +214 -0
  174. package/src/patient-registration/patient-registration.component.tsx +266 -0
  175. package/src/patient-registration/patient-registration.resource.test.tsx +22 -0
  176. package/src/patient-registration/patient-registration.resource.ts +198 -0
  177. package/src/patient-registration/patient-registration.scss +103 -0
  178. package/src/patient-registration/patient-registration.test.tsx +580 -0
  179. package/src/patient-registration/patient-registration.types.ts +322 -0
  180. package/src/patient-registration/section/death-info/death-info-section.component.tsx +36 -0
  181. package/src/patient-registration/section/death-info/death-info-section.test.tsx +47 -0
  182. package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
  183. package/src/patient-registration/section/demographics/demographics-section.test.tsx +98 -0
  184. package/src/patient-registration/section/generic-section.component.tsx +17 -0
  185. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +234 -0
  186. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +113 -0
  187. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +78 -0
  188. package/src/patient-registration/section/patient-relationships/relationships.scss +35 -0
  189. package/src/patient-registration/section/section-wrapper.component.tsx +40 -0
  190. package/src/patient-registration/section/section.component.tsx +23 -0
  191. package/src/patient-registration/section/section.scss +21 -0
  192. package/src/patient-registration/ui-components/overlay/overlay.component.tsx +51 -0
  193. package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
  194. package/src/patient-registration/validation/patient-registration-validation.test.ts +205 -0
  195. package/src/patient-registration/validation/patient-registration-validation.ts +123 -0
  196. package/src/resource.ts +12 -0
  197. package/src/resources-context.ts +14 -0
  198. package/src/root.component.tsx +63 -0
  199. package/src/root.scss +7 -0
  200. package/src/routes.json +61 -0
  201. package/src/widgets/cancel-patient-edit.modal.tsx +33 -0
  202. package/src/widgets/cancel-patient-edit.test.tsx +22 -0
  203. package/src/widgets/delete-identifier-confirmation.modal.tsx +48 -0
  204. package/src/widgets/delete-identifier-confirmation.test.tsx +32 -0
  205. package/src/widgets/edit-patient-details-button.component.tsx +33 -0
  206. package/src/widgets/edit-patient-details-button.scss +3 -0
  207. package/src/widgets/edit-patient-details-button.test.tsx +35 -0
  208. package/translations/am.json +120 -0
  209. package/translations/ar.json +120 -0
  210. package/translations/ar_SY.json +120 -0
  211. package/translations/bn.json +120 -0
  212. package/translations/cs.json +120 -0
  213. package/translations/de.json +120 -0
  214. package/translations/en.json +120 -0
  215. package/translations/en_US.json +120 -0
  216. package/translations/es.json +120 -0
  217. package/translations/es_MX.json +120 -0
  218. package/translations/fr.json +120 -0
  219. package/translations/he.json +120 -0
  220. package/translations/hi.json +120 -0
  221. package/translations/hi_IN.json +120 -0
  222. package/translations/id.json +120 -0
  223. package/translations/it.json +120 -0
  224. package/translations/ka.json +120 -0
  225. package/translations/km.json +120 -0
  226. package/translations/ku.json +120 -0
  227. package/translations/ky.json +120 -0
  228. package/translations/lg.json +120 -0
  229. package/translations/ne.json +120 -0
  230. package/translations/pl.json +120 -0
  231. package/translations/pt.json +120 -0
  232. package/translations/pt_BR.json +120 -0
  233. package/translations/qu.json +120 -0
  234. package/translations/ro_RO.json +120 -0
  235. package/translations/ru_RU.json +120 -0
  236. package/translations/si.json +120 -0
  237. package/translations/sq.json +120 -0
  238. package/translations/sw.json +120 -0
  239. package/translations/sw_KE.json +120 -0
  240. package/translations/tr.json +120 -0
  241. package/translations/tr_TR.json +120 -0
  242. package/translations/uk.json +120 -0
  243. package/translations/uz.json +120 -0
  244. package/translations/uz@Latn.json +120 -0
  245. package/translations/uz_UZ.json +120 -0
  246. package/translations/vi.json +120 -0
  247. package/translations/zh.json +120 -0
  248. package/translations/zh_CN.json +120 -0
  249. package/translations/zh_TW.json +120 -0
  250. package/tsconfig.json +5 -0
@@ -0,0 +1,214 @@
1
+ import * as Yup from 'yup';
2
+ import camelCase from 'lodash-es/camelCase';
3
+ import { parseDate } from '@openmrs/esm-framework';
4
+ import {
5
+ type AddressValidationSchemaType,
6
+ type Encounter,
7
+ type FormValues,
8
+ type PatientIdentifier,
9
+ type PatientIdentifierValue,
10
+ type PatientUuidMapType,
11
+ } from './patient-registration.types';
12
+
13
+ export function parseAddressTemplateXml(addressTemplate: string) {
14
+ const templateXmlDoc = new DOMParser().parseFromString(addressTemplate, 'text/xml');
15
+ const nameMappings = templateXmlDoc.querySelector('nameMappings');
16
+ const properties = nameMappings.getElementsByTagName('entry');
17
+ const validationSchemaObjs = Array.prototype.map.call(properties, (property: Element) => {
18
+ const name = property.getElementsByTagName('string')[0].innerHTML;
19
+ const label = property.getElementsByTagName('string')[1].innerHTML;
20
+ const regex = findElementValueInXmlDoc(name, 'elementRegex', templateXmlDoc) || '.*';
21
+ const regexFormat = findElementValueInXmlDoc(name, 'elementRegexFormats', templateXmlDoc) || '';
22
+
23
+ return {
24
+ name,
25
+ label,
26
+ regex,
27
+ regexFormat,
28
+ };
29
+ });
30
+
31
+ const addressValidationSchema = Yup.object(
32
+ validationSchemaObjs.reduce((final, current) => {
33
+ final[current.name] = Yup.string().matches(current.regex, current.regexFormat);
34
+ return final;
35
+ }, {}),
36
+ );
37
+
38
+ const addressFieldValues = Array.prototype.map.call(properties, (property: Element) => {
39
+ const name = property.getElementsByTagName('string')[0].innerHTML;
40
+ return {
41
+ name,
42
+ defaultValue: '',
43
+ };
44
+ });
45
+ return {
46
+ addressFieldValues,
47
+ addressValidationSchema,
48
+ };
49
+ }
50
+
51
+ export function parseAddressTemplateXmlOld(addressTemplate: string) {
52
+ const templateXmlDoc = new DOMParser().parseFromString(addressTemplate, 'text/xml');
53
+ const nameMappings = templateXmlDoc.querySelector('nameMappings').querySelectorAll('property');
54
+ const validationSchemaObjs: AddressValidationSchemaType[] = Array.prototype.map.call(
55
+ nameMappings,
56
+ (nameMapping: Element) => {
57
+ const name = nameMapping.getAttribute('name');
58
+ const label = nameMapping.getAttribute('value');
59
+ const regex = findElementValueInXmlDoc(name, 'elementRegex', templateXmlDoc) || '.*';
60
+ const regexFormat = findElementValueInXmlDoc(name, 'elementRegexFormats', templateXmlDoc) || '';
61
+
62
+ return {
63
+ name,
64
+ label,
65
+ regex,
66
+ regexFormat,
67
+ };
68
+ },
69
+ );
70
+
71
+ const addressValidationSchema = Yup.object(
72
+ validationSchemaObjs.reduce((final, current) => {
73
+ final[current.name] = Yup.string().matches(current.regex, current.regexFormat);
74
+ return final;
75
+ }, {}),
76
+ );
77
+
78
+ const addressFieldValues: Array<{ name: string; defaultValue: string }> = Array.prototype.map.call(
79
+ nameMappings,
80
+ (nameMapping: Element) => {
81
+ const name = nameMapping.getAttribute('name');
82
+ const defaultValue = findElementValueInXmlDoc(name, 'elementDefaults', templateXmlDoc) ?? '';
83
+ return { name, defaultValue };
84
+ },
85
+ );
86
+
87
+ return {
88
+ addressFieldValues,
89
+ addressValidationSchema,
90
+ };
91
+ }
92
+
93
+ function findElementValueInXmlDoc(fieldName: string, elementSelector: string, doc: XMLDocument) {
94
+ return doc.querySelector(elementSelector)?.querySelector(`[name=${fieldName}]`)?.getAttribute('value') ?? null;
95
+ }
96
+
97
+ export function scrollIntoView(viewId: string) {
98
+ document.getElementById(viewId).scrollIntoView({
99
+ behavior: 'smooth',
100
+ block: 'center',
101
+ inline: 'center',
102
+ });
103
+ }
104
+
105
+ export function cancelRegistration() {
106
+ window.history.back();
107
+ }
108
+
109
+ export function getFormValuesFromFhirPatient(patient: fhir.Patient) {
110
+ const result = {} as FormValues;
111
+ const patientName = patient.name[0];
112
+ const additionalPatientName = patient.name[1];
113
+
114
+ result.patientUuid = patient.id;
115
+ result.givenName = patientName?.given[0];
116
+ result.middleName = patientName?.given[1];
117
+ result.familyName = patientName?.family;
118
+ result.addNameInLocalLanguage = !!additionalPatientName ? true : undefined;
119
+ result.additionalGivenName = additionalPatientName?.given[0];
120
+ result.additionalMiddleName = additionalPatientName?.given[1];
121
+ result.additionalFamilyName = additionalPatientName?.family;
122
+
123
+ result.gender = patient.gender;
124
+ result.birthdate = patient.birthDate ? parseDate(patient.birthDate) : undefined;
125
+ result.telephoneNumber = patient.telecom ? patient.telecom[0].value : '';
126
+
127
+ return {
128
+ ...result,
129
+ ...patient.identifier.map((identifier) => {
130
+ const key = camelCase(identifier.system || identifier.type.text);
131
+ return { [key]: identifier.value };
132
+ }),
133
+ };
134
+ }
135
+
136
+ export function getAddressFieldValuesFromFhirPatient(patient: fhir.Patient) {
137
+ const result = {};
138
+ const address = patient.address?.[0];
139
+
140
+ if (address) {
141
+ for (const key of Object.keys(address)) {
142
+ switch (key) {
143
+ case 'city':
144
+ result['cityVillage'] = address[key];
145
+ break;
146
+ case 'state':
147
+ result['stateProvince'] = address[key];
148
+ break;
149
+ case 'district':
150
+ result['countyDistrict'] = address[key];
151
+ break;
152
+ case 'extension':
153
+ address[key].forEach((ext) => {
154
+ ext.extension.forEach((extension) => {
155
+ result[extension.url.split('#')[1]] = extension.valueString;
156
+ });
157
+ });
158
+ break;
159
+ default:
160
+ if (key === 'country' || key === 'postalCode') {
161
+ result[key] = address[key];
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ return result;
168
+ }
169
+
170
+ export function getPatientUuidMapFromFhirPatient(patient: fhir.Patient): PatientUuidMapType {
171
+ const patientName = patient.name[0];
172
+ const additionalPatientName = patient.name[1];
173
+ const address = patient.address?.[0];
174
+
175
+ return {
176
+ preferredNameUuid: patientName?.id,
177
+ additionalNameUuid: additionalPatientName?.id,
178
+ preferredAddressUuid: address?.id,
179
+ ...patient.identifier.map((identifier) => {
180
+ const key = camelCase(identifier.system || identifier.type.text);
181
+ return { [key]: { uuid: identifier.id, value: identifier.value } };
182
+ }),
183
+ };
184
+ }
185
+
186
+ export function getPatientIdentifiersFromFhirPatient(patient: fhir.Patient): Array<PatientIdentifier> {
187
+ return patient.identifier.map((identifier) => {
188
+ return {
189
+ uuid: identifier.id,
190
+ identifier: identifier.value,
191
+ };
192
+ });
193
+ }
194
+
195
+ export function getPhonePersonAttributeValueFromFhirPatient(patient: fhir.Patient) {
196
+ const result = {};
197
+ if (patient.telecom) {
198
+ result['phone'] = patient.telecom[0].value;
199
+ }
200
+ return result;
201
+ }
202
+
203
+ type IdentifierMap = { [identifierFieldName: string]: PatientIdentifierValue };
204
+ export const filterOutUndefinedPatientIdentifiers = (patientIdentifiers: IdentifierMap): IdentifierMap =>
205
+ Object.fromEntries(
206
+ Object.entries(patientIdentifiers).filter(
207
+ ([key, value]) =>
208
+ (value.autoGeneration && value.selectedSource.autoGenerationOption.manualEntryEnabled) ||
209
+ value.identifierValue !== undefined,
210
+ ),
211
+ );
212
+
213
+ export const latestFirstEncounter = (a: Encounter, b: Encounter) =>
214
+ new Date(b.encounterDatetime).getTime() - new Date(a.encounterDatetime).getTime();
@@ -0,0 +1,266 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { Button, InlineLoading, Link } from '@carbon/react';
4
+ import { XAxis } from '@carbon/react/icons';
5
+ import { useLocation, useParams } from 'react-router-dom';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Form, Formik, type FormikHelpers } from 'formik';
8
+ import {
9
+ createErrorHandler,
10
+ interpolateUrl,
11
+ showSnackbar,
12
+ useConfig,
13
+ usePatient,
14
+ usePatientPhoto,
15
+ } from '@openmrs/esm-framework';
16
+ import { builtInSections, type RegistrationConfig, type SectionDefinition } from '../config-schema';
17
+ import { cancelRegistration, filterOutUndefinedPatientIdentifiers, scrollIntoView } from './patient-registration-utils';
18
+ import { getValidationSchema } from './validation/patient-registration-validation';
19
+ import { DummyDataInput } from './input/dummy-data/dummy-data-input.component';
20
+ import { PatientRegistrationContextProvider } from './patient-registration-context';
21
+ import { useResourcesContext } from '../resources-context';
22
+ import { SectionWrapper } from './section/section-wrapper.component';
23
+ import { type CapturePhotoProps, type FormValues } from './patient-registration.types';
24
+ import { type SavePatientForm, SavePatientTransactionManager } from './form-manager';
25
+ import { useInitialAddressFieldValues, useInitialFormValues, usePatientUuidMap } from './patient-registration-hooks';
26
+ import BeforeSavePrompt from './before-save-prompt.component';
27
+ import styles from './patient-registration.scss';
28
+
29
+ let exportedInitialFormValuesForTesting = {} as FormValues;
30
+
31
+ export interface PatientRegistrationProps {
32
+ savePatientForm: SavePatientForm;
33
+ isOffline: boolean;
34
+ }
35
+
36
+ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePatientForm, isOffline }) => {
37
+ const { t } = useTranslation();
38
+ const { currentSession, identifierTypes } = useResourcesContext();
39
+ const { patientUuid: uuidOfPatientToEdit } = useParams();
40
+ const { search } = useLocation();
41
+ const { isLoading: isLoadingPatientToEdit, patient: patientToEdit } = usePatient(uuidOfPatientToEdit);
42
+ const config = useConfig<RegistrationConfig>();
43
+
44
+ const [initialFormValues, setInitialFormValues] = useInitialFormValues(
45
+ isLoadingPatientToEdit,
46
+ patientToEdit,
47
+ uuidOfPatientToEdit,
48
+ );
49
+ const [initialAddressFieldValues] = useInitialAddressFieldValues(
50
+ {},
51
+ isLoadingPatientToEdit,
52
+ patientToEdit,
53
+ uuidOfPatientToEdit,
54
+ );
55
+
56
+ const [patientUuidMap] = usePatientUuidMap({}, isLoadingPatientToEdit, patientToEdit, uuidOfPatientToEdit);
57
+
58
+ const [target, setTarget] = useState<undefined | string>();
59
+ const [capturePhotoProps, setCapturePhotoProps] = useState<CapturePhotoProps | null>(null);
60
+
61
+ const location = currentSession?.sessionLocation?.uuid;
62
+ const inEditMode = isLoadingPatientToEdit ? undefined : !!(uuidOfPatientToEdit && patientToEdit);
63
+ const showDummyData = useMemo(() => localStorage.getItem('openmrs:devtools') === 'true' && !inEditMode, [inEditMode]);
64
+ const { data: photo } = usePatientPhoto(patientToEdit?.id);
65
+ const savePatientTransactionManager = useRef(new SavePatientTransactionManager());
66
+ const validationSchema = getValidationSchema(config, t);
67
+
68
+ useEffect(() => {
69
+ exportedInitialFormValuesForTesting = initialFormValues;
70
+ }, [initialFormValues]);
71
+
72
+ const sections: Array<SectionDefinition> = useMemo(() => {
73
+ return config.sections
74
+ .map(
75
+ (sectionName) =>
76
+ config.sectionDefinitions.filter((s) => s.id == sectionName)[0] ??
77
+ builtInSections.filter((s) => s.id == sectionName)[0],
78
+ )
79
+ .filter((s) => s);
80
+ }, [config.sections, config.sectionDefinitions]);
81
+
82
+ const onFormSubmit = async (values: FormValues, helpers: FormikHelpers<FormValues>) => {
83
+ const abortController = new AbortController();
84
+ helpers.setSubmitting(true);
85
+
86
+ const updatedFormValues = { ...values, identifiers: filterOutUndefinedPatientIdentifiers(values.identifiers) };
87
+ try {
88
+ await savePatientForm(
89
+ !inEditMode,
90
+ updatedFormValues,
91
+ patientUuidMap,
92
+ initialAddressFieldValues,
93
+ capturePhotoProps,
94
+ location,
95
+ initialFormValues['identifiers'],
96
+ currentSession,
97
+ config,
98
+ savePatientTransactionManager.current,
99
+ abortController,
100
+ );
101
+
102
+ showSnackbar({
103
+ subtitle: inEditMode
104
+ ? t('updatePatientSuccessSnackbarSubtitle', "The patient's information has been successfully updated")
105
+ : t(
106
+ 'registerPatientSuccessSnackbarSubtitle',
107
+ 'The patient can now be found by searching for them using their name or ID number',
108
+ ),
109
+ title: inEditMode
110
+ ? t('updatePatientSuccessSnackbarTitle', 'Patient Details Updated')
111
+ : t('registerPatientSuccessSnackbarTitle', 'New Patient Created'),
112
+ kind: 'success',
113
+ isLowContrast: true,
114
+ });
115
+
116
+ const afterUrl = new URLSearchParams(search).get('afterUrl');
117
+ const redirectUrl = interpolateUrl(afterUrl || config.links.submitButton, { patientUuid: values.patientUuid });
118
+
119
+ setTarget(redirectUrl);
120
+ } catch (error) {
121
+ if (error.responseBody?.error?.globalErrors) {
122
+ error.responseBody.error.globalErrors.forEach((error) => {
123
+ showSnackbar({
124
+ title: inEditMode
125
+ ? t('updatePatientErrorSnackbarTitle', 'Patient Details Update Failed')
126
+ : t('registrationErrorSnackbarTitle', 'Patient Registration Failed'),
127
+ subtitle: error.message,
128
+ kind: 'error',
129
+ });
130
+ });
131
+ } else if (error.responseBody?.error?.message) {
132
+ showSnackbar({
133
+ title: inEditMode
134
+ ? t('updatePatientErrorSnackbarTitle', 'Patient Details Update Failed')
135
+ : t('registrationErrorSnackbarTitle', 'Patient Registration Failed'),
136
+ subtitle: error.responseBody.error.message,
137
+ kind: 'error',
138
+ });
139
+ } else {
140
+ createErrorHandler()(error);
141
+ }
142
+
143
+ helpers.setSubmitting(false);
144
+ }
145
+ };
146
+
147
+ const getDescription = (errors) => {
148
+ return (
149
+ <ul style={{ listStyle: 'inside' }}>
150
+ {Object.keys(errors).map((error, index) => {
151
+ return <li key={index}>{t(`${error}LabelText`, error)}</li>;
152
+ })}
153
+ </ul>
154
+ );
155
+ };
156
+
157
+ const displayErrors = (errors) => {
158
+ if (errors && typeof errors === 'object' && !!Object.keys(errors).length) {
159
+ showSnackbar({
160
+ isLowContrast: true,
161
+ kind: 'warning',
162
+ title: t('fieldsWithErrors', 'The following fields have errors:'),
163
+ subtitle: <>{getDescription(errors)}</>,
164
+ });
165
+ }
166
+ };
167
+
168
+ const createContextValue = useCallback(
169
+ (formikProps) => ({
170
+ identifierTypes,
171
+ validationSchema,
172
+ values: formikProps.values,
173
+ inEditMode,
174
+ setFieldValue: formikProps.setFieldValue,
175
+ setFieldTouched: formikProps.setFieldTouched,
176
+ setCapturePhotoProps,
177
+ currentPhoto: photo?.imageSrc,
178
+ isOffline,
179
+ initialFormValues: formikProps.initialValues,
180
+ setInitialFormValues,
181
+ }),
182
+ [
183
+ identifierTypes,
184
+ validationSchema,
185
+ inEditMode,
186
+ setCapturePhotoProps,
187
+ photo?.imageSrc,
188
+ isOffline,
189
+ setInitialFormValues,
190
+ ],
191
+ );
192
+
193
+ return (
194
+ <Formik
195
+ enableReinitialize
196
+ initialValues={initialFormValues}
197
+ onSubmit={onFormSubmit}
198
+ validationSchema={validationSchema}>
199
+ {(props) => (
200
+ <Form className={styles.form}>
201
+ <BeforeSavePrompt when={Object.keys(props.touched).length > 0} redirect={target} />
202
+ <div className={styles.formContainer}>
203
+ <div>
204
+ <div className={styles.stickyColumn}>
205
+ <h4>
206
+ {inEditMode
207
+ ? t('editPatientDetails', 'Edit patient details')
208
+ : t('createNewPatient', 'Create new patient')}
209
+ </h4>
210
+ {showDummyData && <DummyDataInput setValues={props.setValues} />}
211
+ <p className={styles.label01}>{t('jumpTo', 'Jump to')}</p>
212
+ {sections.map((section) => (
213
+ <div className={classNames(styles.space05, styles.touchTarget)} key={section.name}>
214
+ <Link className={styles.linkName} onClick={() => scrollIntoView(section.id)}>
215
+ <XAxis size={16} /> {t(`${section.id}Section`, section.name)}
216
+ </Link>
217
+ </div>
218
+ ))}
219
+ <Button
220
+ className={styles.submitButton}
221
+ type="submit"
222
+ onClick={() => props.validateForm().then((errors) => displayErrors(errors))}
223
+ // Current session and identifiers are required for patient registration.
224
+ // If currentSession or identifierTypes are not available, then the
225
+ // user should be blocked to register the patient.
226
+ disabled={!currentSession || !identifierTypes || props.isSubmitting}>
227
+ {props.isSubmitting ? (
228
+ <InlineLoading
229
+ className={styles.spinner}
230
+ description={`${t('submitting', 'Submitting')} ...`}
231
+ iconDescription="submitting"
232
+ />
233
+ ) : inEditMode ? (
234
+ t('updatePatient', 'Update patient')
235
+ ) : (
236
+ t('registerPatient', 'Register patient')
237
+ )}
238
+ </Button>
239
+ <Button className={styles.cancelButton} kind="secondary" onClick={cancelRegistration}>
240
+ {t('cancel', 'Cancel')}
241
+ </Button>
242
+ </div>
243
+ </div>
244
+ <div className={styles.infoGrid}>
245
+ <PatientRegistrationContextProvider value={createContextValue(props)}>
246
+ {sections.map((section, index) => (
247
+ <SectionWrapper
248
+ key={`registration-section-${section.id}`}
249
+ sectionDefinition={section}
250
+ index={index}
251
+ />
252
+ ))}
253
+ </PatientRegistrationContextProvider>
254
+ </div>
255
+ </div>
256
+ </Form>
257
+ )}
258
+ </Formik>
259
+ );
260
+ };
261
+
262
+ /**
263
+ * @internal
264
+ * Just exported for testing
265
+ */
266
+ export { exportedInitialFormValuesForTesting as initialFormValues };
@@ -0,0 +1,22 @@
1
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import { savePatient } from './patient-registration.resource';
3
+
4
+ const mockOpenmrsFetch = openmrsFetch as jest.Mock;
5
+
6
+ jest.mock('@openmrs/esm-framework', () => ({
7
+ openmrsFetch: jest.fn(),
8
+ }));
9
+
10
+ describe('savePatient', () => {
11
+ it('appends patient uuid in url if provided', () => {
12
+ mockOpenmrsFetch.mockImplementationOnce((url) => url);
13
+ savePatient(null, '1234');
14
+ expect(mockOpenmrsFetch.mock.calls[0][0]).toEqual(`${restBaseUrl}/patient/1234`);
15
+ });
16
+
17
+ it('does not append patient uuid in url', () => {
18
+ mockOpenmrsFetch.mockImplementationOnce(() => {});
19
+ savePatient(null);
20
+ expect(mockOpenmrsFetch.mock.calls[0][0]).toEqual(`${restBaseUrl}/patient/`);
21
+ });
22
+ });
@@ -0,0 +1,198 @@
1
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import { type Patient, type Relationship, type PatientIdentifier, type Encounter } from './patient-registration.types';
3
+ import dayjs from 'dayjs';
4
+
5
+ export const uuidIdentifier = '05a29f94-c0ed-11e2-94be-8c13b969e334';
6
+ export const uuidTelephoneNumber = '14d4f066-15f5-102d-96e4-000c29c2a5d7';
7
+
8
+ function dataURItoFile(dataURI: string) {
9
+ const byteString = window.atob(dataURI.split(',')[1]);
10
+ const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
11
+ // write the bytes of the string to a typed array
12
+ const buffer = new Uint8Array(byteString.length);
13
+
14
+ for (let i = 0; i < byteString.length; i++) {
15
+ buffer[i] = byteString.charCodeAt(i);
16
+ }
17
+
18
+ const blob = new Blob([buffer], { type: mimeString });
19
+ return new File([blob], 'patient-photo.png');
20
+ }
21
+
22
+ export function savePatient(patient: Patient | null, updatePatientUuid?: string) {
23
+ const abortController = new AbortController();
24
+
25
+ return openmrsFetch(`${restBaseUrl}/patient/${updatePatientUuid || ''}`, {
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ },
29
+ method: 'POST',
30
+ body: patient,
31
+ signal: abortController.signal,
32
+ });
33
+ }
34
+
35
+ export function saveEncounter(encounter: Encounter) {
36
+ const abortController = new AbortController();
37
+
38
+ return openmrsFetch(`${restBaseUrl}/encounter`, {
39
+ headers: {
40
+ 'Content-Type': 'application/json',
41
+ },
42
+ method: 'POST',
43
+ body: encounter,
44
+ signal: abortController.signal,
45
+ });
46
+ }
47
+
48
+ export function generateIdentifier(source: string) {
49
+ const abortController = new AbortController();
50
+
51
+ return openmrsFetch(`${restBaseUrl}/idgen/identifiersource/${source}/identifier`, {
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ },
55
+ method: 'POST',
56
+ body: {},
57
+ signal: abortController.signal,
58
+ });
59
+ }
60
+
61
+ export function deletePersonName(nameUuid: string, personUuid: string) {
62
+ const abortController = new AbortController();
63
+
64
+ return openmrsFetch(`${restBaseUrl}/person/${personUuid}/name/${nameUuid}`, {
65
+ method: 'DELETE',
66
+ signal: abortController.signal,
67
+ });
68
+ }
69
+
70
+ export function saveRelationship(relationship: Relationship) {
71
+ const abortController = new AbortController();
72
+
73
+ return openmrsFetch(`${restBaseUrl}/relationship`, {
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ },
77
+ method: 'POST',
78
+ body: relationship,
79
+ signal: abortController.signal,
80
+ });
81
+ }
82
+
83
+ export function updateRelationship(relationshipUuid, relationship: { relationshipType: string }) {
84
+ const abortController = new AbortController();
85
+
86
+ return openmrsFetch(`${restBaseUrl}/relationship/${relationshipUuid}`, {
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ },
90
+ method: 'POST',
91
+ body: { relationshipType: relationship.relationshipType },
92
+ signal: abortController.signal,
93
+ });
94
+ }
95
+
96
+ export function deleteRelationship(relationshipUuid) {
97
+ const abortController = new AbortController();
98
+
99
+ return openmrsFetch(`${restBaseUrl}/relationship/${relationshipUuid}`, {
100
+ headers: {
101
+ 'Content-Type': 'application/json',
102
+ },
103
+ method: 'DELETE',
104
+ signal: abortController.signal,
105
+ });
106
+ }
107
+
108
+ export async function savePatientPhoto(
109
+ patientUuid: string,
110
+ content: string,
111
+ url: string,
112
+ date: string,
113
+ conceptUuid: string,
114
+ ) {
115
+ const abortController = new AbortController();
116
+
117
+ const formData = new FormData();
118
+ formData.append('patient', patientUuid);
119
+ formData.append('file', dataURItoFile(content));
120
+ formData.append(
121
+ 'json',
122
+ JSON.stringify({
123
+ person: patientUuid,
124
+ concept: conceptUuid,
125
+ groupMembers: [],
126
+ obsDatetime: date,
127
+ }),
128
+ );
129
+
130
+ return openmrsFetch(url, {
131
+ method: 'POST',
132
+ signal: abortController.signal,
133
+ body: formData,
134
+ });
135
+ }
136
+
137
+ export async function fetchPerson(query: string, abortController: AbortController) {
138
+ const [patientsRes, personsRes] = await Promise.all([
139
+ openmrsFetch(`${restBaseUrl}/patient?q=${query}`, {
140
+ signal: abortController.signal,
141
+ }),
142
+ openmrsFetch(`${restBaseUrl}/person?q=${query}`, {
143
+ signal: abortController.signal,
144
+ }),
145
+ ]);
146
+
147
+ const results = [...patientsRes.data.results];
148
+
149
+ personsRes.data.results.forEach((person) => {
150
+ if (!results.some((patient) => patient.uuid === person.uuid)) {
151
+ results.push(person);
152
+ }
153
+ });
154
+
155
+ return results;
156
+ }
157
+
158
+ export async function addPatientIdentifier(patientUuid: string, patientIdentifier: PatientIdentifier) {
159
+ const abortController = new AbortController();
160
+ return openmrsFetch(`${restBaseUrl}/patient/${patientUuid}/identifier/`, {
161
+ method: 'POST',
162
+ headers: {
163
+ 'Content-Type': 'application/json',
164
+ },
165
+ signal: abortController.signal,
166
+ body: patientIdentifier,
167
+ });
168
+ }
169
+
170
+ export async function updatePatientIdentifier(patientUuid: string, identifierUuid: string, identifier: string) {
171
+ const abortController = new AbortController();
172
+ return openmrsFetch(`${restBaseUrl}/patient/${patientUuid}/identifier/${identifierUuid}`, {
173
+ method: 'POST',
174
+ headers: {
175
+ 'Content-Type': 'application/json',
176
+ },
177
+ signal: abortController.signal,
178
+ body: { identifier },
179
+ });
180
+ }
181
+
182
+ export async function deletePatientIdentifier(patientUuid: string, patientIdentifierUuid: string) {
183
+ const abortController = new AbortController();
184
+ return openmrsFetch(`${restBaseUrl}/patient/${patientUuid}/identifier/${patientIdentifierUuid}?purge`, {
185
+ method: 'DELETE',
186
+ headers: {
187
+ 'Content-Type': 'application/json',
188
+ },
189
+ signal: abortController.signal,
190
+ });
191
+ }
192
+
193
+ export function getDatetime(date: Date | string, time: string, timeFormat: 'AM' | 'PM') {
194
+ const datetime = new Date(date);
195
+ const [hours, minutes] = time.split(':').map(Number);
196
+ const fullHours = timeFormat === 'PM' ? (hours % 12) + 12 : hours % 12;
197
+ return dayjs(datetime).hour(fullHours).minute(minutes).second(0).millisecond(0).toDate();
198
+ }