@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,109 @@
1
+ import React, { useCallback, useContext } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { ContentSwitcher, Switch } from '@carbon/react';
4
+ import { useField } from 'formik';
5
+ import { ExtensionSlot, useConfig } from '@openmrs/esm-framework';
6
+ import { Input } from '../../input/basic-input/input/input.component';
7
+ import { PatientRegistrationContext } from '../../patient-registration-context';
8
+ import styles from '../field.scss';
9
+ import { RegistrationConfig } from '../../../config-schema';
10
+
11
+ const containsNoNumbers = /^([^0-9]*)$/;
12
+
13
+ function checkNumber(value: string) {
14
+ if (!containsNoNumbers.test(value)) {
15
+ return 'numberInNameDubious';
16
+ }
17
+
18
+ return undefined;
19
+ }
20
+
21
+ export const NameField = () => {
22
+ const {
23
+ fieldConfigurations: {
24
+ name: { displayCapturePhoto },
25
+ },
26
+ } = useConfig() as RegistrationConfig;
27
+ const { t } = useTranslation();
28
+ const { setCapturePhotoProps, currentPhoto, setFieldValue } = useContext(PatientRegistrationContext);
29
+ const { fieldConfigurations } = useConfig();
30
+ const fieldConfigs = fieldConfigurations?.name;
31
+ const [{ value: unidentified }] = useField('unidentifiedPatient');
32
+ const nameKnown = !unidentified;
33
+
34
+ const onCapturePhoto = useCallback(
35
+ (dataUri: string, photoDateTime: string) => {
36
+ if (setCapturePhotoProps) {
37
+ setCapturePhotoProps({
38
+ imageData: dataUri,
39
+ dateTime: photoDateTime,
40
+ });
41
+ }
42
+ },
43
+ [setCapturePhotoProps],
44
+ );
45
+
46
+ const toggleNameKnown = (e) => {
47
+ if (e.name === 'known') {
48
+ setFieldValue('givenName', '');
49
+ setFieldValue('familyName', '');
50
+ setFieldValue('unidentifiedPatient', false);
51
+ } else {
52
+ setFieldValue('givenName', fieldConfigs.defaultUnknownGivenName);
53
+ setFieldValue('familyName', fieldConfigs.defaultUnknownFamilyName);
54
+ setFieldValue('unidentifiedPatient', true);
55
+ }
56
+ };
57
+
58
+ return (
59
+ <div>
60
+ <h4 className={styles.productiveHeading02Light}>{t('fullNameLabelText', 'Full Name')}</h4>
61
+ <div className={styles.grid}>
62
+ {displayCapturePhoto && (
63
+ <ExtensionSlot
64
+ className={styles.photoExtension}
65
+ extensionSlotName="capture-patient-photo-slot"
66
+ state={{ onCapturePhoto, initialState: currentPhoto }}
67
+ />
68
+ )}
69
+
70
+ <div className={styles.nameField}>
71
+ <div className={styles.dobContentSwitcherLabel}>
72
+ <span className={styles.label01}>{t('patientNameKnown', "Patient's Name is Known?")}</span>
73
+ </div>
74
+ <ContentSwitcher className={styles.contentSwitcher} onChange={toggleNameKnown}>
75
+ <Switch name="known" text={t('yes', 'Yes')} />
76
+ <Switch name="unknown" text={t('no', 'No')} />
77
+ </ContentSwitcher>
78
+ {nameKnown && (
79
+ <>
80
+ <Input
81
+ id="givenName"
82
+ name="givenName"
83
+ labelText={t('givenNameLabelText', 'First Name')}
84
+ checkWarning={checkNumber}
85
+ required
86
+ />
87
+ {fieldConfigs.displayMiddleName && (
88
+ <Input
89
+ id="middleName"
90
+ name="middleName"
91
+ labelText={t('middleNameLabelText', 'Middle Name')}
92
+ light
93
+ checkWarning={checkNumber}
94
+ />
95
+ )}
96
+ <Input
97
+ id="familyName"
98
+ name="familyName"
99
+ labelText={t('familyNameLabelText', 'Family Name')}
100
+ checkWarning={checkNumber}
101
+ required
102
+ />
103
+ </>
104
+ )}
105
+ </div>
106
+ </div>
107
+ </div>
108
+ );
109
+ };
@@ -0,0 +1,185 @@
1
+ import React from 'react';
2
+ import { Field } from 'formik';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { InlineNotification, Layer, Select, SelectItem } from '@carbon/react';
5
+ import { useConfig } from '@openmrs/esm-framework';
6
+ import { ConceptResponse } from '../../patient-registration-types';
7
+ import { FieldDefinition, RegistrationConfig } from '../../../config-schema';
8
+ import { Input } from '../../input/basic-input/input/input.component';
9
+ import { useConcept, useConceptAnswers } from '../field.resource';
10
+ import styles from './../field.scss';
11
+
12
+ export interface ObsFieldProps {
13
+ fieldDefinition: FieldDefinition;
14
+ }
15
+
16
+ export function ObsField({ fieldDefinition }: ObsFieldProps) {
17
+ const { data: concept, isLoading } = useConcept(fieldDefinition.uuid);
18
+ const config = useConfig() as RegistrationConfig;
19
+
20
+ if (!config.registrationObs.encounterTypeUuid) {
21
+ console.error(
22
+ 'The registration form has been configured to have obs fields, ' +
23
+ 'but no registration encounter type has been configured. Obs fields ' +
24
+ 'will not be displayed.',
25
+ );
26
+ return null;
27
+ }
28
+
29
+ if (isLoading) {
30
+ return null;
31
+ }
32
+ switch (concept.datatype.display) {
33
+ case 'Text':
34
+ return (
35
+ <TextObsField
36
+ concept={concept}
37
+ validationRegex={fieldDefinition.validation.matches}
38
+ label={fieldDefinition.label}
39
+ />
40
+ );
41
+ case 'Numeric':
42
+ return <NumericObsField concept={concept} label={fieldDefinition.label} />;
43
+ case 'Coded':
44
+ return (
45
+ <CodedObsField
46
+ concept={concept}
47
+ answerConceptSetUuid={fieldDefinition.answerConceptSetUuid}
48
+ label={fieldDefinition.label}
49
+ />
50
+ );
51
+ default:
52
+ return (
53
+ <InlineNotification kind="error" title="Error">
54
+ Concept has unknown datatype "{concept.datatype.display}"
55
+ </InlineNotification>
56
+ );
57
+ }
58
+ }
59
+
60
+ interface TextObsFieldProps {
61
+ concept: ConceptResponse;
62
+ validationRegex: string;
63
+ label: string;
64
+ }
65
+
66
+ function TextObsField({ concept, validationRegex, label }: TextObsFieldProps) {
67
+ const { t } = useTranslation();
68
+
69
+ const validateInput = (value: string) => {
70
+ if (!value || !validationRegex || validationRegex === '' || typeof validationRegex !== 'string' || value === '') {
71
+ return;
72
+ }
73
+ const regex = new RegExp(validationRegex);
74
+ if (regex.test(value)) {
75
+ return;
76
+ } else {
77
+ return t('invalidInput', 'Invalid Input');
78
+ }
79
+ };
80
+
81
+ const fieldName = `obs.${concept.uuid}`;
82
+ return (
83
+ <div className={`${styles.customField} ${styles.halfWidthInDesktopView}`}>
84
+ <Field name={fieldName} validate={validateInput}>
85
+ {({ field, form: { touched, errors }, meta }) => {
86
+ return (
87
+ <Input
88
+ id={fieldName}
89
+ labelText={label ?? concept.display}
90
+ invalid={errors[fieldName] && touched[fieldName]}
91
+ {...field}
92
+ />
93
+ );
94
+ }}
95
+ </Field>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ interface NumericObsFieldProps {
101
+ concept: ConceptResponse;
102
+ label: string;
103
+ }
104
+
105
+ function NumericObsField({ concept, label }: NumericObsFieldProps) {
106
+ const { t } = useTranslation();
107
+
108
+ const fieldName = `obs.${concept.uuid}`;
109
+
110
+ return (
111
+ <div className={`${styles.customField} ${styles.halfWidthInDesktopView}`}>
112
+ <Field name={fieldName}>
113
+ {({ field, form: { touched, errors }, meta }) => {
114
+ return (
115
+ <Input
116
+ id={fieldName}
117
+ labelText={label ?? concept.display}
118
+ invalid={errors[fieldName] && touched[fieldName]}
119
+ type="number"
120
+ {...field}
121
+ />
122
+ );
123
+ }}
124
+ </Field>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ interface CodedObsFieldProps {
130
+ concept: ConceptResponse;
131
+ answerConceptSetUuid?: string;
132
+ label?: string;
133
+ }
134
+
135
+ function CodedObsField({ concept, answerConceptSetUuid, label }: CodedObsFieldProps) {
136
+ const config = useConfig() as RegistrationConfig;
137
+ const { data: conceptAnswers, isLoading: isLoadingConceptAnswers } = useConceptAnswers(
138
+ answerConceptSetUuid ?? concept.uuid,
139
+ );
140
+
141
+ const fieldName = `obs.${concept.uuid}`;
142
+ const fieldDefinition = config?.fieldDefinitions?.filter((def) => def.type === 'obs' && def.uuid === concept.uuid)[0];
143
+
144
+ return (
145
+ <div className={`${styles.customField} ${styles.halfWidthInDesktopView}`}>
146
+ {!isLoadingConceptAnswers ? (
147
+ <Field name={fieldName}>
148
+ {({ field, form: { touched, errors }, meta }) => {
149
+ if (fieldDefinition?.customConceptAnswers?.length) {
150
+ return (
151
+ <Select
152
+ id={fieldName}
153
+ name={fieldName}
154
+ labelText={label ?? concept?.display}
155
+ light
156
+ invalid={errors[fieldName] && touched[fieldName]}
157
+ {...field}>
158
+ <SelectItem key={`no-answer-select-item-${fieldName}`} value={''} text="" />
159
+ {fieldDefinition?.customConceptAnswers.map((answer) => (
160
+ <SelectItem key={answer.uuid} value={answer.uuid} text={answer.label} />
161
+ ))}
162
+ </Select>
163
+ );
164
+ }
165
+ return (
166
+ <Layer>
167
+ <Select
168
+ id={fieldName}
169
+ name={fieldName}
170
+ labelText={label ?? concept?.display}
171
+ invalid={errors[fieldName] && touched[fieldName]}
172
+ {...field}>
173
+ <SelectItem key={`no-answer-select-item-${fieldName}`} value={''} text="" />
174
+ {conceptAnswers.map((answer) => (
175
+ <SelectItem key={answer.uuid} value={answer.uuid} text={answer.display} />
176
+ ))}
177
+ </Select>
178
+ </Layer>
179
+ );
180
+ }}
181
+ </Field>
182
+ ) : null}
183
+ </div>
184
+ );
185
+ }
@@ -0,0 +1,127 @@
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 { FieldDefinition } from '../../../config-schema';
6
+ import { ObsField } from './obs-field.component';
7
+
8
+ const mockUseConfig = useConfig as jest.Mock;
9
+
10
+ // The UUIDs in this test all refer to ones that are in `__mocks__/field.resource.ts`
11
+ jest.mock('../field.resource');
12
+
13
+ type FieldProps = {
14
+ children: ({ field, form: { touched, errors } }) => React.ReactNode;
15
+ };
16
+ jest.mock('formik', () => ({
17
+ ...(jest.requireActual('formik') as object),
18
+ Field: jest.fn(({ children }: FieldProps) => <>{children({ field: {}, form: { touched: {}, errors: {} } })}</>),
19
+ useField: jest.fn(() => [{ value: null }, {}]),
20
+ }));
21
+
22
+ const textFieldDef: FieldDefinition = {
23
+ id: 'chief-complaint',
24
+ type: 'obs',
25
+ label: '',
26
+ placeholder: '',
27
+ uuid: 'chief-complaint-uuid',
28
+ validation: {
29
+ required: false,
30
+ matches: null,
31
+ },
32
+ answerConceptSetUuid: null,
33
+ customConceptAnswers: [
34
+ {
35
+ uuid: 'concept-uuid',
36
+ label: '',
37
+ },
38
+ ],
39
+ };
40
+
41
+ const numberFieldDef: FieldDefinition = {
42
+ id: 'weight',
43
+ type: 'obs',
44
+ label: '',
45
+ placeholder: '',
46
+ uuid: 'weight-uuid',
47
+ validation: {
48
+ required: false,
49
+ matches: null,
50
+ },
51
+ answerConceptSetUuid: null,
52
+ customConceptAnswers: [
53
+ {
54
+ uuid: 'concept-uuid',
55
+ label: '',
56
+ },
57
+ ],
58
+ };
59
+
60
+ const codedFieldDef: FieldDefinition = {
61
+ id: 'nationality',
62
+ type: 'obs',
63
+ label: '',
64
+ placeholder: '',
65
+ uuid: 'nationality-uuid',
66
+ validation: {
67
+ required: false,
68
+ matches: null,
69
+ },
70
+ answerConceptSetUuid: null,
71
+ customConceptAnswers: [
72
+ {
73
+ uuid: 'concept-uuid',
74
+ label: 'Kenya',
75
+ },
76
+ ],
77
+ };
78
+
79
+ describe('ObsField', () => {
80
+ beforeEach(() => {
81
+ mockUseConfig.mockReturnValue({ registrationObs: { encounterTypeUuid: 'reg-enc-uuid' } });
82
+ });
83
+
84
+ it("logs an error and doesn't render if no registration encounter type is provided", () => {
85
+ mockUseConfig.mockReturnValue({ registrationObs: { encounterTypeUuid: null } });
86
+ console.error = jest.fn();
87
+ render(<ObsField fieldDefinition={textFieldDef} />);
88
+ expect(console.error).toHaveBeenCalledWith(expect.stringMatching(/no registration encounter type.*configure/i));
89
+ expect(screen.queryByRole('textbox')).toBeNull();
90
+ });
91
+
92
+ it('renders a text box for text concept', () => {
93
+ render(<ObsField fieldDefinition={textFieldDef} />);
94
+ // I don't know why the labels aren't in the DOM, but they aren't
95
+ // expect(screen.getByLabelText("Chief Complaint")).toBeInTheDocument();
96
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
97
+ });
98
+
99
+ it('renders a number box for number concept', () => {
100
+ render(<ObsField fieldDefinition={numberFieldDef} />);
101
+ // expect(screen.getByLabelText("Weight (kg)")).toBeInTheDocument();
102
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
103
+ });
104
+
105
+ it('renders a select for a coded concept', () => {
106
+ render(<ObsField fieldDefinition={codedFieldDef} />);
107
+ // expect(screen.getByLabelText("Nationality")).toBeInTheDocument();
108
+ const select = screen.getByRole('combobox');
109
+ expect(select).toBeInTheDocument();
110
+ expect(select).toHaveDisplayValue('');
111
+ });
112
+
113
+ it('select uses answerConcept for answers when it is provided', async () => {
114
+ mockUseConfig.mockReturnValue({
115
+ registrationObs: { encounterTypeUuid: 'reg-enc-uuid' },
116
+ fieldDefinitions: [codedFieldDef],
117
+ });
118
+
119
+ const user = userEvent.setup();
120
+
121
+ render(<ObsField fieldDefinition={{ ...codedFieldDef, answerConceptSetUuid: 'other-countries-uuid' }} />);
122
+ // expect(screen.getByLabelText("Nationality")).toBeInTheDocument();
123
+ const select = screen.getByRole('combobox');
124
+ expect(select).toBeInTheDocument();
125
+ await user.selectOptions(select, 'Kenya');
126
+ });
127
+ });
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+ import { Layer, Select, SelectItem } from '@carbon/react';
3
+ import { useConfig } from '@openmrs/esm-framework';
4
+ import { Input } from '../../input/basic-input/input/input.component';
5
+ import { CodedPersonAttributeConfig } from '../../patient-registration-types';
6
+ import { useConceptAnswers } from '../field.resource';
7
+ import { usePersonAttributeType } from './person-attributes.resource';
8
+ import styles from './../field.scss';
9
+
10
+ export interface CodedAttributesFieldProps {}
11
+
12
+ export const CodedAttributesField: React.FC<CodedAttributesFieldProps> = () => {
13
+ const { codedPersonAttributes } = useConfig();
14
+
15
+ return codedPersonAttributes?.length ? (
16
+ <div>
17
+ {codedPersonAttributes.map((personAttributeType: CodedPersonAttributeConfig, ind) => (
18
+ <PersonAttributeField
19
+ key={ind}
20
+ personAttributeTypeUuid={personAttributeType.personAttributeUuid}
21
+ conceptUuid={personAttributeType.conceptUuid}
22
+ />
23
+ ))}
24
+ </div>
25
+ ) : null;
26
+ };
27
+
28
+ interface PersonAttributeFieldProps {
29
+ personAttributeTypeUuid: string;
30
+ conceptUuid: string;
31
+ }
32
+
33
+ const PersonAttributeField: React.FC<PersonAttributeFieldProps> = ({ personAttributeTypeUuid, conceptUuid }) => {
34
+ const { data: personAttributeType, isLoading } = usePersonAttributeType(personAttributeTypeUuid);
35
+ const { data: conceptAnswers, isLoading: isLoadingConceptAnswers } = useConceptAnswers(conceptUuid);
36
+
37
+ return !isLoading ? (
38
+ <div className={`${styles.attributeField} ${styles.halfWidthInDesktopView}`}>
39
+ {!isLoadingConceptAnswers && conceptAnswers?.length ? (
40
+ <Layer>
41
+ <Select
42
+ id={`person-attribute-${personAttributeTypeUuid}`}
43
+ name={`attributes.${personAttributeTypeUuid}`}
44
+ labelText={personAttributeType?.display}>
45
+ {conceptAnswers.map((answer) => (
46
+ <SelectItem key={answer.uuid} value={answer.uuid} text={answer.display} />
47
+ ))}
48
+ </Select>
49
+ </Layer>
50
+ ) : (
51
+ <Input
52
+ id={`person-attribute-${personAttributeTypeUuid}`}
53
+ labelText={personAttributeType?.display}
54
+ name={`attributes.${personAttributeTypeUuid}`}
55
+ />
56
+ )}
57
+ </div>
58
+ ) : null;
59
+ };
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import { Layer, Select, SelectItem } from '@carbon/react';
3
+ import { Input } from '../../input/basic-input/input/input.component';
4
+ import { PersonAttributeTypeResponse } from '../../patient-registration-types';
5
+ import { useConceptAnswers } from '../field.resource';
6
+ import styles from './../field.scss';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Field } from 'formik';
9
+
10
+ export interface CodedPersonAttributeFieldProps {
11
+ id: string;
12
+ personAttributeType: PersonAttributeTypeResponse;
13
+ answerConceptSetUuid: string;
14
+ label?: string;
15
+ }
16
+
17
+ export function CodedPersonAttributeField({
18
+ id,
19
+ personAttributeType,
20
+ answerConceptSetUuid,
21
+ label,
22
+ }: CodedPersonAttributeFieldProps) {
23
+ const { data: conceptAnswers, isLoading: isLoadingConceptAnswers } = useConceptAnswers(answerConceptSetUuid);
24
+ const { t } = useTranslation();
25
+ const fieldName = `attributes.${personAttributeType.uuid}`;
26
+
27
+ return (
28
+ <div className={`${styles.customField} ${styles.halfWidthInDesktopView}`}>
29
+ {!isLoadingConceptAnswers && conceptAnswers?.length ? (
30
+ <Layer>
31
+ <Field name={fieldName}>
32
+ {({ field, form: { touched, errors }, meta }) => {
33
+ return (
34
+ <Select
35
+ id={id}
36
+ name={`person-attribute-${personAttributeType.uuid}`}
37
+ labelText={label ?? personAttributeType?.display}
38
+ invalid={errors[fieldName] && touched[fieldName]}
39
+ {...field}>
40
+ <SelectItem value={null} text={t('selectAnOption', 'Select an option')} />
41
+ {conceptAnswers.map((answer) => (
42
+ <SelectItem key={answer.uuid} value={answer.uuid} text={answer.display} />
43
+ ))}
44
+ </Select>
45
+ );
46
+ }}
47
+ </Field>
48
+ </Layer>
49
+ ) : (
50
+ <Layer>
51
+ <Field name={fieldName}>
52
+ {({ field, form: { touched, errors }, meta }) => {
53
+ return (
54
+ <Input
55
+ id={id}
56
+ name={`person-attribute-${personAttributeType.uuid}`}
57
+ labelText={label ?? personAttributeType?.display}
58
+ invalid={errors[fieldName] && touched[fieldName]}
59
+ {...field}
60
+ />
61
+ );
62
+ }}
63
+ </Field>
64
+ </Layer>
65
+ )}
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,81 @@
1
+ import React, { useMemo } from 'react';
2
+ import { InlineNotification, TextInputSkeleton, SkeletonText } from '@carbon/react';
3
+ import { 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}
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
+ />
41
+ );
42
+ default:
43
+ return (
44
+ <InlineNotification kind="error" title="Error">
45
+ Patient attribute type has unknown format "{personAttributeType.format}"
46
+ </InlineNotification>
47
+ );
48
+ }
49
+ }, [personAttributeType, fieldDefinition, t]);
50
+
51
+ if (isLoading) {
52
+ return (
53
+ <div>
54
+ {fieldDefinition.showHeading && <h4 className={styles.productiveHeading02Light}>{fieldDefinition?.label}</h4>}
55
+ <TextInputSkeleton />
56
+ </div>
57
+ );
58
+ }
59
+
60
+ if (error) {
61
+ return (
62
+ <div>
63
+ {fieldDefinition.showHeading && <h4 className={styles.productiveHeading02Light}>{fieldDefinition?.label}</h4>}
64
+ <InlineNotification kind="error" title={t('error', 'Error')}>
65
+ {t('unableToFetch', 'Unable to fetch person attribute type - {personattributetype}', {
66
+ personattributetype: fieldDefinition?.label ?? fieldDefinition?.id,
67
+ })}
68
+ </InlineNotification>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ return (
74
+ <div>
75
+ {fieldDefinition.showHeading && (
76
+ <h4 className={styles.productiveHeading02Light}>{fieldDefinition?.label ?? personAttributeType?.display}</h4>
77
+ )}
78
+ {personAttributeField}
79
+ </div>
80
+ );
81
+ }
@@ -0,0 +1,20 @@
1
+ import { FetchResponse, openmrsFetch, showToast } from '@openmrs/esm-framework';
2
+ import useSWRImmutable from 'swr/immutable';
3
+ import { PersonAttributeTypeResponse } from '../../patient-registration-types';
4
+
5
+ export function usePersonAttributeType(personAttributeTypeUuid: string): {
6
+ data: PersonAttributeTypeResponse;
7
+ isLoading: boolean;
8
+ error: any;
9
+ } {
10
+ const { data, error, isLoading } = useSWRImmutable<FetchResponse<PersonAttributeTypeResponse>>(
11
+ `/ws/rest/v1/personattributetype/${personAttributeTypeUuid}`,
12
+ openmrsFetch,
13
+ );
14
+
15
+ return {
16
+ data: data?.data,
17
+ isLoading,
18
+ error,
19
+ };
20
+ }