@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,219 @@
1
+ import { navigate } from '@openmrs/esm-framework';
2
+ import * as Yup from 'yup';
3
+ import {
4
+ AddressValidationSchemaType,
5
+ FormValues,
6
+ PatientIdentifier,
7
+ PatientUuidMapType,
8
+ PatientIdentifierValue,
9
+ Encounter,
10
+ } from './patient-registration-types';
11
+ import camelCase from 'lodash-es/camelCase';
12
+ import capitalize from 'lodash-es/capitalize';
13
+
14
+ export function parseAddressTemplateXml(addressTemplate: string) {
15
+ const templateXmlDoc = new DOMParser().parseFromString(addressTemplate, 'text/xml');
16
+ const nameMappings = templateXmlDoc.querySelector('nameMappings');
17
+ const properties = nameMappings.getElementsByTagName('entry');
18
+ const validationSchemaObjs = Array.prototype.map.call(properties, (property: Element) => {
19
+ const name = property.getElementsByTagName('string')[0].innerHTML;
20
+ const label = property.getElementsByTagName('string')[1].innerHTML;
21
+ const regex = findElementValueInXmlDoc(name, 'elementRegex', templateXmlDoc) || '.*';
22
+ const regexFormat = findElementValueInXmlDoc(name, 'elementRegexFormats', templateXmlDoc) || '';
23
+
24
+ return {
25
+ name,
26
+ label,
27
+ regex,
28
+ regexFormat,
29
+ };
30
+ });
31
+
32
+ const addressValidationSchema = Yup.object(
33
+ validationSchemaObjs.reduce((final, current) => {
34
+ final[current.name] = Yup.string().matches(current.regex, current.regexFormat);
35
+ return final;
36
+ }, {}),
37
+ );
38
+
39
+ const addressFieldValues = Array.prototype.map.call(properties, (property: Element) => {
40
+ const name = property.getElementsByTagName('string')[0].innerHTML;
41
+ return {
42
+ name,
43
+ defaultValue: '',
44
+ };
45
+ });
46
+ return {
47
+ addressFieldValues,
48
+ addressValidationSchema,
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
+ navigate({ to: `${window.spaBase}/home` });
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.unidentifiedPatient =
119
+ patientName.given[0] === 'UNKNOWN' && patientName.family === 'unknown' ? true : undefined;
120
+
121
+ result.addNameInLocalLanguage = !!additionalPatientName ? true : undefined;
122
+ result.additionalGivenName = additionalPatientName?.given[0];
123
+ result.additionalMiddleName = additionalPatientName?.given[1];
124
+ result.additionalFamilyName = additionalPatientName?.family;
125
+
126
+ result.gender = capitalize(patient.gender);
127
+ result.birthdate = patient.birthDate ? (new Date(patient.birthDate) as any) : undefined;
128
+ result.telephoneNumber = patient.telecom ? patient.telecom[0].value : '';
129
+
130
+ if (patient.deceasedBoolean || patient.deceasedDateTime) {
131
+ result.isDead = true;
132
+ result.deathDate = patient.deceasedDateTime ? patient.deceasedDateTime.split('T')[0] : '';
133
+ }
134
+
135
+ return {
136
+ ...result,
137
+ ...patient.identifier.map((identifier) => {
138
+ const key = camelCase(identifier.system || identifier.type.text);
139
+ return { [key]: identifier.value };
140
+ }),
141
+ };
142
+ }
143
+
144
+ export function getAddressFieldValuesFromFhirPatient(patient: fhir.Patient) {
145
+ const result = {};
146
+ const address = patient.address?.[0];
147
+
148
+ if (address) {
149
+ for (const key of Object.keys(address)) {
150
+ switch (key) {
151
+ case 'city':
152
+ result['cityVillage'] = address[key];
153
+ break;
154
+ case 'state':
155
+ result['stateProvince'] = address[key];
156
+ break;
157
+ case 'district':
158
+ result['countyDistrict'] = address[key];
159
+ break;
160
+ case 'extension':
161
+ address[key].forEach((ext) => {
162
+ ext.extension.forEach((extension) => {
163
+ result[extension.url.split('#')[1]] = extension.valueString;
164
+ });
165
+ });
166
+ break;
167
+ default:
168
+ if (key === 'country' || key === 'postalCode') {
169
+ result[key] = address[key];
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ return result;
176
+ }
177
+
178
+ export function getPatientUuidMapFromFhirPatient(patient: fhir.Patient): PatientUuidMapType {
179
+ const patientName = patient.name[0];
180
+ const additionalPatientName = patient.name[1];
181
+ const address = patient.address?.[0];
182
+
183
+ return {
184
+ preferredNameUuid: patientName?.id,
185
+ additionalNameUuid: additionalPatientName?.id,
186
+ preferredAddressUuid: address?.id,
187
+ ...patient.identifier.map((identifier) => {
188
+ const key = camelCase(identifier.system || identifier.type.text);
189
+ return { [key]: { uuid: identifier.id, value: identifier.value } };
190
+ }),
191
+ };
192
+ }
193
+
194
+ export function getPatientIdentifiersFromFhirPatient(patient: fhir.Patient): Array<PatientIdentifier> {
195
+ return patient.identifier.map((identifier) => {
196
+ return {
197
+ uuid: identifier.id,
198
+ identifier: identifier.value,
199
+ };
200
+ });
201
+ }
202
+
203
+ export function getPhonePersonAttributeValueFromFhirPatient(patient: fhir.Patient) {
204
+ const result = {};
205
+ if (patient.telecom) {
206
+ result['phone'] = patient.telecom[0].value;
207
+ }
208
+ return result;
209
+ }
210
+
211
+ export const filterUndefinedPatientIdenfier = (patientIdenfiers) =>
212
+ Object.fromEntries(
213
+ Object.entries<PatientIdentifierValue>(patientIdenfiers).filter(
214
+ ([key, value]) => value.identifierValue !== undefined,
215
+ ),
216
+ );
217
+
218
+ export const latestFirstEncounter = (a: Encounter, b: Encounter) =>
219
+ new Date(b.encounterDatetime).getTime() - new Date(a.encounterDatetime).getTime();
@@ -0,0 +1,250 @@
1
+ import React, { useState, useEffect, useContext, useMemo, useRef } from 'react';
2
+ import { Button, Link } from '@carbon/react';
3
+ import { XAxis, ShareKnowledge } from '@carbon/react/icons';
4
+ import { Router, useLocation, useParams } from 'react-router-dom';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Formik, Form, FormikHelpers } from 'formik';
7
+ import { createErrorHandler, showToast, useConfig, interpolateUrl, usePatient } from '@openmrs/esm-framework';
8
+ import { validationSchema as initialSchema } from './validation/patient-registration-validation';
9
+ import { FormValues, CapturePhotoProps, PatientIdentifierValue } from './patient-registration-types';
10
+ import { PatientRegistrationContext } from './patient-registration-context';
11
+ import { SavePatientForm, SavePatientTransactionManager } from './form-manager';
12
+ import { usePatientPhoto } from './patient-registration.resource';
13
+ import { DummyDataInput } from './input/dummy-data/dummy-data-input.component';
14
+ import {
15
+ cancelRegistration,
16
+ filterUndefinedPatientIdenfier,
17
+ parseAddressTemplateXml,
18
+ scrollIntoView,
19
+ } from './patient-registration-utils';
20
+ import {
21
+ useInitialAddressFieldValues,
22
+ useInitialFormValues,
23
+ usePatientObs,
24
+ usePatientUuidMap,
25
+ } from './patient-registration-hooks';
26
+ import { ResourcesContext } from '../offline.resources';
27
+ import { builtInSections, RegistrationConfig, SectionDefinition } from '../config-schema';
28
+ import { SectionWrapper } from './section/section-wrapper.component';
29
+ import BeforeSavePrompt from './before-save-prompt';
30
+ import styles from './patient-registration.scss';
31
+ import PatientVerification from '../patient-verification/patient-verification.component';
32
+ import { handleSavePatientToClientRegistry } from '../patient-verification/patient-verification-hook';
33
+
34
+ let exportedInitialFormValuesForTesting = {} as FormValues;
35
+
36
+ export interface PatientRegistrationProps {
37
+ savePatientForm: SavePatientForm;
38
+ isOffline: boolean;
39
+ }
40
+
41
+ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePatientForm, isOffline }) => {
42
+ const { currentSession, addressTemplate, identifierTypes } = useContext(ResourcesContext);
43
+ const { search } = useLocation();
44
+ const config = useConfig() as RegistrationConfig;
45
+ const [target, setTarget] = useState<undefined | string>();
46
+ const [validationSchema, setValidationSchema] = useState(initialSchema);
47
+ const { patientUuid: uuidOfPatientToEdit } = useParams();
48
+ const { isLoading: isLoadingPatientToEdit, patient: patientToEdit } = usePatient(uuidOfPatientToEdit);
49
+ const { t } = useTranslation();
50
+ const [capturePhotoProps, setCapturePhotoProps] = useState<CapturePhotoProps | null>(null);
51
+ const [initialFormValues, setInitialFormValues] = useInitialFormValues(uuidOfPatientToEdit);
52
+ const [initialAddressFieldValues] = useInitialAddressFieldValues(uuidOfPatientToEdit);
53
+ const [patientUuidMap] = usePatientUuidMap(uuidOfPatientToEdit);
54
+ const location = currentSession?.sessionLocation?.uuid;
55
+ const inEditMode = isLoadingPatientToEdit ? undefined : !!(uuidOfPatientToEdit && patientToEdit);
56
+ const showDummyData = useMemo(() => localStorage.getItem('openmrs:devtools') === 'true' && !inEditMode, [inEditMode]);
57
+ const { data: photo } = usePatientPhoto(patientToEdit?.id);
58
+ const savePatientTransactionManager = useRef(new SavePatientTransactionManager());
59
+ const fieldDefinition = config?.fieldDefinitions?.filter((def) => def.type === 'address');
60
+ const [enableClientRegistry, setEnableClientRegistry] = useState(
61
+ inEditMode ? initialFormValues.identifiers['nationalUniquePatientIdentifier']?.identifierValue : false,
62
+ );
63
+
64
+ useEffect(() => {
65
+ exportedInitialFormValuesForTesting = initialFormValues;
66
+ }, [initialFormValues]);
67
+
68
+ const sections: Array<SectionDefinition> = useMemo(() => {
69
+ return config.sections
70
+ .map(
71
+ (sectionName) =>
72
+ config.sectionDefinitions.filter((s) => s.id == sectionName)[0] ??
73
+ builtInSections.filter((s) => s.id == sectionName)[0],
74
+ )
75
+ .filter((s) => s);
76
+ }, [config.sections, config.sectionDefinitions]);
77
+
78
+ useEffect(() => {
79
+ if (addressTemplate) {
80
+ const addressTemplateXml = addressTemplate?.results[0].value;
81
+ if (!addressTemplateXml) {
82
+ return;
83
+ }
84
+ const { addressValidationSchema } = parseAddressTemplateXml(addressTemplateXml);
85
+ setValidationSchema((validationSchema) => validationSchema.concat(addressValidationSchema));
86
+ }
87
+ }, [inEditMode, addressTemplate, initialAddressFieldValues]);
88
+
89
+ const onFormSubmit = async (values: FormValues, helpers: FormikHelpers<FormValues>) => {
90
+ const abortController = new AbortController();
91
+ helpers.setSubmitting(true);
92
+
93
+ const updatedFormValues = { ...values, identifiers: filterUndefinedPatientIdenfier(values.identifiers) };
94
+ try {
95
+ await savePatientForm(
96
+ !inEditMode,
97
+ updatedFormValues,
98
+ patientUuidMap,
99
+ initialAddressFieldValues,
100
+ capturePhotoProps,
101
+ location,
102
+ initialFormValues['identifiers'],
103
+ currentSession,
104
+ config,
105
+ savePatientTransactionManager.current,
106
+ abortController,
107
+ );
108
+
109
+ showToast({
110
+ description: inEditMode
111
+ ? t('updationSuccessToastDescription', "The patient's information has been successfully updated")
112
+ : t(
113
+ 'registrationSuccessToastDescription',
114
+ 'The patient can now be found by searching for them using their name or ID number',
115
+ ),
116
+ title: inEditMode
117
+ ? t('updationSuccessToastTitle', 'Patient Details Updated')
118
+ : t('registrationSuccessToastTitle', 'New Patient Created'),
119
+ kind: 'success',
120
+ });
121
+
122
+ const afterUrl = new URLSearchParams(search).get('afterUrl');
123
+ const redirectUrl = interpolateUrl(afterUrl || config.links.submitButton, { patientUuid: values.patientUuid });
124
+
125
+ setTarget(redirectUrl);
126
+ } catch (error) {
127
+ if (error.responseBody?.error?.globalErrors) {
128
+ error.responseBody.error.globalErrors.forEach((error) => {
129
+ showToast({ description: error.message });
130
+ });
131
+ } else if (error.responseBody?.error?.message) {
132
+ showToast({ description: error.responseBody.error.message });
133
+ } else {
134
+ createErrorHandler()(error);
135
+ }
136
+
137
+ helpers.setSubmitting(false);
138
+ }
139
+ };
140
+
141
+ const getDescription = (errors) => {
142
+ return (
143
+ <div>
144
+ <p>{t('fieldErrorTitleMessage', 'The following fields have errors:')}</p>
145
+ <ul style={{ listStyle: 'inside' }}>
146
+ {Object.keys(errors).map((error, index) => (
147
+ <li key={index}>{t(`${error}LabelText`, error)}</li>
148
+ ))}
149
+ </ul>
150
+ </div>
151
+ );
152
+ };
153
+
154
+ const displayErrors = (errors) => {
155
+ if (errors && typeof errors === 'object' && !!Object.keys(errors).length) {
156
+ showToast({
157
+ description: getDescription(errors),
158
+ title: t('incompleteForm', 'Incomplete form'),
159
+ kind: 'warning',
160
+ });
161
+ }
162
+ };
163
+
164
+ return (
165
+ <Formik
166
+ enableReinitialize
167
+ initialValues={initialFormValues}
168
+ validationSchema={validationSchema}
169
+ onSubmit={onFormSubmit}>
170
+ {(props) => (
171
+ <Form className={styles.form}>
172
+ <BeforeSavePrompt when={props.dirty} redirect={target} />
173
+ <div className={styles.formContainer}>
174
+ <div>
175
+ <div className={styles.stickyColumn}>
176
+ <h4>
177
+ {inEditMode ? t('edit', 'Edit') : t('createNew', 'Create New')} {t('patient', 'Patient')}
178
+ </h4>
179
+ {showDummyData && <DummyDataInput setValues={props.setValues} />}
180
+ <p className={styles.label01}>{t('jumpTo', 'Jump to')}</p>
181
+ {sections.map((section) => (
182
+ <div className={`${styles.space05} ${styles.touchTarget}`} key={section.name}>
183
+ <Link className={styles.linkName} onClick={() => scrollIntoView(section.id)}>
184
+ <XAxis size={16} /> {t(`${section.id}Section`, section.name)}
185
+ </Link>
186
+ </div>
187
+ ))}
188
+ <Button
189
+ renderIcon={ShareKnowledge}
190
+ disabled={!currentSession || !identifierTypes}
191
+ onClick={() => {
192
+ setEnableClientRegistry(true);
193
+ props.isValid
194
+ ? handleSavePatientToClientRegistry(props.values, props.setValues, inEditMode)
195
+ : props.validateForm().then((errors) => displayErrors(errors));
196
+ }}
197
+ className={styles.submitButton}>
198
+ {t('postToRegistry', 'Post to registry')}
199
+ </Button>
200
+ <Button
201
+ className={styles.submitButton}
202
+ type="submit"
203
+ onClick={() => props.validateForm().then((errors) => displayErrors(errors))}
204
+ // Current session and identifiers are required for patient registration.
205
+ // If currentSession or identifierTypes are not available, then the
206
+ // user should be blocked to register the patient.
207
+ disabled={!enableClientRegistry}>
208
+ {inEditMode ? t('updatePatient', 'Update Patient') : t('registerPatient', 'Register Patient')}
209
+ </Button>
210
+ <Button className={styles.cancelButton} kind="tertiary" onClick={cancelRegistration}>
211
+ {t('cancel', 'Cancel')}
212
+ </Button>
213
+ </div>
214
+ </div>
215
+ <div className={styles.infoGrid}>
216
+ <PatientRegistrationContext.Provider
217
+ value={{
218
+ identifierTypes: identifierTypes,
219
+ validationSchema,
220
+ setValidationSchema,
221
+ values: props.values,
222
+ inEditMode,
223
+ setFieldValue: props.setFieldValue,
224
+ setCapturePhotoProps,
225
+ currentPhoto: photo?.imageSrc,
226
+ isOffline,
227
+ initialFormValues: props.initialValues,
228
+ }}>
229
+ <PatientVerification props={props} />
230
+ {sections.map((section, index) => (
231
+ <SectionWrapper
232
+ key={`registration-section-${section.id}`}
233
+ sectionDefinition={section}
234
+ index={index}
235
+ />
236
+ ))}
237
+ </PatientRegistrationContext.Provider>
238
+ </div>
239
+ </div>
240
+ </Form>
241
+ )}
242
+ </Formik>
243
+ );
244
+ };
245
+
246
+ /**
247
+ * @internal
248
+ * Just exported for testing
249
+ */
250
+ export { exportedInitialFormValuesForTesting as initialFormValues };
@@ -0,0 +1,26 @@
1
+ import { openmrsFetch } 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
+ afterEach(() => {
12
+ jest.resetAllMocks();
13
+ });
14
+
15
+ it('appends patient uuid in url if provided', () => {
16
+ mockOpenmrsFetch.mockImplementationOnce((url) => url);
17
+ savePatient(null, '1234');
18
+ expect(mockOpenmrsFetch.mock.calls[0][0]).toEqual('/ws/rest/v1/patient/1234');
19
+ });
20
+
21
+ it('does not append patient uuid in url', () => {
22
+ mockOpenmrsFetch.mockImplementationOnce(() => {});
23
+ savePatient(null);
24
+ expect(mockOpenmrsFetch.mock.calls[0][0]).toEqual('/ws/rest/v1/patient/');
25
+ });
26
+ });