@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,181 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { AddressHierarchy } from './address-hierarchy.component';
4
+ import { Formik, Form } from 'formik';
5
+ import { Resources, ResourcesContext } from '../../../offline.resources';
6
+ import { PatientRegistrationContext } from '../../patient-registration-context';
7
+ import { useConfig } from '@openmrs/esm-framework';
8
+
9
+ jest.mock('@openmrs/esm-framework', () => ({
10
+ ...jest.requireActual('@openmrs/esm-framework'),
11
+ useConfig: jest.fn(),
12
+ }));
13
+
14
+ const mockResponse1 = {
15
+ results: [
16
+ {
17
+ value:
18
+ '<?xml version="1.0" encoding="UTF-8"?> \
19
+ <org.openmrs.layout.address.AddressTemplate> \
20
+ <nameMappings class="properties"> \
21
+ <property name="postalCode" value="Postcode" /> \
22
+ <property name="address1" value="Address line 1" /> \
23
+ <property name="address2" value="Address line 2" /> \
24
+ <property name="country" value="Country" /> \
25
+ <property name="stateProvince" value="State" /> \
26
+ <property name="cityVillage" value="City" /> \
27
+ </nameMappings> \
28
+ <sizeMappings class="properties"> \
29
+ <property name="postalCode" value="10" /> \
30
+ <property name="address2" value="40" /> \
31
+ <property name="address1" value="40" /> \
32
+ <property name="country" value="10" /> \
33
+ <property name="stateProvince" value="10" /> \
34
+ <property name="cityVillage" value="10" /> \
35
+ </sizeMappings> \
36
+ <lineByLineFormat> \
37
+ <string> address1</string> \
38
+ <string> address2</string> \
39
+ <string> cityVillage stateProvince country postalCode</string> \
40
+ </lineByLineFormat> \
41
+ <requiredElements /> \
42
+ </org.openmrs.layout.address.AddressTemplate>',
43
+ },
44
+ ],
45
+ };
46
+
47
+ const mockResponse2 = {
48
+ results: [
49
+ {
50
+ value:
51
+ '<org.openmrs.layout.address.AddressTemplate> \
52
+ <nameMappings> \
53
+ <entry> \
54
+ <string>country</string> \
55
+ <string>Country</string> \
56
+ </entry> \
57
+ <entry> \
58
+ <string>postalCode</string> \
59
+ <string>Postcode</string> \
60
+ </entry> \
61
+ <entry> \
62
+ <string>address1</string> \
63
+ <string>Address line 1</string> \
64
+ </entry> \
65
+ <entry> \
66
+ <string>address2</string> \
67
+ <string>Address line 2</string> \
68
+ </entry> \
69
+ <entry> \
70
+ <string>stateProvince</string> \
71
+ <string>State</string> \
72
+ </entry> \
73
+ <entry> \
74
+ <string>cityVillage</string> \
75
+ <string>City</string> \
76
+ </entry> \
77
+ </nameMappings> \
78
+ <sizeMappings> \
79
+ <entry> \
80
+ <string>country</string> \
81
+ <string>40</string> \
82
+ </entry> \
83
+ <entry> \
84
+ <string>countyDistrict</string> \
85
+ <string>40</string> \
86
+ </entry> \
87
+ <entry> \
88
+ <string>address1</string> \
89
+ <string>40</string> \
90
+ </entry> \
91
+ <entry> \
92
+ <string>stateProvince</string> \
93
+ <string>40</string> \
94
+ </entry> \
95
+ <entry> \
96
+ <string>cityVillage</string> \
97
+ <string>40</string> \
98
+ </entry> \
99
+ </sizeMappings> \
100
+ <elementDefaults> \
101
+ <entry> \
102
+ <string>country</string> \
103
+ <string>Cambodia</string> \
104
+ </entry> \
105
+ </elementDefaults> \
106
+ <lineByLineFormat> \
107
+ <string>cityVillage, address1</string> \
108
+ <string>countyDistrict, stateProvince</string> \
109
+ <string>country</string> \
110
+ </lineByLineFormat> \
111
+ <maxTokens>0</maxTokens> \
112
+ </org.openmrs.layout.address.AddressTemplate>',
113
+ },
114
+ ],
115
+ };
116
+
117
+ async function testAddressHierarchy(mockResponse) {
118
+ await render(
119
+ <ResourcesContext.Provider value={{ addressTemplate: mockResponse } as Resources}>
120
+ <Formik initialValues={{}} onSubmit={null}>
121
+ <Form>
122
+ <PatientRegistrationContext.Provider value={{ setFieldValue: jest.fn() }}>
123
+ <AddressHierarchy />
124
+ </PatientRegistrationContext.Provider>
125
+ </Form>
126
+ </Formik>
127
+ </ResourcesContext.Provider>,
128
+ );
129
+ const countryInput = screen.getByLabelText('Country (optional)');
130
+ expect(countryInput).toBeInTheDocument();
131
+ expect(countryInput).toHaveAttribute('name', 'address.country');
132
+ const stateInput = screen.getByLabelText('State (optional)');
133
+ expect(stateInput).toBeInTheDocument();
134
+ expect(stateInput).toHaveAttribute('name', 'address.stateProvince');
135
+ const cityInput = screen.getByLabelText('City (optional)');
136
+ expect(cityInput).toBeInTheDocument();
137
+ expect(cityInput).toHaveAttribute('name', 'address.cityVillage');
138
+ const address1Input = screen.getByLabelText('Address line 1 (optional)');
139
+ expect(address1Input).toBeInTheDocument();
140
+ expect(address1Input).toHaveAttribute('name', 'address.address1');
141
+ const address2Input = screen.getByLabelText('Address line 2 (optional)');
142
+ expect(address2Input).toBeInTheDocument();
143
+ expect(address2Input).toHaveAttribute('name', 'address.address2');
144
+ const postalCodeInput = screen.getByLabelText('Postcode (optional)');
145
+ expect(postalCodeInput).toBeInTheDocument();
146
+ expect(postalCodeInput).toHaveAttribute('name', 'address.postalCode');
147
+ }
148
+
149
+ describe('address hierarchy', () => {
150
+ it('renders text input fields matching addressTemplate config', async () => {
151
+ (useConfig as jest.Mock).mockImplementation(() => ({
152
+ fieldConfigurations: {
153
+ address: {
154
+ useAddressHierarchy: {
155
+ enabled: false,
156
+ useQuickSearch: false,
157
+ searchAddressByLevel: false,
158
+ },
159
+ },
160
+ },
161
+ }));
162
+ testAddressHierarchy(mockResponse1);
163
+ testAddressHierarchy(mockResponse2);
164
+ });
165
+
166
+ it('renders combo input fields matching addressTemplate config', async () => {
167
+ (useConfig as jest.Mock).mockImplementation(() => ({
168
+ fieldConfigurations: {
169
+ address: {
170
+ useAddressHierarchy: {
171
+ enabled: true,
172
+ useQuickSearch: false,
173
+ searchAddressByLevel: true,
174
+ },
175
+ },
176
+ },
177
+ }));
178
+ testAddressHierarchy(mockResponse1);
179
+ testAddressHierarchy(mockResponse2);
180
+ });
181
+ });
@@ -0,0 +1,98 @@
1
+ import React, { useState, useRef, useEffect, useMemo } from 'react';
2
+ import { useAddressHierarchy } from '../../patient-registration.resource';
3
+ import { Search } from '@carbon/react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { useFormikContext } from 'formik';
6
+ import styles from './address-search.scss';
7
+
8
+ interface AddressSearchComponentProps {
9
+ addressLayout: Array<any>;
10
+ }
11
+
12
+ const AddressSearchComponent: React.FC<AddressSearchComponentProps> = ({ addressLayout }) => {
13
+ const { t } = useTranslation();
14
+ const separator = ' > ';
15
+ const searchBox = useRef(null);
16
+ const wrapper = useRef(null);
17
+ const [searchString, setSearchString] = useState<string>('');
18
+ const { addresses, isLoading, error } = useAddressHierarchy(searchString, separator);
19
+ const addressOptions: Array<string> = useMemo(() => {
20
+ const options: Set<string> = new Set();
21
+ addresses.forEach((address) => {
22
+ const values = address.split(separator);
23
+ values.forEach((val, index) => {
24
+ if (val.toLowerCase().includes(searchString.toLowerCase())) {
25
+ options.add(values.slice(0, index + 1).join(separator));
26
+ }
27
+ });
28
+ });
29
+ return [...options];
30
+ }, [addresses, searchString]);
31
+
32
+ const { setFieldValue } = useFormikContext();
33
+
34
+ const handleInputChange = (e) => {
35
+ setSearchString(e.target.value);
36
+ };
37
+
38
+ const handleChange = (address) => {
39
+ if (address) {
40
+ const values = address.split(separator);
41
+ addressLayout.map(({ name }, index) => {
42
+ setFieldValue(`address.${name}`, values?.[index] ?? '');
43
+ });
44
+ setSearchString('');
45
+ }
46
+ };
47
+
48
+ const handleClickOutsideComponent = (e) => {
49
+ if (wrapper.current && !wrapper.current.contains(e.target)) {
50
+ setSearchString('');
51
+ }
52
+ };
53
+
54
+ useEffect(() => {
55
+ document.addEventListener('mousedown', handleClickOutsideComponent);
56
+
57
+ return () => {
58
+ document.removeEventListener('mousedown', handleClickOutsideComponent);
59
+ };
60
+ }, [wrapper]);
61
+
62
+ return (
63
+ <div className={styles.autocomplete} ref={wrapper} style={{ marginBottom: '1rem' }}>
64
+ <Search
65
+ onChange={handleInputChange}
66
+ labelText={t('searchAddress', 'Search address')}
67
+ placeholder={t('searchAddress', 'Search address')}
68
+ ref={searchBox}
69
+ helperText={
70
+ searchString
71
+ ? isLoading
72
+ ? t('loadingResults', 'Loading results')
73
+ : error
74
+ ? error.message
75
+ : addresses?.length === 0
76
+ ? t('noResultsFound', 'No results found')
77
+ : null
78
+ : null
79
+ }
80
+ value={searchString}
81
+ />
82
+ {addressOptions.length > 0 && (
83
+ /* Since the input has a marginBottom of 1rem */
84
+ <ul className={styles.suggestions}>
85
+ {addressOptions.map((address, index) => (
86
+ <li //eslint-disable-line jsx-a11y/no-noninteractive-element-interactions
87
+ key={index}
88
+ onClick={(e) => handleChange(address)}>
89
+ {address}
90
+ </li>
91
+ ))}
92
+ </ul>
93
+ )}
94
+ </div>
95
+ );
96
+ };
97
+
98
+ export default AddressSearchComponent;
@@ -0,0 +1,53 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .label01 {
6
+ @include type.type-style('label-01');
7
+ }
8
+
9
+ .suggestions {
10
+ border-top-width: 0;
11
+ list-style: none;
12
+ margin-top: 0;
13
+ max-height: 20rem;
14
+ overflow-y: auto;
15
+ padding-left: 0;
16
+ width: 100%;
17
+ position: absolute;
18
+ left: 0;
19
+ background-color: #fff;
20
+ margin-bottom: 20px;
21
+ z-index: 99;
22
+ border: 1px solid $ui-03;
23
+ }
24
+
25
+ .suggestions li {
26
+ padding: spacing.$spacing-05;
27
+ line-height: 1.29;
28
+ color: #525252;
29
+ border-bottom: 1px solid #8d8d8d;
30
+ }
31
+
32
+ .suggestions li:hover {
33
+ background-color: #e5e5e5;
34
+ color: #161616;
35
+ cursor: pointer;
36
+ }
37
+
38
+ .suggestions li:not(:last-of-type) {
39
+ border-bottom: 1px solid #999;
40
+ }
41
+
42
+ .autocomplete {
43
+ position: relative;
44
+ }
45
+
46
+ .autocompleteSearch {
47
+ width: 100%;
48
+ }
49
+
50
+ .suggestions a {
51
+ color: inherit;
52
+ text-decoration: none;
53
+ }
@@ -0,0 +1,25 @@
1
+ import { useConfig } from '@openmrs/esm-framework';
2
+ import React from 'react';
3
+ import { RegistrationConfig } from '../../config-schema';
4
+ import { AddressField } from './address/address-field.component';
5
+ import { ObsField } from './obs/obs-field.component';
6
+ import { PersonAttributeField } from './person-attributes/person-attribute-field.component';
7
+
8
+ export interface CustomFieldProps {
9
+ name: string;
10
+ }
11
+
12
+ export function CustomField({ name }: CustomFieldProps) {
13
+ const config = useConfig() as RegistrationConfig;
14
+ const fieldDefinition = config.fieldDefinitions.filter((def) => def.id == name)[0];
15
+
16
+ if (fieldDefinition.type === 'person attribute') {
17
+ return <PersonAttributeField fieldDefinition={fieldDefinition} />;
18
+ } else if (fieldDefinition.type === 'obs') {
19
+ return <ObsField fieldDefinition={fieldDefinition} />;
20
+ } else if (fieldDefinition.type === 'address') {
21
+ return <AddressField fieldDefinition={fieldDefinition} />;
22
+ } else {
23
+ return <div>Error: Unknown field type {fieldDefinition.type}</div>;
24
+ }
25
+ }
@@ -0,0 +1,143 @@
1
+ import React, { useContext } from 'react';
2
+ import { ContentSwitcher, DatePicker, DatePickerInput, Switch, TextInput, Layer } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { useField } from 'formik';
5
+ import { generateFormatting } from '../../date-util';
6
+ import { PatientRegistrationContext } from '../../patient-registration-context';
7
+ import styles from '../field.scss';
8
+ import { useConfig } from '@openmrs/esm-framework';
9
+ import { RegistrationConfig } from '../../../config-schema';
10
+
11
+ const calcBirthdate = (yearDelta, monthDelta, dateOfBirth) => {
12
+ const { enabled, month, dayOfMonth } = dateOfBirth.useEstimatedDateOfBirth;
13
+ const startDate = new Date();
14
+ const resultMonth = new Date(startDate.getFullYear() - yearDelta, startDate.getMonth() - monthDelta, 1);
15
+ const daysInResultMonth = new Date(resultMonth.getFullYear(), resultMonth.getMonth() + 1, 0).getDate();
16
+ const resultDate = new Date(
17
+ resultMonth.getFullYear(),
18
+ resultMonth.getMonth(),
19
+ Math.min(startDate.getDate(), daysInResultMonth),
20
+ );
21
+ return enabled ? new Date(resultDate.getFullYear(), month, dayOfMonth) : resultDate;
22
+ };
23
+
24
+ export const DobField: React.FC = () => {
25
+ const { t } = useTranslation();
26
+ const {
27
+ fieldConfigurations: { dateOfBirth },
28
+ } = useConfig() as RegistrationConfig;
29
+ const [dobUnknown] = useField('birthdateEstimated');
30
+ const dobKnown = !dobUnknown.value;
31
+ const [birthdate, birthdateMeta] = useField('birthdate');
32
+ const [yearsEstimated, yearsEstimateMeta] = useField('yearsEstimated');
33
+ const [monthsEstimated, monthsEstimateMeta] = useField('monthsEstimated');
34
+ const { setFieldValue } = useContext(PatientRegistrationContext);
35
+ const { format, placeHolder, dateFormat } = generateFormatting(['d', 'm', 'Y'], '/');
36
+ const today = new Date();
37
+
38
+ const onToggle = (e) => {
39
+ setFieldValue('birthdateEstimated', e.name === 'unknown');
40
+ setFieldValue('birthdate', '');
41
+ setFieldValue('yearsEstimated', 0);
42
+ setFieldValue('monthsEstimated', '');
43
+ };
44
+
45
+ const onDateChange = ([birthdate]) => {
46
+ setFieldValue('birthdate', birthdate);
47
+ };
48
+
49
+ const onEstimatedYearsChange = (ev) => {
50
+ const years = +ev.target.value;
51
+
52
+ if (!isNaN(years) && years < 140 && years >= 0) {
53
+ setFieldValue('yearsEstimated', years);
54
+ setFieldValue('birthdate', calcBirthdate(years, monthsEstimateMeta.value, dateOfBirth));
55
+ }
56
+ };
57
+
58
+ const onEstimatedMonthsChange = (e) => {
59
+ const months = +e.target.value;
60
+
61
+ if (!isNaN(months)) {
62
+ setFieldValue('monthsEstimated', months);
63
+ setFieldValue('birthdate', calcBirthdate(yearsEstimateMeta.value, months, dateOfBirth));
64
+ }
65
+ };
66
+
67
+ const updateBirthdate = () => {
68
+ const months = +monthsEstimateMeta.value % 12;
69
+ const years = +yearsEstimateMeta.value + Math.floor(monthsEstimateMeta.value / 12);
70
+ setFieldValue('yearsEstimated', years);
71
+ setFieldValue('monthsEstimated', months > 0 ? months : '');
72
+ setFieldValue('birthdate', calcBirthdate(years, months, dateOfBirth));
73
+ };
74
+
75
+ return (
76
+ <div className={styles.halfWidthInDesktopView}>
77
+ <h4 className={styles.productiveHeading02Light}>{t('birthFieldLabelText', 'Birth')}</h4>
78
+ <div className={styles.dobField}>
79
+ <div className={styles.dobContentSwitcherLabel}>
80
+ <span className={styles.label01}>{t('dobToggleLabelText', 'Date of Birth Known?')}</span>
81
+ </div>
82
+ <ContentSwitcher onChange={onToggle}>
83
+ <Switch name="known" text={t('yes', 'Yes')} />
84
+ <Switch name="unknown" text={t('no', 'No')} />
85
+ </ContentSwitcher>
86
+ </div>
87
+ <Layer>
88
+ {dobKnown ? (
89
+ <div className={styles.dobField}>
90
+ <DatePicker dateFormat={dateFormat} datePickerType="single" onChange={onDateChange} maxDate={format(today)}>
91
+ <DatePickerInput
92
+ id="birthdate"
93
+ {...birthdate}
94
+ placeholder={placeHolder}
95
+ labelText={t('dateOfBirthLabelText', 'Date of Birth')}
96
+ invalid={!!(birthdateMeta.touched && birthdateMeta.error)}
97
+ invalidText={birthdateMeta.error && t(birthdateMeta.error)}
98
+ value={format(birthdate.value)}
99
+ />
100
+ </DatePicker>
101
+ </div>
102
+ ) : (
103
+ <div className={styles.grid}>
104
+ <div className={styles.dobField}>
105
+ <TextInput
106
+ id="yearsEstimated"
107
+ type="number"
108
+ name={yearsEstimated.name}
109
+ light
110
+ onChange={onEstimatedYearsChange}
111
+ labelText={t('estimatedAgeInYearsLabelText', 'Estimated age in years')}
112
+ invalid={!!(yearsEstimateMeta.touched && yearsEstimateMeta.error)}
113
+ invalidText={yearsEstimateMeta.error && t(yearsEstimateMeta.error)}
114
+ value={yearsEstimated.value}
115
+ min={0}
116
+ required
117
+ {...yearsEstimated}
118
+ onBlur={updateBirthdate}
119
+ />
120
+ </div>
121
+ <div className={styles.dobField}>
122
+ <TextInput
123
+ id="monthsEstimated"
124
+ type="number"
125
+ name={monthsEstimated.name}
126
+ light
127
+ onChange={onEstimatedMonthsChange}
128
+ labelText={t('estimatedAgeInMonthsLabelText', 'Estimated age in months')}
129
+ invalid={!!(monthsEstimateMeta.touched && monthsEstimateMeta.error)}
130
+ invalidText={monthsEstimateMeta.error && t(monthsEstimateMeta.error)}
131
+ value={monthsEstimated.value}
132
+ min={0}
133
+ {...monthsEstimated}
134
+ required={!yearsEstimateMeta.value}
135
+ onBlur={updateBirthdate}
136
+ />
137
+ </div>
138
+ </div>
139
+ )}
140
+ </Layer>
141
+ </div>
142
+ );
143
+ };
@@ -0,0 +1,73 @@
1
+ import React from 'react';
2
+ import { Formik, Form } from 'formik';
3
+ import { render, screen } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import '@testing-library/jest-dom/extend-expect';
6
+ import '@testing-library/jest-dom';
7
+ import { DobField } from './dob.component';
8
+ import { PatientRegistrationContext } from '../../patient-registration-context';
9
+ import { initialFormValues } from '../../patient-registration.component';
10
+ import { FormValues } from '../../patient-registration-types';
11
+
12
+ jest.mock('@openmrs/esm-framework', () => {
13
+ const originalModule = jest.requireActual('@openmrs/esm-framework');
14
+ return {
15
+ ...originalModule,
16
+ useConfig: jest.fn().mockImplementation(() => ({
17
+ fieldConfigurations: { dateOfBirth: { useEstimatedDateOfBirth: { enabled: true, dayOfMonth: 0, month: 0 } } },
18
+ })),
19
+ };
20
+ });
21
+
22
+ describe('Dob', () => {
23
+ it('renders the fields in the birth section of the registration form', async () => {
24
+ renderDob();
25
+
26
+ expect(screen.getByRole('heading', { name: /birth/i })).toBeInTheDocument();
27
+ expect(screen.getByText(/date of birth known?/i)).toBeInTheDocument();
28
+ expect(screen.getByRole('tab', { name: /no/i })).toBeInTheDocument();
29
+ expect(screen.getByRole('tab', { name: /yes/i })).toBeInTheDocument();
30
+ expect(screen.getByRole('tab', { name: /yes/i })).toHaveAttribute('aria-selected', 'true');
31
+ expect(screen.getByRole('tab', { name: /no/i })).toHaveAttribute('aria-selected', 'false');
32
+ expect(screen.getByRole('textbox', { name: /date of birth/i })).toBeInTheDocument();
33
+ });
34
+
35
+ it('typing in the date picker input sets the date of birth', async () => {
36
+ const user = userEvent.setup();
37
+
38
+ renderDob();
39
+
40
+ const dateInput = screen.getByRole('textbox', { name: /date of birth/i });
41
+ expect(dateInput).toBeInTheDocument();
42
+
43
+ await user.type(dateInput, '10/10/2022');
44
+
45
+ expect(screen.getByPlaceholderText('dd/mm/YYYY')).toHaveValue('10/10/2022');
46
+ });
47
+ });
48
+
49
+ function renderDob() {
50
+ let formValues: FormValues = initialFormValues;
51
+
52
+ render(
53
+ <Formik initialValues={{ birthdate: '' }} onSubmit={() => {}}>
54
+ <Form>
55
+ <PatientRegistrationContext.Provider
56
+ value={{
57
+ identifierTypes: [],
58
+ values: formValues,
59
+ validationSchema: null,
60
+ setValidationSchema: (value) => {},
61
+ inEditMode: false,
62
+ setFieldValue: () => {},
63
+ setCapturePhotoProps: (value) => {},
64
+ currentPhoto: '',
65
+ isOffline: false,
66
+ initialFormValues: formValues,
67
+ }}>
68
+ <DobField />
69
+ </PatientRegistrationContext.Provider>
70
+ </Form>
71
+ </Formik>,
72
+ );
73
+ }
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { NameField } from './name/name-field.component';
3
+ import { GenderField } from './gender/gender-field.component';
4
+ import { Identifiers } from './id/id-field.component';
5
+ import { DobField } from './dob/dob.component';
6
+ import { reportError, useConfig } from '@openmrs/esm-framework';
7
+ import { builtInFields, RegistrationConfig } from '../../config-schema';
8
+ import { CustomField } from './custom-field.component';
9
+ import { AddressHierarchy } from './address/address-hierarchy.component';
10
+
11
+ export interface FieldProps {
12
+ name: string;
13
+ }
14
+
15
+ export function Field({ name }: FieldProps) {
16
+ const config = useConfig() as RegistrationConfig;
17
+ if (
18
+ !(builtInFields as ReadonlyArray<string>).includes(name) &&
19
+ !config.fieldDefinitions.some((def) => def.id == name)
20
+ ) {
21
+ reportError(
22
+ `Invalid field name '${name}'. Valid options are '${config.fieldDefinitions
23
+ .map((def) => def.id)
24
+ .concat(builtInFields)
25
+ .join("', '")}'.`,
26
+ );
27
+ return null;
28
+ }
29
+
30
+ switch (name) {
31
+ case 'name':
32
+ return <NameField />;
33
+ case 'gender':
34
+ return <GenderField />;
35
+ case 'dob':
36
+ return <DobField />;
37
+ case 'address':
38
+ return <AddressHierarchy />;
39
+ case 'id':
40
+ return <Identifiers />;
41
+ default:
42
+ return <CustomField name={name} />;
43
+ }
44
+ }
@@ -0,0 +1,35 @@
1
+ import { FetchResponse, openmrsFetch, showToast } from '@openmrs/esm-framework';
2
+ import useSWRImmutable from 'swr/immutable';
3
+ import { ConceptAnswers, ConceptResponse } from '../patient-registration-types';
4
+
5
+ export function useConcept(conceptUuid: string): { data: ConceptResponse; isLoading: boolean } {
6
+ const shouldFetch = typeof conceptUuid === 'string' && conceptUuid !== '';
7
+ const { data, error, isLoading } = useSWRImmutable<FetchResponse<ConceptResponse>, Error>(
8
+ shouldFetch ? `/ws/rest/v1/concept/${conceptUuid}` : null,
9
+ openmrsFetch,
10
+ );
11
+ if (error) {
12
+ showToast({
13
+ title: error.name,
14
+ description: error.message,
15
+ kind: 'error',
16
+ });
17
+ }
18
+ return { data: data?.data, isLoading };
19
+ }
20
+
21
+ export function useConceptAnswers(conceptUuid: string): { data: Array<ConceptAnswers>; isLoading: boolean } {
22
+ const shouldFetch = typeof conceptUuid === 'string' && conceptUuid !== '';
23
+ const { data, error, isLoading } = useSWRImmutable<FetchResponse<ConceptResponse>, Error>(
24
+ shouldFetch ? `/ws/rest/v1/concept/${conceptUuid}` : null,
25
+ openmrsFetch,
26
+ );
27
+ if (error) {
28
+ showToast({
29
+ title: error.name,
30
+ description: error.message,
31
+ kind: 'error',
32
+ });
33
+ }
34
+ return { data: data?.data?.answers, isLoading };
35
+ }