@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,127 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .productiveHeading02 {
6
+ @include type.type-style('heading-compact-02');
7
+ margin-bottom: 1rem;
8
+ }
9
+
10
+ .productiveHeading02Light {
11
+ @include type.type-style('heading-compact-02');
12
+ margin-bottom: 1rem;
13
+ color: #525252;
14
+ }
15
+
16
+ .label01 {
17
+ @include type.type-style('label-01');
18
+ }
19
+
20
+ .grid {
21
+ display: grid;
22
+ grid-template-columns: 1fr 1fr;
23
+ column-gap: spacing.$spacing-05;
24
+ }
25
+
26
+ .halfWidthInDesktopView {
27
+ width: calc(50% - spacing.$spacing-05);
28
+ }
29
+
30
+ .patientPhoto {
31
+ display: flex;
32
+ justify-content: center;
33
+ }
34
+
35
+ .nameField {
36
+ grid-row: 1;
37
+ grid-column: 1;
38
+ }
39
+
40
+ .nameField > :global(.cds--content-switcher) {
41
+ display: grid;
42
+ grid-template-columns: 1fr 1fr;
43
+ width: max-content;
44
+ justify-content: flex-start;
45
+ }
46
+
47
+ .contentSwitcher {
48
+ margin-bottom: 1rem;
49
+ }
50
+
51
+ .dobField > :global(.cds--content-switcher) {
52
+ display: grid;
53
+ grid-template-columns: 1fr 1fr;
54
+ width: max-content;
55
+ justify-content: flex-start;
56
+ }
57
+
58
+ .photoExtension {
59
+ margin-bottom: 1rem;
60
+ grid-row: 1;
61
+ grid-column: 2;
62
+ justify-self: center;
63
+ }
64
+
65
+ .sexField,
66
+ .dobField {
67
+ margin-bottom: spacing.$spacing-05;
68
+ }
69
+
70
+ .dobContentSwitcherLabel {
71
+ margin-bottom: spacing.$spacing-03;
72
+ }
73
+
74
+ .identifierLabelText {
75
+ display: flex;
76
+ align-items: center;
77
+ }
78
+
79
+ .setIDNumberButton {
80
+ margin-bottom: spacing.$spacing-05;
81
+ }
82
+
83
+ .setIDNumberButton > svg {
84
+ margin-left: spacing.$spacing-03;
85
+ }
86
+
87
+ .customField {
88
+ margin-bottom: spacing.$spacing-05;
89
+ }
90
+
91
+ .attributeField {
92
+ margin-bottom: spacing.$spacing-05;
93
+ }
94
+
95
+ :global(.omrs-breakpoint-lt-desktop) {
96
+ .grid {
97
+ grid-template-columns: 1fr;
98
+ grid-template-rows: auto auto;
99
+ }
100
+ .nameField {
101
+ grid-row: 2;
102
+ grid-column: 1;
103
+ }
104
+ .photoExtension {
105
+ grid-column: 1;
106
+ grid-row: 1;
107
+ justify-self: start;
108
+ }
109
+ .radioButton label {
110
+ height: spacing.$spacing-09 !important;
111
+ }
112
+ .halfWidthInDesktopView {
113
+ width: 100%;
114
+ }
115
+ }
116
+
117
+ .radioFieldError {
118
+ color: #da1e28;
119
+ display: block;
120
+ font-weight: 400;
121
+ max-height: 12.5rem;
122
+ overflow: visible;
123
+ font-size: 0.75rem;
124
+ letter-spacing: 0.32px;
125
+ line-height: 1.34;
126
+ margin: 0.25rem 0 0;
127
+ }
@@ -0,0 +1,49 @@
1
+ import React, { useContext } from 'react';
2
+ import { RadioButton, RadioButtonGroup } from '@carbon/react';
3
+ import styles from '../field.scss';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { PatientRegistrationContext } from '../../patient-registration-context';
6
+ import { useField } from 'formik';
7
+ import { RegistrationConfig } from '../../../config-schema';
8
+ import { useConfig } from '@openmrs/esm-framework';
9
+
10
+ export const GenderField: React.FC = () => {
11
+ const { fieldConfigurations } = useConfig() as RegistrationConfig;
12
+ const { t } = useTranslation();
13
+ const [field, meta] = useField('gender');
14
+ const { setFieldValue } = useContext(PatientRegistrationContext);
15
+ const fieldConfigs = fieldConfigurations?.gender;
16
+
17
+ const setGender = (gender: string) => {
18
+ setFieldValue('gender', gender);
19
+ };
20
+ /**
21
+ * DO NOT REMOVE THIS COMMENT HERE, ADDS TRANSLATION FOR SEX OPTIONS
22
+ * t('male', 'Male')
23
+ * t('female', 'Female')
24
+ * t('other', 'Other')
25
+ * t('unknown', 'Unknown')
26
+ */
27
+
28
+ return (
29
+ <div className={styles.halfWidthInDesktopView}>
30
+ <h4 className={styles.productiveHeading02Light}>{t('sexFieldLabelText', 'Sex')}</h4>
31
+ <div className={styles.sexField}>
32
+ <p className="cds--label">{t('genderLabelText', 'Sex')}</p>
33
+ <RadioButtonGroup name="gender" orientation="vertical" onChange={setGender} valueSelected={field.value}>
34
+ {fieldConfigs.map((option) => (
35
+ <RadioButton
36
+ key={option.label}
37
+ id={option.id}
38
+ value={option.value}
39
+ labelText={t(`${option.label}`, `${option.label}`)}
40
+ />
41
+ ))}
42
+ </RadioButtonGroup>
43
+ {meta.touched && meta.error && (
44
+ <div className={styles.radioFieldError}>{t(meta.error, 'Gender is required')}</div>
45
+ )}
46
+ </div>
47
+ </div>
48
+ );
49
+ };
@@ -0,0 +1,66 @@
1
+ import React, { useContext } from 'react';
2
+ import { render, fireEvent } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+ import '@testing-library/jest-dom';
5
+ import { Formik, Form } from 'formik';
6
+
7
+ import { GenderField } from './gender-field.component';
8
+
9
+ jest.mock('@openmrs/esm-framework', () => ({
10
+ ...(jest.requireActual('@openmrs/esm-framework') as any),
11
+ useConfig: jest.fn(() => ({
12
+ fieldConfigurations: {
13
+ gender: [
14
+ {
15
+ value: 'Male',
16
+ label: 'Male',
17
+ id: 'male',
18
+ },
19
+ ],
20
+ name: {
21
+ displayMiddleName: false,
22
+ unidentifiedPatient: false,
23
+ defaultUnknownGivenName: '',
24
+ defaultUnknownFamilyName: '',
25
+ },
26
+ },
27
+ })),
28
+ }));
29
+
30
+ jest.mock('react', () => ({
31
+ ...(jest.requireActual('react') as any),
32
+ useContext: jest.fn(() => ({
33
+ setFieldValue: jest.fn(),
34
+ })),
35
+ }));
36
+
37
+ jest.mock('formik', () => ({
38
+ ...(jest.requireActual('formik') as any),
39
+ useField: jest.fn(() => [{}, {}]),
40
+ }));
41
+
42
+ describe('GenderField', () => {
43
+ const renderComponent = () => {
44
+ return render(
45
+ <Formik initialValues={{}} onSubmit={null}>
46
+ <Form>
47
+ <GenderField />
48
+ </Form>
49
+ </Formik>,
50
+ );
51
+ };
52
+
53
+ it('renders', () => {
54
+ expect(renderComponent()).not.toBeNull();
55
+ });
56
+
57
+ it('has a label', () => {
58
+ expect(renderComponent().getAllByText('Sex')).toBeTruthy();
59
+ });
60
+
61
+ it('checks an option', () => {
62
+ const component = renderComponent();
63
+ fireEvent.click(component.getByLabelText('Male'));
64
+ expect(component.getByLabelText('Male')).toBeChecked();
65
+ });
66
+ });
@@ -0,0 +1,142 @@
1
+ import React, { useContext, useEffect, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button, SkeletonText } from '@carbon/react';
4
+ import { ArrowRight } from '@carbon/react/icons';
5
+ import { useLayoutType, useConfig, isDesktop, UserHasAccess } from '@openmrs/esm-framework';
6
+ import IdentifierSelectionOverlay from './identifier-selection-overlay';
7
+ import { IdentifierInput } from '../../input/custom-input/identifier/identifier-input.component';
8
+ import { PatientRegistrationContext } from '../../patient-registration-context';
9
+ import {
10
+ FormValues,
11
+ IdentifierSource,
12
+ PatientIdentifierType,
13
+ PatientIdentifierValue,
14
+ } from '../../patient-registration-types';
15
+ import { ResourcesContext } from '../../../offline.resources';
16
+ import styles from '../field.scss';
17
+
18
+ export function setIdentifierSource(
19
+ identifierSource: IdentifierSource,
20
+ identifierValue: string,
21
+ initialValue: string,
22
+ ): {
23
+ identifierValue: string;
24
+ autoGeneration: boolean;
25
+ selectedSource: IdentifierSource;
26
+ } {
27
+ const autoGeneration = identifierSource?.autoGenerationOption?.automaticGenerationEnabled;
28
+ return {
29
+ selectedSource: identifierSource,
30
+ autoGeneration,
31
+ identifierValue: autoGeneration
32
+ ? 'auto-generated'
33
+ : identifierValue !== 'auto-generated'
34
+ ? identifierValue
35
+ : initialValue,
36
+ };
37
+ }
38
+
39
+ export function initializeIdentifier(identifierType: PatientIdentifierType, identifierProps): PatientIdentifierValue {
40
+ return {
41
+ identifierTypeUuid: identifierType.uuid,
42
+ identifierName: identifierType.name,
43
+ preferred: identifierType.isPrimary,
44
+ initialValue: '',
45
+ required: identifierType.isPrimary || identifierType.required,
46
+ ...identifierProps,
47
+ ...setIdentifierSource(
48
+ identifierProps?.selectedSource ?? identifierType.identifierSources?.[0],
49
+ identifierProps?.identifierValue,
50
+ identifierProps?.initialValue ?? '',
51
+ ),
52
+ };
53
+ }
54
+
55
+ export function deleteIdentifierType(identifiers: FormValues['identifiers'], identifierFieldName) {
56
+ return Object.fromEntries(Object.entries(identifiers).filter(([fieldName]) => fieldName !== identifierFieldName));
57
+ }
58
+
59
+ export const Identifiers: React.FC = () => {
60
+ const { identifierTypes } = useContext(ResourcesContext);
61
+ const isLoading = !identifierTypes;
62
+ const { values, setFieldValue, initialFormValues } = useContext(PatientRegistrationContext);
63
+ const { t } = useTranslation();
64
+ const layout = useLayoutType();
65
+ const [showIdentifierOverlay, setShowIdentifierOverlay] = useState(false);
66
+ const config = useConfig();
67
+ const { defaultPatientIdentifierTypes } = config;
68
+
69
+ useEffect(() => {
70
+ // Initialization
71
+ if (identifierTypes) {
72
+ const identifiers = {};
73
+ identifierTypes
74
+ .filter(
75
+ (type) =>
76
+ type.isPrimary ||
77
+ type.required ||
78
+ !!defaultPatientIdentifierTypes?.find(
79
+ (defaultIdentifierTypeUuid) => defaultIdentifierTypeUuid === type.uuid,
80
+ ),
81
+ )
82
+ .filter((type) => !values.identifiers[type.fieldName])
83
+ .forEach((type) => {
84
+ identifiers[type.fieldName] = initializeIdentifier(
85
+ type,
86
+ values.identifiers[type.uuid] ?? initialFormValues.identifiers[type.uuid] ?? {},
87
+ );
88
+ });
89
+ /*
90
+ Identifier value should only be updated if there is any update in the
91
+ identifier values, otherwise, if the below 'if' clause is removed, it will
92
+ fall into an infinite run.
93
+ */
94
+ if (Object.keys(identifiers).length) {
95
+ setFieldValue('identifiers', {
96
+ ...values.identifiers,
97
+ ...identifiers,
98
+ });
99
+ }
100
+ }
101
+ // eslint-disable-next-line react-hooks/exhaustive-deps
102
+ }, [identifierTypes, setFieldValue, defaultPatientIdentifierTypes, values.identifiers, initializeIdentifier]);
103
+
104
+ if (isLoading) {
105
+ return (
106
+ <div className={styles.halfWidthInDesktopView}>
107
+ <div className={styles.identifierLabelText}>
108
+ <h4 className={styles.productiveHeading02Light}>{t('idFieldLabelText', 'Identifiers')}</h4>
109
+ </div>
110
+ <SkeletonText />
111
+ </div>
112
+ );
113
+ }
114
+
115
+ return (
116
+ <div className={styles.halfWidthInDesktopView}>
117
+ <UserHasAccess privilege={['Get Identifier Types', 'Add Patient Identifiers']}>
118
+ <div className={styles.identifierLabelText}>
119
+ <h4 className={styles.productiveHeading02Light}>{t('idFieldLabelText', 'Identifiers')}</h4>
120
+ <Button
121
+ kind="ghost"
122
+ className={styles.setIDNumberButton}
123
+ onClick={() => setShowIdentifierOverlay(true)}
124
+ size={isDesktop(layout) ? 'sm' : 'md'}>
125
+ {t('configure', 'Configure')} <ArrowRight size={16} />
126
+ </Button>
127
+ </div>
128
+ </UserHasAccess>
129
+ <div>
130
+ {Object.entries(values.identifiers).map(([fieldName, identifier]) => (
131
+ <IdentifierInput key={fieldName} fieldName={fieldName} patientIdentifier={identifier} />
132
+ ))}
133
+ {showIdentifierOverlay && (
134
+ <IdentifierSelectionOverlay
135
+ setFieldValue={setFieldValue}
136
+ closeOverlay={() => setShowIdentifierOverlay(false)}
137
+ />
138
+ )}
139
+ </div>
140
+ </div>
141
+ );
142
+ };
@@ -0,0 +1,194 @@
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 { FormValues, PatientIdentifierType, PatientIdentifierValue } from '../../patient-registration-types';
6
+ import Overlay from '../../ui-components/overlay';
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
+ [unsavedIdentifierTypes, searchString],
46
+ );
47
+
48
+ const handleCheckingIdentifier = (identifierType: PatientIdentifierType, checked: boolean) =>
49
+ setUnsavedIdentifierTypes((unsavedIdentifierTypes) => {
50
+ if (checked) {
51
+ return {
52
+ ...unsavedIdentifierTypes,
53
+ [identifierType.fieldName]: initializeIdentifier(
54
+ identifierType,
55
+ values.identifiers[identifierType.fieldName] ??
56
+ initialFormValues.identifiers[identifierType.fieldName] ??
57
+ {},
58
+ ),
59
+ };
60
+ }
61
+ if (unsavedIdentifierTypes[identifierType.fieldName]) {
62
+ return Object.fromEntries(
63
+ Object.entries(unsavedIdentifierTypes).filter(([fieldName]) => fieldName !== identifierType.fieldName),
64
+ );
65
+ }
66
+ return unsavedIdentifierTypes;
67
+ });
68
+
69
+ const handleSelectingIdentifierSource = (identifierType: PatientIdentifierType, sourceUuid) =>
70
+ setUnsavedIdentifierTypes((unsavedIdentifierTypes) => ({
71
+ ...unsavedIdentifierTypes,
72
+ [identifierType.fieldName]: {
73
+ ...unsavedIdentifierTypes[identifierType.fieldName],
74
+ ...setIdentifierSource(
75
+ identifierType.identifierSources.find((source) => source.uuid === sourceUuid),
76
+ unsavedIdentifierTypes[identifierType.fieldName].identifierValue,
77
+ unsavedIdentifierTypes[identifierType.fieldName].initialValue,
78
+ ),
79
+ },
80
+ }));
81
+
82
+ const identifierTypeFields = useMemo(
83
+ () =>
84
+ filteredIdentifiers.map((identifierType) => {
85
+ const patientIdentifier = unsavedIdentifierTypes[identifierType.fieldName];
86
+ const isDisabled =
87
+ identifierType.isPrimary ||
88
+ identifierType.required ||
89
+ defaultPatientIdentifierTypesMap[identifierType.uuid] ||
90
+ // De-selecting shouldn't be allowed if the identifier was selected earlier and is present in the form.
91
+ // 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.
92
+ values.identifiers[identifierType.fieldName];
93
+ const isDisabledOffline = isOffline && shouldBlockPatientIdentifierInOfflineMode(identifierType);
94
+
95
+ return (
96
+ <div key={identifierType.uuid} className={styles.space05}>
97
+ <Checkbox
98
+ id={identifierType.uuid}
99
+ value={identifierType.uuid}
100
+ labelText={identifierType.name}
101
+ onChange={(e, { checked }) => handleCheckingIdentifier(identifierType, checked)}
102
+ checked={!!patientIdentifier}
103
+ disabled={isDisabled || (isOffline && isDisabledOffline)}
104
+ />
105
+ {patientIdentifier &&
106
+ identifierType?.identifierSources?.length > 0 &&
107
+ /*
108
+ This check are for the cases when there's an initialValue identifier is assigned
109
+ to the patient
110
+ The corresponding flow is like:
111
+ 1. If there's no change to the actual initial identifier, then the source remains null,
112
+ hence the list of the identifier sources shouldn't be displayed.
113
+ 2. If user wants to edit the patient identifier's value, hence there will be an initialValue,
114
+ along with a source assigned to itself(only if the identifierType has sources, else there's nothing to worry about), which by
115
+ default is the first identifierSource
116
+ */
117
+ (!patientIdentifier.initialValue || patientIdentifier?.selectedSource) && (
118
+ <div className={styles.radioGroup}>
119
+ <RadioButtonGroup
120
+ legendText={t('source', 'Source')}
121
+ name={`${identifierType?.fieldName}-identifier-sources`}
122
+ defaultSelected={patientIdentifier?.selectedSource?.uuid}
123
+ onChange={(sourceUuid: string) => handleSelectingIdentifierSource(identifierType, sourceUuid)}
124
+ orientation="vertical">
125
+ {identifierType?.identifierSources.map((source) => (
126
+ <RadioButton
127
+ key={source.uuid}
128
+ labelText={source.name}
129
+ name={source.uuid}
130
+ value={source.uuid}
131
+ className={styles.radioButton}
132
+ disabled={
133
+ isOffline &&
134
+ isUniqueIdentifierTypeForOffline(identifierType) &&
135
+ source.autoGenerationOption?.manualEntryEnabled
136
+ }
137
+ />
138
+ ))}
139
+ </RadioButtonGroup>
140
+ </div>
141
+ )}
142
+ </div>
143
+ );
144
+ }),
145
+ [
146
+ filteredIdentifiers,
147
+ unsavedIdentifierTypes,
148
+ defaultPatientIdentifierTypesMap,
149
+ values.identifiers,
150
+ isOffline,
151
+ handleCheckingIdentifier,
152
+ ],
153
+ );
154
+
155
+ const handleConfiguringIdentifiers = useCallback(() => {
156
+ setFieldValue('identifiers', unsavedIdentifierTypes);
157
+ closeOverlay();
158
+ }, [unsavedIdentifierTypes, setFieldValue]);
159
+
160
+ return (
161
+ <Overlay
162
+ close={closeOverlay}
163
+ header={t('configureIdentifiers', 'Configure identifiers')}
164
+ buttonsGroup={
165
+ <ButtonSet className={isDesktop(layout) ? styles.desktop : styles.tablet}>
166
+ <Button className={styles.button} kind="secondary" onClick={closeOverlay}>
167
+ {t('cancel', 'Cancel')}
168
+ </Button>
169
+ <Button className={styles.button} kind="primary" onClick={handleConfiguringIdentifiers}>
170
+ {t('configureIdentifiers', 'Configure identifiers')}
171
+ </Button>
172
+ </ButtonSet>
173
+ }>
174
+ <div>
175
+ <p className={styles.bodyLong02}>
176
+ {t('IDInstructions', "Select the identifiers you'd like to add for this patient:")}
177
+ </p>
178
+ {identifierTypes.length > 7 && (
179
+ <div className={styles.space05}>
180
+ <Search
181
+ labelText={t('searchIdentifierPlaceholder', 'Search identifier')}
182
+ placeholder={t('searchIdentifierPlaceholder', 'Search identifier')}
183
+ onChange={handleSearch}
184
+ value={searchString}
185
+ />
186
+ </div>
187
+ )}
188
+ <fieldset>{identifierTypeFields}</fieldset>
189
+ </div>
190
+ </Overlay>
191
+ );
192
+ };
193
+
194
+ 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
+ }