@olaboot/esm-patient-registration-app 9.2.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 (250) hide show
  1. package/ADDRESS_CONFIGURATION.md +152 -0
  2. package/IDENTIFIER_CONFIGURATION.md +142 -0
  3. package/IMPLEMENTATION_SUMMARY.md +111 -0
  4. package/QUICK_START.md +95 -0
  5. package/README.md +7 -0
  6. package/address-required-fields-config.json +26 -0
  7. package/dist/126.js +1 -0
  8. package/dist/15.js +1 -0
  9. package/dist/1564.js +1 -0
  10. package/dist/1567.js +1 -0
  11. package/dist/1845.js +1 -0
  12. package/dist/1953.js +1 -0
  13. package/dist/200.js +1 -0
  14. package/dist/200.js.map +1 -0
  15. package/dist/215.js +1 -0
  16. package/dist/2178.js +1 -0
  17. package/dist/250.js +1 -0
  18. package/dist/250.js.map +1 -0
  19. package/dist/2523.js +1 -0
  20. package/dist/2523.js.map +1 -0
  21. package/dist/2566.js +1 -0
  22. package/dist/2586.js +1 -0
  23. package/dist/2586.js.map +1 -0
  24. package/dist/2716.js +1 -0
  25. package/dist/2716.js.map +1 -0
  26. package/dist/2759.js +1 -0
  27. package/dist/2821.js +6 -0
  28. package/dist/2821.js.map +1 -0
  29. package/dist/3089.js +1 -0
  30. package/dist/3089.js.map +1 -0
  31. package/dist/3230.js +1 -0
  32. package/dist/3441.js +1 -0
  33. package/dist/3565.js +1 -0
  34. package/dist/3571.js +1 -0
  35. package/dist/3571.js.map +1 -0
  36. package/dist/3746.js +1 -0
  37. package/dist/3925.js +1 -0
  38. package/dist/3946.js +1 -0
  39. package/dist/4024.js +1 -0
  40. package/dist/4024.js.map +1 -0
  41. package/dist/4744.js +1 -0
  42. package/dist/4744.js.map +1 -0
  43. package/dist/4809.js +1 -0
  44. package/dist/4894.js +1 -0
  45. package/dist/4970.js +1 -0
  46. package/dist/4970.js.map +1 -0
  47. package/dist/5130.js +1 -0
  48. package/dist/5187.js +1 -0
  49. package/dist/5491.js +1 -0
  50. package/dist/5491.js.map +1 -0
  51. package/dist/5595.js +1 -0
  52. package/dist/5961.js +1 -0
  53. package/dist/6133.js +1 -0
  54. package/dist/634.js +1 -0
  55. package/dist/634.js.map +1 -0
  56. package/dist/6456.js +1 -0
  57. package/dist/6466.js +1 -0
  58. package/dist/6613.js +1 -0
  59. package/dist/6783.js +1 -0
  60. package/dist/7073.js +38 -0
  61. package/dist/7073.js.map +1 -0
  62. package/dist/7154.js +1 -0
  63. package/dist/7154.js.map +1 -0
  64. package/dist/7348.js +1 -0
  65. package/dist/7439.js +1 -0
  66. package/dist/7439.js.map +1 -0
  67. package/dist/7543.js +1 -0
  68. package/dist/7607.js +1 -0
  69. package/dist/772.js +1 -0
  70. package/dist/7984.js +1 -0
  71. package/dist/7984.js.map +1 -0
  72. package/dist/8538.js +1 -0
  73. package/dist/8538.js.map +1 -0
  74. package/dist/8599.js +1 -0
  75. package/dist/8727.js +1 -0
  76. package/dist/8847.js +1 -0
  77. package/dist/9015.js +1 -0
  78. package/dist/906.js +1 -0
  79. package/dist/9065.js +1 -0
  80. package/dist/9182.js +1 -0
  81. package/dist/9339.js +1 -0
  82. package/dist/9453.js +1 -0
  83. package/dist/9833.js +1 -0
  84. package/dist/9833.js.map +1 -0
  85. package/dist/9856.js +1 -0
  86. package/dist/9856.js.map +1 -0
  87. package/dist/9920.js +1 -0
  88. package/dist/9938.js +1 -0
  89. package/dist/9943.js +1 -0
  90. package/dist/9943.js.map +1 -0
  91. package/dist/main.js +10 -0
  92. package/dist/main.js.map +1 -0
  93. package/dist/olaboot-esm-patient-registration-app.js +5 -0
  94. package/dist/olaboot-esm-patient-registration-app.js.buildmanifest.json +1627 -0
  95. package/dist/olaboot-esm-patient-registration-app.js.map +1 -0
  96. package/dist/routes.json +1 -0
  97. package/docs/images/patient-registration-hierarchy.png +0 -0
  98. package/example-config.json +14 -0
  99. package/jest.config.js +3 -0
  100. package/package.json +60 -0
  101. package/rspack.config.js +1 -0
  102. package/src/add-patient-link.extension.tsx +21 -0
  103. package/src/add-patient-link.scss +3 -0
  104. package/src/add-patient-link.test.tsx +16 -0
  105. package/src/config-schema.ts +507 -0
  106. package/src/constants.ts +14 -0
  107. package/src/declarations.d.ts +4 -0
  108. package/src/index.ts +59 -0
  109. package/src/nav-link.test.tsx +13 -0
  110. package/src/nav-link.tsx +10 -0
  111. package/src/offline.resources.ts +157 -0
  112. package/src/offline.ts +93 -0
  113. package/src/patient-photo.extension.tsx +11 -0
  114. package/src/patient-registration/before-save-prompt.component.tsx +72 -0
  115. package/src/patient-registration/field/__mocks__/field.resource.ts +60 -0
  116. package/src/patient-registration/field/address/address-field.component.tsx +186 -0
  117. package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +71 -0
  118. package/src/patient-registration/field/address/address-hierarchy.resource.tsx +157 -0
  119. package/src/patient-registration/field/address/address-hierarchy.test.tsx +296 -0
  120. package/src/patient-registration/field/address/address-search.component.tsx +87 -0
  121. package/src/patient-registration/field/address/address-search.scss +53 -0
  122. package/src/patient-registration/field/address/address-search.test.tsx +141 -0
  123. package/src/patient-registration/field/address/custom-address-field.component.tsx +32 -0
  124. package/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx +98 -0
  125. package/src/patient-registration/field/custom-field.component.tsx +25 -0
  126. package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx +79 -0
  127. package/src/patient-registration/field/dob/dob.component.tsx +167 -0
  128. package/src/patient-registration/field/dob/dob.test.tsx +90 -0
  129. package/src/patient-registration/field/field.component.tsx +53 -0
  130. package/src/patient-registration/field/field.resource.ts +42 -0
  131. package/src/patient-registration/field/field.scss +171 -0
  132. package/src/patient-registration/field/field.test.tsx +330 -0
  133. package/src/patient-registration/field/gender/gender-field.component.tsx +54 -0
  134. package/src/patient-registration/field/gender/gender-field.test.tsx +99 -0
  135. package/src/patient-registration/field/id/id-field.component.tsx +136 -0
  136. package/src/patient-registration/field/id/id-field.test.tsx +121 -0
  137. package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +200 -0
  138. package/src/patient-registration/field/id/identifier-selection.scss +41 -0
  139. package/src/patient-registration/field/name/name-field.component.tsx +148 -0
  140. package/src/patient-registration/field/obs/obs-field.component.tsx +261 -0
  141. package/src/patient-registration/field/obs/obs-field.test.tsx +299 -0
  142. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +120 -0
  143. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +141 -0
  144. package/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx +105 -0
  145. package/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx +48 -0
  146. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +100 -0
  147. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +193 -0
  148. package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +20 -0
  149. package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +58 -0
  150. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +90 -0
  151. package/src/patient-registration/field/phone/phone-field.component.tsx +17 -0
  152. package/src/patient-registration/form-manager.test.ts +91 -0
  153. package/src/patient-registration/form-manager.ts +443 -0
  154. package/src/patient-registration/input/basic-input/input/input.component.tsx +183 -0
  155. package/src/patient-registration/input/basic-input/input/input.test.tsx +72 -0
  156. package/src/patient-registration/input/basic-input/select/select-input.component.tsx +32 -0
  157. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
  158. package/src/patient-registration/input/combo-input/combo-input.component.tsx +130 -0
  159. package/src/patient-registration/input/combo-input/selection-tick.component.tsx +20 -0
  160. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +187 -0
  161. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +62 -0
  162. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +164 -0
  163. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +193 -0
  164. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +335 -0
  165. package/src/patient-registration/input/custom-input/identifier/utils.test.ts +81 -0
  166. package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
  167. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +56 -0
  168. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +34 -0
  169. package/src/patient-registration/input/input.scss +122 -0
  170. package/src/patient-registration/patient-registration-context.ts +35 -0
  171. package/src/patient-registration/patient-registration-hooks.ts +376 -0
  172. package/src/patient-registration/patient-registration-utils.test.ts +33 -0
  173. package/src/patient-registration/patient-registration-utils.ts +214 -0
  174. package/src/patient-registration/patient-registration.component.tsx +266 -0
  175. package/src/patient-registration/patient-registration.resource.test.tsx +22 -0
  176. package/src/patient-registration/patient-registration.resource.ts +198 -0
  177. package/src/patient-registration/patient-registration.scss +103 -0
  178. package/src/patient-registration/patient-registration.test.tsx +580 -0
  179. package/src/patient-registration/patient-registration.types.ts +322 -0
  180. package/src/patient-registration/section/death-info/death-info-section.component.tsx +36 -0
  181. package/src/patient-registration/section/death-info/death-info-section.test.tsx +47 -0
  182. package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
  183. package/src/patient-registration/section/demographics/demographics-section.test.tsx +98 -0
  184. package/src/patient-registration/section/generic-section.component.tsx +17 -0
  185. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +234 -0
  186. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +113 -0
  187. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +78 -0
  188. package/src/patient-registration/section/patient-relationships/relationships.scss +35 -0
  189. package/src/patient-registration/section/section-wrapper.component.tsx +40 -0
  190. package/src/patient-registration/section/section.component.tsx +23 -0
  191. package/src/patient-registration/section/section.scss +21 -0
  192. package/src/patient-registration/ui-components/overlay/overlay.component.tsx +51 -0
  193. package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
  194. package/src/patient-registration/validation/patient-registration-validation.test.ts +205 -0
  195. package/src/patient-registration/validation/patient-registration-validation.ts +123 -0
  196. package/src/resource.ts +12 -0
  197. package/src/resources-context.ts +14 -0
  198. package/src/root.component.tsx +63 -0
  199. package/src/root.scss +7 -0
  200. package/src/routes.json +61 -0
  201. package/src/widgets/cancel-patient-edit.modal.tsx +33 -0
  202. package/src/widgets/cancel-patient-edit.test.tsx +22 -0
  203. package/src/widgets/delete-identifier-confirmation.modal.tsx +48 -0
  204. package/src/widgets/delete-identifier-confirmation.test.tsx +32 -0
  205. package/src/widgets/edit-patient-details-button.component.tsx +33 -0
  206. package/src/widgets/edit-patient-details-button.scss +3 -0
  207. package/src/widgets/edit-patient-details-button.test.tsx +35 -0
  208. package/translations/am.json +120 -0
  209. package/translations/ar.json +120 -0
  210. package/translations/ar_SY.json +120 -0
  211. package/translations/bn.json +120 -0
  212. package/translations/cs.json +120 -0
  213. package/translations/de.json +120 -0
  214. package/translations/en.json +120 -0
  215. package/translations/en_US.json +120 -0
  216. package/translations/es.json +120 -0
  217. package/translations/es_MX.json +120 -0
  218. package/translations/fr.json +120 -0
  219. package/translations/he.json +120 -0
  220. package/translations/hi.json +120 -0
  221. package/translations/hi_IN.json +120 -0
  222. package/translations/id.json +120 -0
  223. package/translations/it.json +120 -0
  224. package/translations/ka.json +120 -0
  225. package/translations/km.json +120 -0
  226. package/translations/ku.json +120 -0
  227. package/translations/ky.json +120 -0
  228. package/translations/lg.json +120 -0
  229. package/translations/ne.json +120 -0
  230. package/translations/pl.json +120 -0
  231. package/translations/pt.json +120 -0
  232. package/translations/pt_BR.json +120 -0
  233. package/translations/qu.json +120 -0
  234. package/translations/ro_RO.json +120 -0
  235. package/translations/ru_RU.json +120 -0
  236. package/translations/si.json +120 -0
  237. package/translations/sq.json +120 -0
  238. package/translations/sw.json +120 -0
  239. package/translations/sw_KE.json +120 -0
  240. package/translations/tr.json +120 -0
  241. package/translations/tr_TR.json +120 -0
  242. package/translations/uk.json +120 -0
  243. package/translations/uz.json +120 -0
  244. package/translations/uz@Latn.json +120 -0
  245. package/translations/uz_UZ.json +120 -0
  246. package/translations/vi.json +120 -0
  247. package/translations/zh.json +120 -0
  248. package/translations/zh_CN.json +120 -0
  249. package/translations/zh_TW.json +120 -0
  250. package/tsconfig.json +5 -0
@@ -0,0 +1,99 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { Formik, Form } from 'formik';
4
+ import { screen } from '@testing-library/react';
5
+ import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
6
+ import { renderWithContext } from 'tools';
7
+ import { type RegistrationConfig, esmPatientRegistrationSchema } from '../../../config-schema';
8
+ import { type FormValues } from '../../patient-registration.types';
9
+ import {
10
+ type PatientRegistrationContextProps,
11
+ PatientRegistrationContextProvider,
12
+ } from '../../patient-registration-context';
13
+ import { GenderField } from './gender-field.component';
14
+
15
+ const mockUseConfig = jest.mocked(useConfig<RegistrationConfig>);
16
+
17
+ const mockContextValues: PatientRegistrationContextProps = {
18
+ currentPhoto: null,
19
+ identifierTypes: [],
20
+ inEditMode: false,
21
+ initialFormValues: {
22
+ gender: 'male',
23
+ } as FormValues,
24
+ isOffline: false,
25
+ setCapturePhotoProps: jest.fn(),
26
+ setFieldTouched: jest.fn(),
27
+ setFieldValue: jest.fn(),
28
+ validationSchema: esmPatientRegistrationSchema,
29
+ values: {
30
+ gender: 'male',
31
+ } as FormValues,
32
+ };
33
+
34
+ jest.mock('formik', () => ({
35
+ ...(jest.requireActual('formik') as any),
36
+ useField: jest.fn(() => [{}, {}]),
37
+ }));
38
+
39
+ describe('GenderField', () => {
40
+ beforeEach(() => {
41
+ mockUseConfig.mockReturnValue({
42
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
43
+ fieldConfigurations: {
44
+ gender: [
45
+ {
46
+ value: 'male',
47
+ label: 'Male',
48
+ },
49
+ {
50
+ value: 'female',
51
+ label: 'Female',
52
+ },
53
+ ],
54
+ name: {
55
+ displayMiddleName: false,
56
+ allowUnidentifiedPatients: false,
57
+ defaultUnknownGivenName: '',
58
+ defaultUnknownFamilyName: '',
59
+ displayCapturePhoto: false,
60
+ displayReverseFieldOrder: false,
61
+ },
62
+ } as RegistrationConfig['fieldConfigurations'],
63
+ });
64
+ });
65
+
66
+ it('has a label', () => {
67
+ renderWithContext(
68
+ <Formik initialValues={{}} onSubmit={null}>
69
+ <Form>
70
+ <GenderField />
71
+ </Form>
72
+ </Formik>,
73
+ PatientRegistrationContextProvider,
74
+ mockContextValues,
75
+ );
76
+
77
+ expect(screen.getByRole('heading', { name: /sex/i })).toBeInTheDocument();
78
+ expect(screen.getByLabelText(/^male/i)).toBeInTheDocument();
79
+ expect(screen.getByLabelText(/female/i)).toBeInTheDocument();
80
+ });
81
+
82
+ it('checks an option', async () => {
83
+ const user = userEvent.setup();
84
+
85
+ renderWithContext(
86
+ <Formik initialValues={{}} onSubmit={null}>
87
+ <Form>
88
+ <GenderField />
89
+ </Form>
90
+ </Formik>,
91
+ PatientRegistrationContextProvider,
92
+ mockContextValues,
93
+ );
94
+
95
+ await user.click(screen.getByText(/female/i));
96
+ expect(screen.getByLabelText(/female/i)).toBeChecked();
97
+ expect(screen.getByLabelText(/^male/i)).not.toBeChecked();
98
+ });
99
+ });
@@ -0,0 +1,136 @@
1
+ import React, { useEffect } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { SkeletonText } from '@carbon/react';
4
+ import { useConfig } from '@openmrs/esm-framework';
5
+ import { usePatientRegistrationContext } from '../../patient-registration-context';
6
+ import { useResourcesContext } from '../../../resources-context';
7
+ import type {
8
+ FormValues,
9
+ IdentifierSource,
10
+ PatientIdentifierType,
11
+ PatientIdentifierValue,
12
+ } from '../../patient-registration.types';
13
+ import IdentifierInput from '../../input/custom-input/identifier/identifier-input.component';
14
+ import styles from '../field.scss';
15
+
16
+ export function setIdentifierSource(
17
+ identifierSource: IdentifierSource,
18
+ identifierValue: string,
19
+ initialValue: string,
20
+ ): {
21
+ identifierValue: string;
22
+ autoGeneration: boolean;
23
+ selectedSource: IdentifierSource;
24
+ } {
25
+ const autoGeneration = identifierSource?.autoGenerationOption?.automaticGenerationEnabled;
26
+ const manualEntryEnabled = identifierSource?.autoGenerationOption?.manualEntryEnabled;
27
+ return {
28
+ selectedSource: identifierSource,
29
+ autoGeneration,
30
+ identifierValue:
31
+ autoGeneration && !manualEntryEnabled
32
+ ? 'auto-generated'
33
+ : identifierValue !== 'auto-generated'
34
+ ? identifierValue
35
+ : initialValue,
36
+ };
37
+ }
38
+
39
+ export function initializeIdentifier(
40
+ identifierType: PatientIdentifierType,
41
+ identifierProps,
42
+ identifierTypeOverrides?: Array<{ identifierTypeUuid: string; required?: boolean }>,
43
+ ): PatientIdentifierValue {
44
+ // Check if there's an override for this identifier type
45
+ const override = identifierTypeOverrides?.find((override) => override.identifierTypeUuid === identifierType.uuid);
46
+ const isRequired = override?.required !== undefined ? override.required : identifierType.isPrimary || identifierType.required;
47
+
48
+ return {
49
+ identifierTypeUuid: identifierType.uuid,
50
+ identifierName: identifierType.name,
51
+ preferred: identifierType.isPrimary,
52
+ initialValue: '',
53
+ required: isRequired,
54
+ ...identifierProps,
55
+ ...setIdentifierSource(
56
+ identifierProps?.selectedSource ?? identifierType.identifierSources?.[0],
57
+ identifierProps?.identifierValue,
58
+ identifierProps?.initialValue ?? '',
59
+ ),
60
+ };
61
+ }
62
+
63
+ export function deleteIdentifierType(identifiers: FormValues['identifiers'], identifierFieldName) {
64
+ return Object.fromEntries(Object.entries(identifiers).filter(([fieldName]) => fieldName !== identifierFieldName));
65
+ }
66
+
67
+ export const Identifiers: React.FC = () => {
68
+ const { identifierTypes } = useResourcesContext();
69
+ const isLoading = !identifierTypes?.length;
70
+ const { values, setFieldValue, initialFormValues, isOffline } = usePatientRegistrationContext();
71
+ const { t } = useTranslation();
72
+ const config = useConfig();
73
+ const { defaultPatientIdentifierTypes, identifierTypeOverrides } = config;
74
+
75
+ useEffect(() => {
76
+ if (identifierTypes) {
77
+ const identifiers = {};
78
+ identifierTypes
79
+ .filter(
80
+ (type) =>
81
+ type.isPrimary ||
82
+ type.required ||
83
+ !!defaultPatientIdentifierTypes?.find(
84
+ (defaultIdentifierTypeUuid) => defaultIdentifierTypeUuid === type.uuid,
85
+ ),
86
+ )
87
+ .filter((type) => !values.identifiers[type.fieldName])
88
+ .forEach((type) => {
89
+ identifiers[type.fieldName] = initializeIdentifier(
90
+ type,
91
+ values.identifiers[type.uuid] ?? initialFormValues.identifiers[type.uuid] ?? {},
92
+ identifierTypeOverrides,
93
+ );
94
+ });
95
+ /*
96
+ Identifier value should only be updated if there is any update in the
97
+ identifier values, otherwise, if the below 'if' clause is removed, it will
98
+ fall into an infinite run.
99
+ */
100
+ if (Object.keys(identifiers).length) {
101
+ setFieldValue('identifiers', {
102
+ ...values.identifiers,
103
+ ...identifiers,
104
+ });
105
+ }
106
+ }
107
+ // eslint-disable-next-line react-hooks/exhaustive-deps
108
+ }, [identifierTypes, setFieldValue, defaultPatientIdentifierTypes, identifierTypeOverrides, values.identifiers, initializeIdentifier]);
109
+
110
+ if (isLoading && !isOffline) {
111
+ return (
112
+ <div className={styles.halfWidthInDesktopView}>
113
+ <div className={styles.identifierLabelText}>
114
+ <h4 className={styles.productiveHeading02Light}>{t('idFieldLabelText', 'Identifiers')}</h4>
115
+ </div>
116
+ <SkeletonText
117
+ // @ts-expect-error
118
+ role="progressbar"
119
+ />
120
+ </div>
121
+ );
122
+ }
123
+
124
+ return (
125
+ <div className={styles.halfWidthInDesktopView}>
126
+ <div className={styles.identifierLabelText}>
127
+ <h4 className={styles.productiveHeading02Light}>{t('idFieldLabelText', 'Identifiers')}</h4>
128
+ </div>
129
+ <div>
130
+ {Object.entries(values.identifiers).map(([fieldName, identifier]) => (
131
+ <IdentifierInput key={fieldName} fieldName={fieldName} patientIdentifier={identifier} />
132
+ ))}
133
+ </div>
134
+ </div>
135
+ );
136
+ };
@@ -0,0 +1,121 @@
1
+ import React from 'react';
2
+ import { Form, Formik } from 'formik';
3
+ import { screen } from '@testing-library/react';
4
+ import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
5
+ import { type AddressTemplate, type IdentifierSource } from '../../patient-registration.types';
6
+ import { mockIdentifierTypes, mockOpenmrsId, mockPatient, mockSession } from '__mocks__';
7
+ import { renderWithContext } from 'tools';
8
+ import { esmPatientRegistrationSchema, type RegistrationConfig } from '../../../config-schema';
9
+ import { type Resources } from '../../../offline.resources';
10
+ import {
11
+ PatientRegistrationContextProvider,
12
+ type PatientRegistrationContextProps,
13
+ } from '../../patient-registration-context';
14
+ import { Identifiers, setIdentifierSource } from './id-field.component';
15
+ import { ResourcesContextProvider } from '../../../resources-context';
16
+
17
+ const mockUseConfig = jest.mocked(useConfig<RegistrationConfig>);
18
+
19
+ const mockResourcesContextValue = {
20
+ addressTemplate: null as unknown as AddressTemplate,
21
+ currentSession: mockSession.data,
22
+ identifierTypes: [],
23
+ relationshipTypes: [],
24
+ } as Resources;
25
+
26
+ const mockInitialFormValues = {
27
+ additionalFamilyName: '',
28
+ additionalGivenName: '',
29
+ additionalMiddleName: '',
30
+ addNameInLocalLanguage: false,
31
+ address: {},
32
+ birthdate: null,
33
+ birthdateEstimated: false,
34
+ deathCause: '',
35
+ deathDate: '',
36
+ familyName: 'Doe',
37
+ gender: 'male',
38
+ givenName: 'John',
39
+ identifiers: mockOpenmrsId,
40
+ isDead: false,
41
+ middleName: 'Test',
42
+ monthsEstimated: 0,
43
+ patientUuid: mockPatient.uuid,
44
+ relationships: [],
45
+ telephoneNumber: '',
46
+ yearsEstimated: 0,
47
+ };
48
+
49
+ const mockContextValues: PatientRegistrationContextProps = {
50
+ currentPhoto: null,
51
+ inEditMode: false,
52
+ identifierTypes: [],
53
+ initialFormValues: mockInitialFormValues,
54
+ isOffline: false,
55
+ setCapturePhotoProps: jest.fn(),
56
+ setFieldValue: jest.fn(),
57
+ setInitialFormValues: jest.fn(),
58
+ validationSchema: null,
59
+ values: mockInitialFormValues,
60
+ } as unknown as PatientRegistrationContextProps;
61
+
62
+ describe('Identifiers', () => {
63
+ beforeEach(() => {
64
+ mockUseConfig.mockReturnValue({
65
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
66
+ defaultPatientIdentifierTypes: ['OpenMRS ID'],
67
+ });
68
+ });
69
+
70
+ it('should render loading skeleton when identifier types are loading', () => {
71
+ renderWithContext(
72
+ <Formik initialValues={{}} onSubmit={null}>
73
+ <Form>
74
+ <PatientRegistrationContextProvider value={mockContextValues}>
75
+ <Identifiers />
76
+ </PatientRegistrationContextProvider>
77
+ </Form>
78
+ </Formik>,
79
+ ResourcesContextProvider,
80
+ mockResourcesContextValue,
81
+ );
82
+
83
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
84
+ });
85
+
86
+ it('should render identifier inputs when identifier types are loaded', () => {
87
+ mockResourcesContextValue.identifierTypes = mockIdentifierTypes;
88
+
89
+ renderWithContext(
90
+ <Formik initialValues={{}} onSubmit={null}>
91
+ <Form>
92
+ <PatientRegistrationContextProvider value={mockContextValues}>
93
+ <Identifiers />
94
+ </PatientRegistrationContextProvider>
95
+ </Form>
96
+ </Formik>,
97
+ ResourcesContextProvider,
98
+ mockResourcesContextValue,
99
+ );
100
+
101
+ expect(screen.getByText('Identifiers')).toBeInTheDocument();
102
+ });
103
+ });
104
+
105
+ describe('setIdentifierSource', () => {
106
+ describe('auto-generation', () => {
107
+ it('should return auto-generated as the identifier value', () => {
108
+ const identifierSource = { autoGenerationOption: { automaticGenerationEnabled: true } } as IdentifierSource;
109
+ const { identifierValue } = setIdentifierSource(identifierSource, '', '');
110
+ expect(identifierValue).toBe('auto-generated');
111
+ });
112
+
113
+ it('should return the identifier value when manual entry enabled', () => {
114
+ const identifierSource = {
115
+ autoGenerationOption: { automaticGenerationEnabled: true, manualEntryEnabled: true },
116
+ } as IdentifierSource;
117
+ const { identifierValue } = setIdentifierSource(identifierSource, '10001V', '');
118
+ expect(identifierValue).toBe('10001V');
119
+ });
120
+ });
121
+ });
@@ -0,0 +1,200 @@
1
+ import React, { useMemo, useCallback, useEffect, useState } 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 { usePatientRegistrationContext } from '../../patient-registration-context';
7
+ import {
8
+ isUniqueIdentifierTypeForOffline,
9
+ shouldBlockPatientIdentifierInOfflineMode,
10
+ } from '../../input/custom-input/identifier/utils';
11
+ import { initializeIdentifier, setIdentifierSource } from './id-field.component';
12
+ import { useResourcesContext } from '../../../resources-context';
13
+ import Overlay from '../../ui-components/overlay/overlay.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 } = useResourcesContext();
24
+ const { isOffline, values, initialFormValues } = usePatientRegistrationContext();
25
+ const [unsavedIdentifierTypes, setUnsavedIdentifierTypes] = useState<FormValues['identifiers']>(values.identifiers);
26
+ const [searchString, setSearchString] = useState('');
27
+ const { t } = useTranslation();
28
+ const { defaultPatientIdentifierTypes, identifierTypeOverrides } = useConfig();
29
+
30
+ const defaultPatientIdentifierTypesMap = useMemo(() => {
31
+ const map = {};
32
+ defaultPatientIdentifierTypes?.forEach((typeUuid) => {
33
+ map[typeUuid] = true;
34
+ });
35
+ return map;
36
+ }, [defaultPatientIdentifierTypes]);
37
+
38
+ useEffect(() => {
39
+ setUnsavedIdentifierTypes(values.identifiers);
40
+ }, [values.identifiers]);
41
+
42
+ const handleSearch = useCallback((event) => setSearchString(event?.target?.value ?? ''), []);
43
+
44
+ const filteredIdentifiers = useMemo(
45
+ () => identifierTypes?.filter((identifier) => identifier?.name?.toLowerCase().includes(searchString.toLowerCase())),
46
+ [searchString, identifierTypes],
47
+ );
48
+
49
+ const handleCheckingIdentifier = useCallback(
50
+ (identifierType: PatientIdentifierType, checked: boolean) =>
51
+ setUnsavedIdentifierTypes((unsavedIdentifierTypes) => {
52
+ if (checked) {
53
+ return {
54
+ ...unsavedIdentifierTypes,
55
+ [identifierType.fieldName]: initializeIdentifier(
56
+ identifierType,
57
+ values.identifiers[identifierType.fieldName] ??
58
+ initialFormValues.identifiers[identifierType.fieldName] ??
59
+ {},
60
+ identifierTypeOverrides,
61
+ ),
62
+ };
63
+ }
64
+ if (unsavedIdentifierTypes[identifierType.fieldName]) {
65
+ return Object.fromEntries(
66
+ Object.entries(unsavedIdentifierTypes).filter(([fieldName]) => fieldName !== identifierType.fieldName),
67
+ );
68
+ }
69
+ return unsavedIdentifierTypes;
70
+ }),
71
+ [initialFormValues.identifiers, values.identifiers, identifierTypeOverrides],
72
+ );
73
+
74
+ const handleSelectingIdentifierSource = (identifierType: PatientIdentifierType, sourceUuid) =>
75
+ setUnsavedIdentifierTypes((unsavedIdentifierTypes) => ({
76
+ ...unsavedIdentifierTypes,
77
+ [identifierType.fieldName]: {
78
+ ...unsavedIdentifierTypes[identifierType.fieldName],
79
+ ...setIdentifierSource(
80
+ identifierType.identifierSources.find((source) => source.uuid === sourceUuid),
81
+ unsavedIdentifierTypes[identifierType.fieldName].identifierValue,
82
+ unsavedIdentifierTypes[identifierType.fieldName].initialValue,
83
+ ),
84
+ },
85
+ }));
86
+
87
+ const identifierTypeFields = useMemo(
88
+ () =>
89
+ filteredIdentifiers.map((identifierType) => {
90
+ const patientIdentifier = unsavedIdentifierTypes[identifierType.fieldName];
91
+ const isDisabled =
92
+ identifierType.isPrimary ||
93
+ identifierType.required ||
94
+ defaultPatientIdentifierTypesMap[identifierType.uuid] ||
95
+ // De-selecting shouldn't be allowed if the identifier was selected earlier and is present in the form.
96
+ // 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.
97
+ values.identifiers[identifierType.fieldName];
98
+ const isDisabledOffline = isOffline && shouldBlockPatientIdentifierInOfflineMode(identifierType);
99
+
100
+ return (
101
+ <div key={identifierType.uuid} className={styles.space05}>
102
+ <Checkbox
103
+ id={identifierType.uuid}
104
+ value={identifierType.uuid}
105
+ labelText={identifierType.name}
106
+ onChange={(e, { checked }) => handleCheckingIdentifier(identifierType, checked)}
107
+ checked={!!patientIdentifier}
108
+ disabled={isDisabled || (isOffline && isDisabledOffline)}
109
+ />
110
+ {patientIdentifier &&
111
+ identifierType?.identifierSources?.length > 0 &&
112
+ /*
113
+ This check are for the cases when there's an initialValue identifier is assigned
114
+ to the patient
115
+ The corresponding flow is like:
116
+ 1. If there's no change to the actual initial identifier, then the source remains null,
117
+ hence the list of the identifier sources shouldn't be displayed.
118
+ 2. If user wants to edit the patient identifier's value, hence there will be an initialValue,
119
+ along with a source assigned to itself(only if the identifierType has sources, else there's nothing to worry about), which by
120
+ default is the first identifierSource
121
+ */
122
+ (!patientIdentifier.initialValue || patientIdentifier?.selectedSource) && (
123
+ <div className={styles.radioGroup}>
124
+ <RadioButtonGroup
125
+ legendText={t('source', 'Source')}
126
+ name={`${identifierType?.fieldName}-identifier-sources`}
127
+ defaultSelected={patientIdentifier?.selectedSource?.uuid}
128
+ onChange={(sourceUuid: string) => handleSelectingIdentifierSource(identifierType, sourceUuid)}
129
+ orientation="vertical">
130
+ {identifierType?.identifierSources.map((source) => (
131
+ <RadioButton
132
+ key={source.uuid}
133
+ labelText={source.name}
134
+ name={source.uuid}
135
+ value={source.uuid}
136
+ className={styles.radioButton}
137
+ disabled={
138
+ isOffline &&
139
+ isUniqueIdentifierTypeForOffline(identifierType) &&
140
+ source.autoGenerationOption?.manualEntryEnabled
141
+ }
142
+ />
143
+ ))}
144
+ </RadioButtonGroup>
145
+ </div>
146
+ )}
147
+ </div>
148
+ );
149
+ }),
150
+ [
151
+ filteredIdentifiers,
152
+ unsavedIdentifierTypes,
153
+ defaultPatientIdentifierTypesMap,
154
+ values.identifiers,
155
+ isOffline,
156
+ handleCheckingIdentifier,
157
+ t,
158
+ ],
159
+ );
160
+
161
+ const handleConfiguringIdentifiers = useCallback(() => {
162
+ setFieldValue('identifiers', unsavedIdentifierTypes);
163
+ closeOverlay();
164
+ }, [unsavedIdentifierTypes, setFieldValue, closeOverlay]);
165
+
166
+ return (
167
+ <Overlay
168
+ close={closeOverlay}
169
+ header={t('configureIdentifiers', 'Configure identifiers')}
170
+ buttonsGroup={
171
+ <ButtonSet className={isDesktop(layout) ? styles.desktop : styles.tablet}>
172
+ <Button className={styles.button} kind="secondary" onClick={closeOverlay}>
173
+ {t('cancel', 'Cancel')}
174
+ </Button>
175
+ <Button className={styles.button} kind="primary" onClick={handleConfiguringIdentifiers}>
176
+ {t('configureIdentifiers', 'Configure identifiers')}
177
+ </Button>
178
+ </ButtonSet>
179
+ }>
180
+ <div>
181
+ <p className={styles.bodyLong02}>
182
+ {t('IDInstructions', "Select the identifiers you'd like to add for this patient:")}
183
+ </p>
184
+ {identifierTypes.length > 7 && (
185
+ <div className={styles.space05}>
186
+ <Search
187
+ labelText={t('searchIdentifierPlaceholder', 'Search identifier')}
188
+ placeholder={t('searchIdentifierPlaceholder', 'Search identifier')}
189
+ onChange={handleSearch}
190
+ value={searchString}
191
+ />
192
+ </div>
193
+ )}
194
+ <fieldset>{identifierTypeFields}</fieldset>
195
+ </div>
196
+ </Overlay>
197
+ );
198
+ };
199
+
200
+ export default PatientIdentifierOverlay;
@@ -0,0 +1,41 @@
1
+ @use '@carbon/layout';
2
+ @use '@openmrs/esm-styleguide/src/vars' as *;
3
+
4
+ .button {
5
+ height: layout.$spacing-10;
6
+ display: flex;
7
+ align-content: flex-start;
8
+ align-items: baseline;
9
+ min-width: 50%;
10
+ }
11
+
12
+ .tablet {
13
+ padding: layout.$spacing-06 layout.$spacing-05;
14
+ background-color: $ui-02;
15
+ }
16
+
17
+ .desktop {
18
+ padding: 0;
19
+ }
20
+
21
+ .radioGroup {
22
+ background-color: $ui-01;
23
+ padding: layout.$spacing-05;
24
+ }
25
+
26
+ .radioButton {
27
+ margin: 0 !important;
28
+ label {
29
+ height: layout.$spacing-07;
30
+ }
31
+ }
32
+
33
+ .space05 {
34
+ margin: layout.$spacing-05 0;
35
+ }
36
+
37
+ :global(.omrs-breakpoint-lt-desktop) {
38
+ .radioButton label {
39
+ height: layout.$spacing-09 !important;
40
+ }
41
+ }