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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/.turbo/turbo-build.log +41 -0
  2. package/README.md +7 -0
  3. package/dist/130.js +2 -0
  4. package/dist/130.js.LICENSE.txt +3 -0
  5. package/dist/130.js.map +1 -0
  6. package/dist/152.js +1 -0
  7. package/dist/152.js.map +1 -0
  8. package/dist/249.js +2 -0
  9. package/dist/249.js.LICENSE.txt +46 -0
  10. package/dist/249.js.map +1 -0
  11. package/dist/255.js +2 -0
  12. package/dist/255.js.LICENSE.txt +9 -0
  13. package/dist/255.js.map +1 -0
  14. package/dist/271.js +1 -0
  15. package/dist/303.js +1 -0
  16. package/dist/303.js.map +1 -0
  17. package/dist/319.js +1 -0
  18. package/dist/365.js +1 -0
  19. package/dist/365.js.map +1 -0
  20. package/dist/460.js +1 -0
  21. package/dist/525.js +1 -0
  22. package/dist/525.js.map +1 -0
  23. package/dist/537.js +1 -0
  24. package/dist/537.js.map +1 -0
  25. package/dist/574.js +1 -0
  26. package/dist/591.js +2 -0
  27. package/dist/591.js.LICENSE.txt +32 -0
  28. package/dist/591.js.map +1 -0
  29. package/dist/621.js +1 -0
  30. package/dist/621.js.map +1 -0
  31. package/dist/644.js +1 -0
  32. package/dist/729.js +1 -0
  33. package/dist/729.js.map +1 -0
  34. package/dist/735.js +1 -0
  35. package/dist/735.js.map +1 -0
  36. package/dist/757.js +1 -0
  37. package/dist/784.js +2 -0
  38. package/dist/784.js.LICENSE.txt +9 -0
  39. package/dist/784.js.map +1 -0
  40. package/dist/788.js +1 -0
  41. package/dist/807.js +1 -0
  42. package/dist/833.js +1 -0
  43. package/dist/879.js +1 -0
  44. package/dist/879.js.map +1 -0
  45. package/dist/ampath-esm-patient-registration-app.js +1 -0
  46. package/dist/ampath-esm-patient-registration-app.js.buildmanifest.json +649 -0
  47. package/dist/ampath-esm-patient-registration-app.js.map +1 -0
  48. package/dist/main.js +2 -0
  49. package/dist/main.js.LICENSE.txt +56 -0
  50. package/dist/main.js.map +1 -0
  51. package/dist/routes.json +1 -0
  52. package/docs/images/patient-registration-hierarchy.png +0 -0
  53. package/jest.config.js +3 -0
  54. package/package.json +61 -0
  55. package/src/add-patient-link.scss +3 -0
  56. package/src/add-patient-link.test.tsx +20 -0
  57. package/src/add-patient-link.tsx +21 -0
  58. package/src/config-schema.ts +410 -0
  59. package/src/constants.ts +14 -0
  60. package/src/declarations.d.ts +6 -0
  61. package/src/index.ts +71 -0
  62. package/src/nav-link.test.tsx +13 -0
  63. package/src/nav-link.tsx +10 -0
  64. package/src/offline.resources.ts +155 -0
  65. package/src/offline.ts +91 -0
  66. package/src/patient-registration/before-save-prompt.tsx +73 -0
  67. package/src/patient-registration/date-util.ts +52 -0
  68. package/src/patient-registration/field/__mocks__/field.resource.ts +60 -0
  69. package/src/patient-registration/field/address/address-field.component.tsx +153 -0
  70. package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +73 -0
  71. package/src/patient-registration/field/address/address-hierarchy.resource.tsx +157 -0
  72. package/src/patient-registration/field/address/address-search.component.tsx +85 -0
  73. package/src/patient-registration/field/address/address-search.scss +53 -0
  74. package/src/patient-registration/field/address/custom-address-field.component.tsx +31 -0
  75. package/src/patient-registration/field/address/tests/address-hierarchy.test.tsx +214 -0
  76. package/src/patient-registration/field/address/tests/address-search-component.test.tsx +135 -0
  77. package/src/patient-registration/field/custom-field.component.tsx +25 -0
  78. package/src/patient-registration/field/dob/dob.component.tsx +159 -0
  79. package/src/patient-registration/field/dob/dob.test.tsx +75 -0
  80. package/src/patient-registration/field/field.component.tsx +47 -0
  81. package/src/patient-registration/field/field.resource.ts +35 -0
  82. package/src/patient-registration/field/field.scss +127 -0
  83. package/src/patient-registration/field/field.test.tsx +294 -0
  84. package/src/patient-registration/field/gender/gender-field.component.tsx +49 -0
  85. package/src/patient-registration/field/gender/gender-field.test.tsx +59 -0
  86. package/src/patient-registration/field/id/id-field.component.tsx +144 -0
  87. package/src/patient-registration/field/id/id-field.test.tsx +107 -0
  88. package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +198 -0
  89. package/src/patient-registration/field/id/identifier-selection.scss +37 -0
  90. package/src/patient-registration/field/name/name-field.component.tsx +142 -0
  91. package/src/patient-registration/field/obs/obs-field.component.tsx +204 -0
  92. package/src/patient-registration/field/obs/obs-field.test.tsx +205 -0
  93. package/src/patient-registration/field/person-attributes/coded-attributes.component.tsx +60 -0
  94. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +116 -0
  95. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +127 -0
  96. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +88 -0
  97. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +187 -0
  98. package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +20 -0
  99. package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +58 -0
  100. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +88 -0
  101. package/src/patient-registration/field/phone/phone-field.component.tsx +16 -0
  102. package/src/patient-registration/form-manager.test.ts +67 -0
  103. package/src/patient-registration/form-manager.ts +414 -0
  104. package/src/patient-registration/input/basic-input/input/input.component.tsx +179 -0
  105. package/src/patient-registration/input/basic-input/input/input.test.tsx +72 -0
  106. package/src/patient-registration/input/basic-input/select/select-input.component.tsx +32 -0
  107. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
  108. package/src/patient-registration/input/combo-input/combo-input.component.tsx +128 -0
  109. package/src/patient-registration/input/combo-input/selection-tick.component.tsx +20 -0
  110. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +187 -0
  111. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +62 -0
  112. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +132 -0
  113. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +156 -0
  114. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +107 -0
  115. package/src/patient-registration/input/custom-input/identifier/utils.test.ts +81 -0
  116. package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
  117. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +53 -0
  118. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +43 -0
  119. package/src/patient-registration/input/input.scss +118 -0
  120. package/src/patient-registration/patient-registration-context.ts +24 -0
  121. package/src/patient-registration/patient-registration-hooks.ts +287 -0
  122. package/src/patient-registration/patient-registration-utils.ts +216 -0
  123. package/src/patient-registration/patient-registration.component.tsx +240 -0
  124. package/src/patient-registration/patient-registration.resource.test.tsx +26 -0
  125. package/src/patient-registration/patient-registration.resource.ts +250 -0
  126. package/src/patient-registration/patient-registration.scss +122 -0
  127. package/src/patient-registration/patient-registration.test.tsx +471 -0
  128. package/src/patient-registration/patient-registration.types.ts +318 -0
  129. package/src/patient-registration/section/death-info/death-info-section.component.tsx +31 -0
  130. package/src/patient-registration/section/death-info/death-info-section.test.tsx +64 -0
  131. package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
  132. package/src/patient-registration/section/demographics/demographics-section.test.tsx +83 -0
  133. package/src/patient-registration/section/generic-section.component.tsx +17 -0
  134. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +235 -0
  135. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +100 -0
  136. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +78 -0
  137. package/src/patient-registration/section/patient-relationships/relationships.scss +35 -0
  138. package/src/patient-registration/section/section-wrapper.component.tsx +40 -0
  139. package/src/patient-registration/section/section.component.tsx +23 -0
  140. package/src/patient-registration/section/section.scss +1 -0
  141. package/src/patient-registration/ui-components/overlay/overlay.component.tsx +51 -0
  142. package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
  143. package/src/patient-registration/validation/patient-registration-validation.test.tsx +157 -0
  144. package/src/patient-registration/validation/patient-registration-validation.tsx +60 -0
  145. package/src/patient-verification/client-registry-constants.ts +13 -0
  146. package/src/patient-verification/client-registry.component.tsx +66 -0
  147. package/src/patient-verification/client-registry.scss +1 -0
  148. package/src/patient-verification/utils.tsx +56 -0
  149. package/src/patient-verification/verification-modal.scss +20 -0
  150. package/src/patient-verification/verification.component.tsx +48 -0
  151. package/src/resource.ts +12 -0
  152. package/src/root.component.tsx +63 -0
  153. package/src/root.scss +7 -0
  154. package/src/root.test.tsx +32 -0
  155. package/src/routes.json +66 -0
  156. package/src/widgets/cancel-patient-edit.component.tsx +37 -0
  157. package/src/widgets/cancel-patient-edit.test.tsx +27 -0
  158. package/src/widgets/delete-identifier-confirmation-modal.test.tsx +34 -0
  159. package/src/widgets/delete-identifier-confirmation-modal.tsx +41 -0
  160. package/src/widgets/delete-identifier-modal.scss +34 -0
  161. package/src/widgets/display-photo.component.tsx +30 -0
  162. package/src/widgets/display-photo.test.tsx +37 -0
  163. package/src/widgets/edit-patient-details-button.component.tsx +34 -0
  164. package/src/widgets/edit-patient-details-button.scss +3 -0
  165. package/src/widgets/edit-patient-details-button.test.tsx +41 -0
  166. package/translations/am.json +97 -0
  167. package/translations/ar.json +97 -0
  168. package/translations/en.json +103 -0
  169. package/translations/es.json +97 -0
  170. package/translations/fr.json +97 -0
  171. package/translations/he.json +97 -0
  172. package/translations/km.json +97 -0
  173. package/translations/zh.json +89 -0
  174. package/translations/zh_CN.json +89 -0
  175. package/tsconfig.json +5 -0
  176. package/webpack.config.js +1 -0
@@ -0,0 +1,205 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { useConfig } from '@openmrs/esm-framework';
5
+ import { type FieldDefinition } from '../../../config-schema';
6
+ import { useConcept, useConceptAnswers } from '../field.resource';
7
+ import { ObsField } from './obs-field.component';
8
+
9
+ const mockUseConfig = useConfig as jest.Mock;
10
+
11
+ jest.mock('../field.resource'); // Mock the useConceptAnswers hook
12
+
13
+ const mockedUseConcept = useConcept as jest.Mock;
14
+ const mockedUseConceptAnswers = useConceptAnswers as jest.Mock;
15
+
16
+ const useConceptMockImpl = (uuid: string) => {
17
+ let data;
18
+ if (uuid == 'weight-uuid') {
19
+ data = {
20
+ uuid: 'weight-uuid',
21
+ display: 'Weight (kg)',
22
+ datatype: { display: 'Numeric', uuid: 'num' },
23
+ answers: [],
24
+ setMembers: [],
25
+ };
26
+ } else if (uuid == 'chief-complaint-uuid') {
27
+ data = {
28
+ uuid: 'chief-complaint-uuid',
29
+ display: 'Chief Complaint',
30
+ datatype: { display: 'Text', uuid: 'txt' },
31
+ answers: [],
32
+ setMembers: [],
33
+ };
34
+ } else if (uuid == 'nationality-uuid') {
35
+ data = {
36
+ uuid: 'nationality-uuid',
37
+ display: 'Nationality',
38
+ datatype: { display: 'Coded', uuid: 'cdd' },
39
+ answers: [
40
+ { display: 'USA', uuid: 'usa' },
41
+ { display: 'Mexico', uuid: 'mex' },
42
+ ],
43
+ setMembers: [],
44
+ };
45
+ } else {
46
+ throw Error(`Programming error, you probably didn't mean to do this: unknown concept uuid '${uuid}'`);
47
+ }
48
+ return {
49
+ data: data ?? null,
50
+ isLoading: !data,
51
+ };
52
+ };
53
+
54
+ const useConceptAnswersMockImpl = (uuid: string) => {
55
+ if (uuid == 'nationality-uuid') {
56
+ return {
57
+ data: [
58
+ { display: 'USA', uuid: 'usa' },
59
+ { display: 'Mexico', uuid: 'mex' },
60
+ ],
61
+ isLoading: false,
62
+ };
63
+ } else if (uuid == 'other-countries-uuid') {
64
+ return {
65
+ data: [
66
+ { display: 'Kenya', uuid: 'ke' },
67
+ { display: 'Uganda', uuid: 'ug' },
68
+ ],
69
+ isLoading: false,
70
+ };
71
+ } else if (uuid == '') {
72
+ return {
73
+ data: [],
74
+ isLoading: false,
75
+ };
76
+ } else {
77
+ throw Error(`Programming error, you probably didn't mean to do this: unknown concept answer set uuid '${uuid}'`);
78
+ }
79
+ };
80
+
81
+ type FieldProps = {
82
+ children: ({ field, form: { touched, errors } }) => React.ReactNode;
83
+ };
84
+
85
+ jest.mock('formik', () => ({
86
+ ...(jest.requireActual('formik') as object),
87
+ Field: jest.fn(({ children }: FieldProps) => <>{children({ field: {}, form: { touched: {}, errors: {} } })}</>),
88
+ useField: jest.fn(() => [{ value: null }, {}]),
89
+ }));
90
+
91
+ const textFieldDef: FieldDefinition = {
92
+ id: 'chief-complaint',
93
+ type: 'obs',
94
+ label: '',
95
+ placeholder: '',
96
+ showHeading: false,
97
+ uuid: 'chief-complaint-uuid',
98
+ validation: {
99
+ required: false,
100
+ matches: null,
101
+ },
102
+ answerConceptSetUuid: null,
103
+ customConceptAnswers: [],
104
+ };
105
+
106
+ const numberFieldDef: FieldDefinition = {
107
+ id: 'weight',
108
+ type: 'obs',
109
+ label: '',
110
+ placeholder: '',
111
+ showHeading: false,
112
+ uuid: 'weight-uuid',
113
+ validation: {
114
+ required: false,
115
+ matches: null,
116
+ },
117
+ answerConceptSetUuid: null,
118
+ customConceptAnswers: [],
119
+ };
120
+
121
+ const codedFieldDef: FieldDefinition = {
122
+ id: 'nationality',
123
+ type: 'obs',
124
+ label: '',
125
+ placeholder: '',
126
+ showHeading: false,
127
+ uuid: 'nationality-uuid',
128
+ validation: {
129
+ required: false,
130
+ matches: null,
131
+ },
132
+ answerConceptSetUuid: null,
133
+ customConceptAnswers: [],
134
+ };
135
+
136
+ describe('ObsField', () => {
137
+ beforeEach(() => {
138
+ mockUseConfig.mockReturnValue({ registrationObs: { encounterTypeUuid: 'reg-enc-uuid' } });
139
+ mockedUseConcept.mockImplementation(useConceptMockImpl);
140
+ mockedUseConceptAnswers.mockImplementation(useConceptAnswersMockImpl);
141
+ });
142
+
143
+ it("logs an error and doesn't render if no registration encounter type is provided", () => {
144
+ mockUseConfig.mockReturnValue({ registrationObs: { encounterTypeUuid: null } });
145
+ console.error = jest.fn();
146
+ render(<ObsField fieldDefinition={textFieldDef} />);
147
+ expect(console.error).toHaveBeenCalledWith(
148
+ expect.stringMatching(/no registration encounter type has been configured/i),
149
+ );
150
+ expect(screen.queryByRole('textbox')).toBeNull();
151
+ });
152
+
153
+ it('renders a text box for text concept', () => {
154
+ render(<ObsField fieldDefinition={textFieldDef} />);
155
+ // I don't know why the labels aren't in the DOM, but they aren't
156
+ // expect(screen.getByLabelText("Chief Complaint")).toBeInTheDocument();
157
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
158
+ });
159
+
160
+ it('renders a number box for number concept', () => {
161
+ render(<ObsField fieldDefinition={numberFieldDef} />);
162
+ // expect(screen.getByLabelText("Weight (kg)")).toBeInTheDocument();
163
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
164
+ });
165
+
166
+ it('renders a select for a coded concept', () => {
167
+ render(<ObsField fieldDefinition={codedFieldDef} />);
168
+ // expect(screen.getByLabelText("Nationality")).toBeInTheDocument();
169
+ const select = screen.getByRole('combobox');
170
+ expect(select).toBeInTheDocument();
171
+ expect(select).toHaveDisplayValue('Select an option');
172
+ });
173
+
174
+ it('select uses answerConcept for answers when it is provided', async () => {
175
+ const user = userEvent.setup();
176
+
177
+ render(<ObsField fieldDefinition={{ ...codedFieldDef, answerConceptSetUuid: 'other-countries-uuid' }} />);
178
+ // expect(screen.getByLabelText("Nationality")).toBeInTheDocument();
179
+ const select = screen.getByRole('combobox');
180
+ expect(select).toBeInTheDocument();
181
+ await user.selectOptions(select, 'Kenya');
182
+ });
183
+
184
+ it('select uses customConceptAnswers for answers when provided', async () => {
185
+ const user = userEvent.setup();
186
+
187
+ render(
188
+ <ObsField
189
+ fieldDefinition={{
190
+ ...codedFieldDef,
191
+ customConceptAnswers: [
192
+ {
193
+ uuid: 'mozambique-uuid',
194
+ label: 'Mozambique',
195
+ },
196
+ ],
197
+ }}
198
+ />,
199
+ );
200
+ // expect(screen.getByLabelText("Nationality")).toBeInTheDocument();
201
+ const select = screen.getByRole('combobox');
202
+ expect(select).toBeInTheDocument();
203
+ await user.selectOptions(select, 'Mozambique');
204
+ });
205
+ });
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import classNames from 'classnames';
3
+ import { Layer, Select, SelectItem } from '@carbon/react';
4
+ import { useConfig } from '@openmrs/esm-framework';
5
+ import { Input } from '../../input/basic-input/input/input.component';
6
+ import { type CodedPersonAttributeConfig } from '../../patient-registration.types';
7
+ import { useConceptAnswers } from '../field.resource';
8
+ import { usePersonAttributeType } from './person-attributes.resource';
9
+ import styles from './../field.scss';
10
+
11
+ export interface CodedAttributesFieldProps {}
12
+
13
+ export const CodedAttributesField: React.FC<CodedAttributesFieldProps> = () => {
14
+ const { codedPersonAttributes } = useConfig();
15
+
16
+ return codedPersonAttributes?.length ? (
17
+ <div>
18
+ {codedPersonAttributes.map((personAttributeType: CodedPersonAttributeConfig, ind) => (
19
+ <PersonAttributeField
20
+ key={ind}
21
+ personAttributeTypeUuid={personAttributeType.personAttributeUuid}
22
+ conceptUuid={personAttributeType.conceptUuid}
23
+ />
24
+ ))}
25
+ </div>
26
+ ) : null;
27
+ };
28
+
29
+ interface PersonAttributeFieldProps {
30
+ personAttributeTypeUuid: string;
31
+ conceptUuid: string;
32
+ }
33
+
34
+ const PersonAttributeField: React.FC<PersonAttributeFieldProps> = ({ personAttributeTypeUuid, conceptUuid }) => {
35
+ const { data: personAttributeType, isLoading } = usePersonAttributeType(personAttributeTypeUuid);
36
+ const { data: conceptAnswers, isLoading: isLoadingConceptAnswers } = useConceptAnswers(conceptUuid);
37
+
38
+ return !isLoading ? (
39
+ <div className={classNames(styles.attributeField, styles.halfWidthInDesktopView)}>
40
+ {!isLoadingConceptAnswers && conceptAnswers?.length ? (
41
+ <Layer>
42
+ <Select
43
+ id={`person-attribute-${personAttributeTypeUuid}`}
44
+ name={`attributes.${personAttributeTypeUuid}`}
45
+ labelText={personAttributeType?.display}>
46
+ {conceptAnswers.map((answer) => (
47
+ <SelectItem key={answer.uuid} value={answer.uuid} text={answer.display} />
48
+ ))}
49
+ </Select>
50
+ </Layer>
51
+ ) : (
52
+ <Input
53
+ id={`person-attribute-${personAttributeTypeUuid}`}
54
+ labelText={personAttributeType?.display}
55
+ name={`attributes.${personAttributeTypeUuid}`}
56
+ />
57
+ )}
58
+ </div>
59
+ ) : null;
60
+ };
@@ -0,0 +1,116 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Field } from 'formik';
5
+ import { Layer, Select, SelectItem } from '@carbon/react';
6
+ import { type PersonAttributeTypeResponse } from '../../patient-registration.types';
7
+ import { useConceptAnswers } from '../field.resource';
8
+ import styles from './../field.scss';
9
+ import { reportError } from '@openmrs/esm-framework';
10
+
11
+ export interface CodedPersonAttributeFieldProps {
12
+ id: string;
13
+ personAttributeType: PersonAttributeTypeResponse;
14
+ answerConceptSetUuid: string;
15
+ label?: string;
16
+ customConceptAnswers: Array<{ uuid: string; label?: string }>;
17
+ }
18
+
19
+ export function CodedPersonAttributeField({
20
+ id,
21
+ personAttributeType,
22
+ answerConceptSetUuid,
23
+ label,
24
+ customConceptAnswers,
25
+ }: CodedPersonAttributeFieldProps) {
26
+ const { data: conceptAnswers, isLoading: isLoadingConceptAnswers } = useConceptAnswers(
27
+ customConceptAnswers.length ? '' : answerConceptSetUuid,
28
+ );
29
+ const { t } = useTranslation();
30
+ const fieldName = `attributes.${personAttributeType.uuid}`;
31
+ const [error, setError] = useState(false);
32
+
33
+ useEffect(() => {
34
+ if (!answerConceptSetUuid && !customConceptAnswers.length) {
35
+ reportError(
36
+ t(
37
+ 'codedPersonAttributeNoAnswerSet',
38
+ `The person attribute field '{{codedPersonAttributeFieldId}}' is of type 'coded' but has been defined without an answer concept set UUID. The 'answerConceptSetUuid' key is required.`,
39
+ { codedPersonAttributeFieldId: id },
40
+ ),
41
+ );
42
+ setError(true);
43
+ }
44
+ }, [answerConceptSetUuid, customConceptAnswers]);
45
+
46
+ useEffect(() => {
47
+ if (!isLoadingConceptAnswers && !customConceptAnswers.length) {
48
+ if (!conceptAnswers) {
49
+ reportError(
50
+ t(
51
+ 'codedPersonAttributeAnswerSetInvalid',
52
+ `The coded person attribute field '{{codedPersonAttributeFieldId}}' has been defined with an invalid answer concept set UUID '{{answerConceptSetUuid}}'.`,
53
+ { codedPersonAttributeFieldId: id, answerConceptSetUuid },
54
+ ),
55
+ );
56
+ setError(true);
57
+ }
58
+ if (conceptAnswers?.length == 0) {
59
+ reportError(
60
+ t(
61
+ 'codedPersonAttributeAnswerSetEmpty',
62
+ `The coded person attribute field '{{codedPersonAttributeFieldId}}' has been defined with an answer concept set UUID '{{answerConceptSetUuid}}' that does not have any concept answers.`,
63
+ {
64
+ codedPersonAttributeFieldId: id,
65
+ answerConceptSetUuid,
66
+ },
67
+ ),
68
+ );
69
+ setError(true);
70
+ }
71
+ }
72
+ }, [isLoadingConceptAnswers, conceptAnswers, customConceptAnswers]);
73
+
74
+ const answers = useMemo(() => {
75
+ if (customConceptAnswers.length) {
76
+ return customConceptAnswers;
77
+ }
78
+ return isLoadingConceptAnswers || !conceptAnswers
79
+ ? []
80
+ : conceptAnswers
81
+ .map((answer) => ({ ...answer, label: answer.display }))
82
+ .sort((a, b) => a.label.localeCompare(b.label));
83
+ }, [customConceptAnswers, conceptAnswers, isLoadingConceptAnswers]);
84
+
85
+ if (error) {
86
+ return null;
87
+ }
88
+
89
+ return (
90
+ <div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
91
+ {!isLoadingConceptAnswers ? (
92
+ <Layer>
93
+ <Field name={fieldName}>
94
+ {({ field, form: { touched, errors }, meta }) => {
95
+ return (
96
+ <>
97
+ <Select
98
+ id={id}
99
+ name={`person-attribute-${personAttributeType.uuid}`}
100
+ labelText={label ?? personAttributeType?.display}
101
+ invalid={errors[fieldName] && touched[fieldName]}
102
+ {...field}>
103
+ <SelectItem value={''} text={t('selectAnOption', 'Select an option')} />
104
+ {answers.map((answer) => (
105
+ <SelectItem key={answer.uuid} value={answer.uuid} text={answer.label} />
106
+ ))}
107
+ </Select>
108
+ </>
109
+ );
110
+ }}
111
+ </Field>
112
+ </Layer>
113
+ ) : null}
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,127 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { useConceptAnswers } from '../field.resource';
4
+ import { CodedPersonAttributeField } from './coded-person-attribute-field.component';
5
+ import { Form, Formik } from 'formik';
6
+
7
+ jest.mock('formik', () => ({
8
+ ...jest.requireActual('formik'),
9
+ }));
10
+
11
+ jest.mock('../field.resource'); // Mock the useConceptAnswers hook
12
+
13
+ const mockedUseConceptAnswers = useConceptAnswers as jest.Mock;
14
+
15
+ describe('CodedPersonAttributeField', () => {
16
+ const conceptAnswers = [
17
+ { uuid: '1', display: 'Option 1' },
18
+ { uuid: '2', display: 'Option 2' },
19
+ ];
20
+ const personAttributeType = {
21
+ format: 'org.openmrs.Concept',
22
+ display: 'Referred by',
23
+ uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0',
24
+ name: '',
25
+ description: '',
26
+ };
27
+ const answerConceptSetUuid = '6682d17f-0777-45e4-a39b-93f77eb3531c';
28
+
29
+ beforeEach(() => {
30
+ jest.clearAllMocks();
31
+ mockedUseConceptAnswers.mockReturnValue({
32
+ data: conceptAnswers,
33
+ isLoading: false,
34
+ });
35
+ });
36
+
37
+ it('shows error if there is no concept answer set provided', () => {
38
+ expect(() => {
39
+ render(
40
+ <Formik initialValues={{}} onSubmit={() => {}}>
41
+ <Form>
42
+ <CodedPersonAttributeField
43
+ id="attributeId"
44
+ personAttributeType={personAttributeType}
45
+ answerConceptSetUuid={null}
46
+ label={personAttributeType.display}
47
+ customConceptAnswers={[]}
48
+ />
49
+ </Form>
50
+ </Formik>,
51
+ );
52
+ }).toThrow(expect.stringMatching(/has been defined without an answer concept set UUID/i));
53
+ });
54
+
55
+ it('shows error if the concept answer set does not have any concept answers', () => {
56
+ mockedUseConceptAnswers.mockReturnValue({
57
+ data: [],
58
+ isLoading: false,
59
+ });
60
+ expect(() => {
61
+ render(
62
+ <Formik initialValues={{}} onSubmit={() => {}}>
63
+ <Form>
64
+ <CodedPersonAttributeField
65
+ id="attributeId"
66
+ personAttributeType={personAttributeType}
67
+ answerConceptSetUuid={answerConceptSetUuid}
68
+ label={personAttributeType.display}
69
+ customConceptAnswers={[]}
70
+ />
71
+ </Form>
72
+ </Formik>,
73
+ );
74
+ }).toThrow(expect.stringMatching(/does not have any concept answers/i));
75
+ });
76
+
77
+ it('renders the conceptAnswers as select options', () => {
78
+ render(
79
+ <Formik initialValues={{}} onSubmit={() => {}}>
80
+ <Form>
81
+ <CodedPersonAttributeField
82
+ id="attributeId"
83
+ personAttributeType={personAttributeType}
84
+ answerConceptSetUuid={answerConceptSetUuid}
85
+ label={personAttributeType.display}
86
+ customConceptAnswers={[]}
87
+ />
88
+ </Form>
89
+ </Formik>,
90
+ );
91
+
92
+ expect(screen.getByLabelText(/Referred by/i)).toBeInTheDocument();
93
+ expect(screen.getByText(/Option 1/i)).toBeInTheDocument();
94
+ expect(screen.getByText(/Option 2/i)).toBeInTheDocument();
95
+ });
96
+
97
+ it('renders customConceptAnswers as select options when they are provided', () => {
98
+ render(
99
+ <Formik initialValues={{}} onSubmit={() => {}}>
100
+ <Form>
101
+ <CodedPersonAttributeField
102
+ id="attributeId"
103
+ personAttributeType={personAttributeType}
104
+ answerConceptSetUuid={answerConceptSetUuid}
105
+ label={personAttributeType.display}
106
+ customConceptAnswers={[
107
+ {
108
+ uuid: 'A',
109
+ label: 'Special Option A',
110
+ },
111
+ {
112
+ uuid: 'B',
113
+ label: 'Special Option B',
114
+ },
115
+ ]}
116
+ />
117
+ </Form>
118
+ </Formik>,
119
+ );
120
+
121
+ expect(screen.getByLabelText(/Referred by/i)).toBeInTheDocument();
122
+ expect(screen.getByText(/Special Option A/i)).toBeInTheDocument();
123
+ expect(screen.getByText(/Special Option B/i)).toBeInTheDocument();
124
+ expect(screen.queryByText(/Option 1/i)).not.toBeInTheDocument();
125
+ expect(screen.queryByText(/Option 2/i)).not.toBeInTheDocument();
126
+ });
127
+ });
@@ -0,0 +1,88 @@
1
+ import React, { useMemo } from 'react';
2
+ import { InlineNotification, TextInputSkeleton, SkeletonText } from '@carbon/react';
3
+ import { type FieldDefinition } from '../../../config-schema';
4
+ import { CodedPersonAttributeField } from './coded-person-attribute-field.component';
5
+ import { usePersonAttributeType } from './person-attributes.resource';
6
+ import { TextPersonAttributeField } from './text-person-attribute-field.component';
7
+ import { useTranslation } from 'react-i18next';
8
+ import styles from '../field.scss';
9
+
10
+ export interface PersonAttributeFieldProps {
11
+ fieldDefinition: FieldDefinition;
12
+ }
13
+
14
+ export function PersonAttributeField({ fieldDefinition }: PersonAttributeFieldProps) {
15
+ const { data: personAttributeType, isLoading, error } = usePersonAttributeType(fieldDefinition.uuid);
16
+ const { t } = useTranslation();
17
+
18
+ const personAttributeField = useMemo(() => {
19
+ if (!personAttributeType) {
20
+ return null;
21
+ }
22
+ switch (personAttributeType.format) {
23
+ case 'java.lang.String':
24
+ return (
25
+ <TextPersonAttributeField
26
+ personAttributeType={personAttributeType}
27
+ validationRegex={fieldDefinition.validation?.matches ?? ''}
28
+ label={fieldDefinition.label}
29
+ required={fieldDefinition.validation?.required ?? false}
30
+ id={fieldDefinition?.id}
31
+ />
32
+ );
33
+ case 'org.openmrs.Concept':
34
+ return (
35
+ <CodedPersonAttributeField
36
+ personAttributeType={personAttributeType}
37
+ answerConceptSetUuid={fieldDefinition.answerConceptSetUuid}
38
+ label={fieldDefinition.label}
39
+ id={fieldDefinition?.id}
40
+ customConceptAnswers={fieldDefinition.customConceptAnswers ?? []}
41
+ />
42
+ );
43
+ default:
44
+ return (
45
+ <InlineNotification kind="error" title="Error">
46
+ {t(
47
+ 'unknownPatientAttributeType',
48
+ 'Patient attribute type has unknown format {{personAttributeTypeFormat}}',
49
+ {
50
+ personAttributeTypeFormat: personAttributeType.format,
51
+ },
52
+ )}
53
+ </InlineNotification>
54
+ );
55
+ }
56
+ }, [personAttributeType, fieldDefinition, t]);
57
+
58
+ if (isLoading) {
59
+ return (
60
+ <div>
61
+ {fieldDefinition.showHeading && <h4 className={styles.productiveHeading02Light}>{fieldDefinition?.label}</h4>}
62
+ <TextInputSkeleton />
63
+ </div>
64
+ );
65
+ }
66
+
67
+ if (error) {
68
+ return (
69
+ <div>
70
+ {fieldDefinition.showHeading && <h4 className={styles.productiveHeading02Light}>{fieldDefinition?.label}</h4>}
71
+ <InlineNotification kind="error" title={t('error', 'Error')}>
72
+ {t('unableToFetch', 'Unable to fetch person attribute type - {{personattributetype}}', {
73
+ personattributetype: fieldDefinition?.label ?? fieldDefinition?.id,
74
+ })}
75
+ </InlineNotification>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ return (
81
+ <div>
82
+ {fieldDefinition.showHeading && (
83
+ <h4 className={styles.productiveHeading02Light}>{fieldDefinition?.label ?? personAttributeType?.display}</h4>
84
+ )}
85
+ {personAttributeField}
86
+ </div>
87
+ );
88
+ }