@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,198 @@
1
+ import React, { useMemo, useCallback, useEffect, useState, useContext } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button, ButtonSet, Checkbox, Search, RadioButtonGroup, RadioButton } from '@carbon/react';
4
+ import { isDesktop, useConfig, useLayoutType } from '@openmrs/esm-framework';
5
+ import { type FormValues, type PatientIdentifierType, PatientIdentifierValue } from '../../patient-registration.types';
6
+ import Overlay from '../../ui-components/overlay/overlay.component';
7
+ import { ResourcesContext } from '../../../offline.resources';
8
+ import { PatientRegistrationContext } from '../../patient-registration-context';
9
+ import {
10
+ isUniqueIdentifierTypeForOffline,
11
+ shouldBlockPatientIdentifierInOfflineMode,
12
+ } from '../../input/custom-input/identifier/utils';
13
+ import { initializeIdentifier, setIdentifierSource } from './id-field.component';
14
+ import styles from './identifier-selection.scss';
15
+
16
+ interface PatientIdentifierOverlayProps {
17
+ setFieldValue: (string, PatientIdentifierValue) => void;
18
+ closeOverlay: () => void;
19
+ }
20
+
21
+ const PatientIdentifierOverlay: React.FC<PatientIdentifierOverlayProps> = ({ closeOverlay, setFieldValue }) => {
22
+ const layout = useLayoutType();
23
+ const { identifierTypes } = useContext(ResourcesContext);
24
+ const { isOffline, values, initialFormValues } = useContext(PatientRegistrationContext);
25
+ const [unsavedIdentifierTypes, setUnsavedIdentifierTypes] = useState<FormValues['identifiers']>(values.identifiers);
26
+ const [searchString, setSearchString] = useState<string>('');
27
+ const { t } = useTranslation();
28
+ const { defaultPatientIdentifierTypes } = useConfig();
29
+ const defaultPatientIdentifierTypesMap = useMemo(() => {
30
+ const map = {};
31
+ defaultPatientIdentifierTypes?.forEach((typeUuid) => {
32
+ map[typeUuid] = true;
33
+ });
34
+ return map;
35
+ }, [defaultPatientIdentifierTypes]);
36
+
37
+ useEffect(() => {
38
+ setUnsavedIdentifierTypes(values.identifiers);
39
+ }, [values.identifiers]);
40
+
41
+ const handleSearch = useCallback((event) => setSearchString(event?.target?.value ?? ''), []);
42
+
43
+ const filteredIdentifiers = useMemo(
44
+ () => identifierTypes?.filter((identifier) => identifier?.name?.toLowerCase().includes(searchString.toLowerCase())),
45
+ [searchString, identifierTypes],
46
+ );
47
+
48
+ const handleCheckingIdentifier = useCallback(
49
+ (identifierType: PatientIdentifierType, checked: boolean) =>
50
+ setUnsavedIdentifierTypes((unsavedIdentifierTypes) => {
51
+ if (checked) {
52
+ return {
53
+ ...unsavedIdentifierTypes,
54
+ [identifierType.fieldName]: initializeIdentifier(
55
+ identifierType,
56
+ values.identifiers[identifierType.fieldName] ??
57
+ initialFormValues.identifiers[identifierType.fieldName] ??
58
+ {},
59
+ ),
60
+ };
61
+ }
62
+ if (unsavedIdentifierTypes[identifierType.fieldName]) {
63
+ return Object.fromEntries(
64
+ Object.entries(unsavedIdentifierTypes).filter(([fieldName]) => fieldName !== identifierType.fieldName),
65
+ );
66
+ }
67
+ return unsavedIdentifierTypes;
68
+ }),
69
+ [initialFormValues.identifiers, values.identifiers],
70
+ );
71
+
72
+ const handleSelectingIdentifierSource = (identifierType: PatientIdentifierType, sourceUuid) =>
73
+ setUnsavedIdentifierTypes((unsavedIdentifierTypes) => ({
74
+ ...unsavedIdentifierTypes,
75
+ [identifierType.fieldName]: {
76
+ ...unsavedIdentifierTypes[identifierType.fieldName],
77
+ ...setIdentifierSource(
78
+ identifierType.identifierSources.find((source) => source.uuid === sourceUuid),
79
+ unsavedIdentifierTypes[identifierType.fieldName].identifierValue,
80
+ unsavedIdentifierTypes[identifierType.fieldName].initialValue,
81
+ ),
82
+ },
83
+ }));
84
+
85
+ const identifierTypeFields = useMemo(
86
+ () =>
87
+ filteredIdentifiers.map((identifierType) => {
88
+ const patientIdentifier = unsavedIdentifierTypes[identifierType.fieldName];
89
+ const isDisabled =
90
+ identifierType.isPrimary ||
91
+ identifierType.required ||
92
+ defaultPatientIdentifierTypesMap[identifierType.uuid] ||
93
+ // De-selecting shouldn't be allowed if the identifier was selected earlier and is present in the form.
94
+ // If the user wants to de-select an identifier-type already present in the form, they'll need to delete the particular identifier from the form itself.
95
+ values.identifiers[identifierType.fieldName];
96
+ const isDisabledOffline = isOffline && shouldBlockPatientIdentifierInOfflineMode(identifierType);
97
+
98
+ return (
99
+ <div key={identifierType.uuid} className={styles.space05}>
100
+ <Checkbox
101
+ id={identifierType.uuid}
102
+ value={identifierType.uuid}
103
+ labelText={identifierType.name}
104
+ onChange={(e, { checked }) => handleCheckingIdentifier(identifierType, checked)}
105
+ checked={!!patientIdentifier}
106
+ disabled={isDisabled || (isOffline && isDisabledOffline)}
107
+ />
108
+ {patientIdentifier &&
109
+ identifierType?.identifierSources?.length > 0 &&
110
+ /*
111
+ This check are for the cases when there's an initialValue identifier is assigned
112
+ to the patient
113
+ The corresponding flow is like:
114
+ 1. If there's no change to the actual initial identifier, then the source remains null,
115
+ hence the list of the identifier sources shouldn't be displayed.
116
+ 2. If user wants to edit the patient identifier's value, hence there will be an initialValue,
117
+ along with a source assigned to itself(only if the identifierType has sources, else there's nothing to worry about), which by
118
+ default is the first identifierSource
119
+ */
120
+ (!patientIdentifier.initialValue || patientIdentifier?.selectedSource) && (
121
+ <div className={styles.radioGroup}>
122
+ <RadioButtonGroup
123
+ legendText={t('source', 'Source')}
124
+ name={`${identifierType?.fieldName}-identifier-sources`}
125
+ defaultSelected={patientIdentifier?.selectedSource?.uuid}
126
+ onChange={(sourceUuid: string) => handleSelectingIdentifierSource(identifierType, sourceUuid)}
127
+ orientation="vertical">
128
+ {identifierType?.identifierSources.map((source) => (
129
+ <RadioButton
130
+ key={source.uuid}
131
+ labelText={source.name}
132
+ name={source.uuid}
133
+ value={source.uuid}
134
+ className={styles.radioButton}
135
+ disabled={
136
+ isOffline &&
137
+ isUniqueIdentifierTypeForOffline(identifierType) &&
138
+ source.autoGenerationOption?.manualEntryEnabled
139
+ }
140
+ />
141
+ ))}
142
+ </RadioButtonGroup>
143
+ </div>
144
+ )}
145
+ </div>
146
+ );
147
+ }),
148
+ [
149
+ filteredIdentifiers,
150
+ unsavedIdentifierTypes,
151
+ defaultPatientIdentifierTypesMap,
152
+ values.identifiers,
153
+ isOffline,
154
+ handleCheckingIdentifier,
155
+ t,
156
+ ],
157
+ );
158
+
159
+ const handleConfiguringIdentifiers = useCallback(() => {
160
+ setFieldValue('identifiers', unsavedIdentifierTypes);
161
+ closeOverlay();
162
+ }, [unsavedIdentifierTypes, setFieldValue, closeOverlay]);
163
+
164
+ return (
165
+ <Overlay
166
+ close={closeOverlay}
167
+ header={t('configureIdentifiers', 'Configure identifiers')}
168
+ buttonsGroup={
169
+ <ButtonSet className={isDesktop(layout) ? styles.desktop : styles.tablet}>
170
+ <Button className={styles.button} kind="secondary" onClick={closeOverlay}>
171
+ {t('cancel', 'Cancel')}
172
+ </Button>
173
+ <Button className={styles.button} kind="primary" onClick={handleConfiguringIdentifiers}>
174
+ {t('configureIdentifiers', 'Configure identifiers')}
175
+ </Button>
176
+ </ButtonSet>
177
+ }>
178
+ <div>
179
+ <p className={styles.bodyLong02}>
180
+ {t('IDInstructions', "Select the identifiers you'd like to add for this patient:")}
181
+ </p>
182
+ {identifierTypes.length > 7 && (
183
+ <div className={styles.space05}>
184
+ <Search
185
+ labelText={t('searchIdentifierPlaceholder', 'Search identifier')}
186
+ placeholder={t('searchIdentifierPlaceholder', 'Search identifier')}
187
+ onChange={handleSearch}
188
+ value={searchString}
189
+ />
190
+ </div>
191
+ )}
192
+ <fieldset>{identifierTypeFields}</fieldset>
193
+ </div>
194
+ </Overlay>
195
+ );
196
+ };
197
+
198
+ export default PatientIdentifierOverlay;
@@ -0,0 +1,37 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @import '../../patient-registration.scss';
3
+
4
+ .button {
5
+ height: 4rem;
6
+ display: flex;
7
+ align-content: flex-start;
8
+ align-items: baseline;
9
+ min-width: 50%;
10
+ }
11
+
12
+ .tablet {
13
+ padding: 1.5rem 1rem;
14
+ background-color: $ui-02;
15
+ }
16
+
17
+ .desktop {
18
+ padding: 0rem;
19
+ }
20
+
21
+ .radioGroup {
22
+ background-color: $ui-01;
23
+ padding: spacing.$spacing-05;
24
+ }
25
+
26
+ .radioButton {
27
+ margin: 0 !important;
28
+ label {
29
+ height: spacing.$spacing-07;
30
+ }
31
+ }
32
+
33
+ :global(.omrs-breakpoint-lt-desktop) {
34
+ .radioButton label {
35
+ height: spacing.$spacing-09 !important;
36
+ }
37
+ }
@@ -0,0 +1,142 @@
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 { type RegistrationConfig } from '../../../config-schema';
10
+
11
+ export const unidentifiedPatientAttributeTypeUuid = '8b56eac7-5c76-4b9c-8c6f-1deab8d3fc47';
12
+ const containsNoNumbers = /^([^0-9]*)$/;
13
+
14
+ function checkNumber(value: string) {
15
+ if (!containsNoNumbers.test(value)) {
16
+ return 'numberInNameDubious';
17
+ }
18
+
19
+ return undefined;
20
+ }
21
+
22
+ export const NameField = () => {
23
+ const { t } = useTranslation();
24
+ const { setCapturePhotoProps, currentPhoto, setFieldValue } = useContext(PatientRegistrationContext);
25
+ const {
26
+ fieldConfigurations: {
27
+ name: {
28
+ displayCapturePhoto,
29
+ allowUnidentifiedPatients,
30
+ defaultUnknownGivenName,
31
+ defaultUnknownFamilyName,
32
+ displayMiddleName,
33
+ displayReverseFieldOrder,
34
+ },
35
+ },
36
+ } = useConfig<RegistrationConfig>();
37
+
38
+ const [{ value: isPatientUnknownValue }, , { setValue: setUnknownPatient }] = useField<string>(
39
+ `attributes.${unidentifiedPatientAttributeTypeUuid}`,
40
+ );
41
+
42
+ const isPatientUnknown = isPatientUnknownValue === 'true';
43
+
44
+ const onCapturePhoto = useCallback(
45
+ (dataUri: string, photoDateTime: string) => {
46
+ if (setCapturePhotoProps) {
47
+ setCapturePhotoProps({
48
+ imageData: dataUri,
49
+ dateTime: photoDateTime,
50
+ });
51
+ }
52
+ },
53
+ [setCapturePhotoProps],
54
+ );
55
+
56
+ const toggleNameKnown = (e) => {
57
+ if (e.name === 'known') {
58
+ setFieldValue('givenName', '');
59
+ setFieldValue('familyName', '');
60
+ setUnknownPatient('false');
61
+ } else {
62
+ setFieldValue('givenName', defaultUnknownGivenName);
63
+ setFieldValue('familyName', defaultUnknownFamilyName);
64
+ setUnknownPatient('true');
65
+ }
66
+ };
67
+
68
+ const firstNameField = (
69
+ <Input
70
+ id="givenName"
71
+ name="givenName"
72
+ labelText={t('givenNameLabelText', 'First Name')}
73
+ checkWarning={checkNumber}
74
+ required
75
+ />
76
+ );
77
+
78
+ const middleNameField = displayMiddleName && (
79
+ <Input
80
+ id="middleName"
81
+ name="middleName"
82
+ labelText={t('middleNameLabelText', 'Middle Name')}
83
+ checkWarning={checkNumber}
84
+ />
85
+ );
86
+
87
+ const familyNameField = (
88
+ <Input
89
+ id="familyName"
90
+ name="familyName"
91
+ labelText={t('familyNameLabelText', 'Family Name')}
92
+ checkWarning={checkNumber}
93
+ required
94
+ />
95
+ );
96
+
97
+ return (
98
+ <div>
99
+ <h4 className={styles.productiveHeading02Light}>{t('fullNameLabelText', 'Full Name')}</h4>
100
+ <div className={styles.grid}>
101
+ {displayCapturePhoto && (
102
+ <ExtensionSlot
103
+ className={styles.photoExtension}
104
+ name="capture-patient-photo-slot"
105
+ state={{ onCapturePhoto, initialState: currentPhoto }}
106
+ />
107
+ )}
108
+
109
+ <div className={styles.nameField}>
110
+ {(allowUnidentifiedPatients || isPatientUnknown) && (
111
+ <>
112
+ <div className={styles.dobContentSwitcherLabel}>
113
+ <span className={styles.label01}>{t('patientNameKnown', "Patient's Name is Known?")}</span>
114
+ </div>
115
+ <ContentSwitcher
116
+ className={styles.contentSwitcher}
117
+ selectedIndex={isPatientUnknown ? 1 : 0}
118
+ onChange={toggleNameKnown}>
119
+ <Switch name="known" text={t('yes', 'Yes')} />
120
+ <Switch name="unknown" text={t('no', 'No')} />
121
+ </ContentSwitcher>
122
+ </>
123
+ )}
124
+ {!isPatientUnknown &&
125
+ (!displayReverseFieldOrder ? (
126
+ <>
127
+ {firstNameField}
128
+ {middleNameField}
129
+ {familyNameField}
130
+ </>
131
+ ) : (
132
+ <>
133
+ {familyNameField}
134
+ {middleNameField}
135
+ {firstNameField}
136
+ </>
137
+ ))}
138
+ </div>
139
+ </div>
140
+ </div>
141
+ );
142
+ };
@@ -0,0 +1,204 @@
1
+ import React, { useMemo } from 'react';
2
+ import classNames from 'classnames';
3
+ import { Field } from 'formik';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { InlineNotification, Layer, Select, SelectItem } from '@carbon/react';
6
+ import { useConfig } from '@openmrs/esm-framework';
7
+ import { type ConceptResponse } from '../../patient-registration.types';
8
+ import { type FieldDefinition, type RegistrationConfig } from '../../../config-schema';
9
+ import { Input } from '../../input/basic-input/input/input.component';
10
+ import { useConcept, useConceptAnswers } from '../field.resource';
11
+ import styles from './../field.scss';
12
+
13
+ export interface ObsFieldProps {
14
+ fieldDefinition: FieldDefinition;
15
+ }
16
+
17
+ export function ObsField({ fieldDefinition }: ObsFieldProps) {
18
+ const { t } = useTranslation();
19
+ const { data: concept, isLoading } = useConcept(fieldDefinition.uuid);
20
+
21
+ const config = useConfig<RegistrationConfig>();
22
+
23
+ if (!config.registrationObs.encounterTypeUuid) {
24
+ console.error(
25
+ 'The registration form has been configured to have obs fields, ' +
26
+ 'but no registration encounter type has been configured. Obs fields ' +
27
+ 'will not be displayed.',
28
+ );
29
+ return null;
30
+ }
31
+
32
+ if (isLoading) {
33
+ return null;
34
+ }
35
+
36
+ switch (concept.datatype.display) {
37
+ case 'Text':
38
+ return (
39
+ <TextObsField
40
+ concept={concept}
41
+ validationRegex={fieldDefinition.validation.matches}
42
+ label={fieldDefinition.label}
43
+ required={fieldDefinition.validation.required}
44
+ />
45
+ );
46
+ case 'Numeric':
47
+ return (
48
+ <NumericObsField
49
+ concept={concept}
50
+ label={fieldDefinition.label}
51
+ required={fieldDefinition.validation.required}
52
+ />
53
+ );
54
+ case 'Coded':
55
+ return (
56
+ <CodedObsField
57
+ concept={concept}
58
+ answerConceptSetUuid={fieldDefinition.answerConceptSetUuid}
59
+ label={fieldDefinition.label}
60
+ required={fieldDefinition.validation.required}
61
+ customConceptAnswers={fieldDefinition.customConceptAnswers}
62
+ />
63
+ );
64
+ default:
65
+ return (
66
+ <InlineNotification kind="error" title="Error">
67
+ {t(
68
+ 'obsFieldUnknownDatatype',
69
+ `Concept for obs field '{{fieldDefinitionId}}' has unknown datatype '{{datatypeName}}'`,
70
+ { fieldDefinitionId: fieldDefinition.id, datatypeName: concept.datatype.display },
71
+ )}
72
+ </InlineNotification>
73
+ );
74
+ }
75
+ }
76
+
77
+ interface TextObsFieldProps {
78
+ concept: ConceptResponse;
79
+ validationRegex: string;
80
+ label: string;
81
+ required?: boolean;
82
+ }
83
+
84
+ function TextObsField({ concept, validationRegex, label, required }: TextObsFieldProps) {
85
+ const { t } = useTranslation();
86
+
87
+ const validateInput = (value: string) => {
88
+ if (!value || !validationRegex || validationRegex === '' || typeof validationRegex !== 'string' || value === '') {
89
+ return;
90
+ }
91
+ const regex = new RegExp(validationRegex);
92
+ if (regex.test(value)) {
93
+ return;
94
+ } else {
95
+ return t('invalidInput', 'Invalid Input');
96
+ }
97
+ };
98
+
99
+ const fieldName = `obs.${concept.uuid}`;
100
+ return (
101
+ <div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
102
+ <Field name={fieldName} validate={validateInput}>
103
+ {({ field, form: { touched, errors }, meta }) => {
104
+ return (
105
+ <Input
106
+ id={fieldName}
107
+ labelText={label ?? concept.display}
108
+ required={required}
109
+ invalid={errors[fieldName] && touched[fieldName]}
110
+ {...field}
111
+ />
112
+ );
113
+ }}
114
+ </Field>
115
+ </div>
116
+ );
117
+ }
118
+
119
+ interface NumericObsFieldProps {
120
+ concept: ConceptResponse;
121
+ label: string;
122
+ required?: boolean;
123
+ }
124
+
125
+ function NumericObsField({ concept, label, required }: NumericObsFieldProps) {
126
+ const fieldName = `obs.${concept.uuid}`;
127
+
128
+ return (
129
+ <div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
130
+ <Field name={fieldName}>
131
+ {({ field, form: { touched, errors }, meta }) => {
132
+ return (
133
+ <Input
134
+ id={fieldName}
135
+ labelText={label ?? concept.display}
136
+ required={required}
137
+ invalid={errors[fieldName] && touched[fieldName]}
138
+ type="number"
139
+ {...field}
140
+ />
141
+ );
142
+ }}
143
+ </Field>
144
+ </div>
145
+ );
146
+ }
147
+
148
+ interface CodedObsFieldProps {
149
+ concept: ConceptResponse;
150
+ answerConceptSetUuid?: string;
151
+ label?: string;
152
+ required?: boolean;
153
+ customConceptAnswers: Array<{ uuid: string; label?: string }>;
154
+ }
155
+
156
+ function CodedObsField({ concept, answerConceptSetUuid, label, required, customConceptAnswers }: CodedObsFieldProps) {
157
+ const { t } = useTranslation();
158
+ const fieldName = `obs.${concept.uuid}`;
159
+
160
+ const { data: conceptAnswers, isLoading: isLoadingConceptAnswers } = useConceptAnswers(
161
+ customConceptAnswers.length ? '' : answerConceptSetUuid ?? concept.uuid,
162
+ );
163
+
164
+ const answers = useMemo(
165
+ () =>
166
+ customConceptAnswers.length
167
+ ? customConceptAnswers
168
+ : isLoadingConceptAnswers
169
+ ? []
170
+ : conceptAnswers.map((answer) => ({ ...answer, label: answer.display })),
171
+ [customConceptAnswers, conceptAnswers, isLoadingConceptAnswers],
172
+ );
173
+
174
+ return (
175
+ <div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
176
+ {!isLoadingConceptAnswers ? (
177
+ <Field name={fieldName}>
178
+ {({ field, form: { touched, errors }, meta }) => {
179
+ return (
180
+ <Layer>
181
+ <Select
182
+ id={fieldName}
183
+ name={fieldName}
184
+ labelText={label ?? concept?.display}
185
+ required={required}
186
+ invalid={errors[fieldName] && touched[fieldName]}
187
+ {...field}>
188
+ <SelectItem
189
+ key={`no-answer-select-item-${fieldName}`}
190
+ value={''}
191
+ text={t('selectAnOption', 'Select an option')}
192
+ />
193
+ {answers.map((answer) => (
194
+ <SelectItem key={answer.uuid} value={answer.uuid} text={answer.label} />
195
+ ))}
196
+ </Select>
197
+ </Layer>
198
+ );
199
+ }}
200
+ </Field>
201
+ ) : null}
202
+ </div>
203
+ );
204
+ }