@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,226 @@
1
+ import React, { useCallback, useContext, useEffect, useState } from 'react';
2
+ import {
3
+ Button,
4
+ Layer,
5
+ Select,
6
+ SelectItem,
7
+ InlineNotification,
8
+ NotificationActionButton,
9
+ SkeletonText,
10
+ } from '@carbon/react';
11
+ import { TrashCan } from '@carbon/react/icons';
12
+ import { FieldArray } from 'formik';
13
+ import { useTranslation } from 'react-i18next';
14
+ import { Autosuggest } from '../../input/custom-input/autosuggest/autosuggest.component';
15
+ import { PatientRegistrationContext } from '../../patient-registration-context';
16
+ import { ResourcesContext } from '../../../offline.resources';
17
+ import { fetchPerson } from '../../patient-registration.resource';
18
+ import { RelationshipValue } from '../../patient-registration-types';
19
+ import sectionStyles from '../section.scss';
20
+ import styles from './relationships.scss';
21
+
22
+ interface RelationshipType {
23
+ display: string;
24
+ uuid: string;
25
+ direction: string;
26
+ }
27
+
28
+ export const RelationshipsSection = () => {
29
+ const { relationshipTypes } = useContext(ResourcesContext);
30
+ const [displayRelationshipTypes, setDisplayRelationshipTypes] = useState<RelationshipType[]>([]);
31
+ const { t } = useTranslation();
32
+
33
+ useEffect(() => {
34
+ if (relationshipTypes) {
35
+ const tmp: RelationshipType[] = [];
36
+ relationshipTypes.results.forEach((type) => {
37
+ const aIsToB = {
38
+ display: type.aIsToB,
39
+ uuid: type.uuid,
40
+ direction: 'aIsToB',
41
+ };
42
+ const bIsToA = {
43
+ display: type.bIsToA,
44
+ uuid: type.uuid,
45
+ direction: 'bIsToA',
46
+ };
47
+ aIsToB.display === bIsToA.display ? tmp.push(aIsToB) : tmp.push(aIsToB, bIsToA);
48
+ });
49
+ setDisplayRelationshipTypes(tmp);
50
+ }
51
+ }, [relationshipTypes]);
52
+
53
+ if (!relationshipTypes) {
54
+ return (
55
+ <section aria-label="Relationships Section">
56
+ <SkeletonText />
57
+ </section>
58
+ );
59
+ }
60
+
61
+ return (
62
+ <section aria-label="Relationships Section">
63
+ <FieldArray name="relationships">
64
+ {({
65
+ push,
66
+ remove,
67
+ form: {
68
+ values: { relationships },
69
+ },
70
+ }) => (
71
+ <div>
72
+ {relationships && relationships.length > 0
73
+ ? relationships.map((relationship: RelationshipValue, index) => (
74
+ <div key={index} className={sectionStyles.formSection}>
75
+ <RelationshipView
76
+ relationship={relationship}
77
+ index={index}
78
+ displayRelationshipTypes={displayRelationshipTypes}
79
+ key={index}
80
+ remove={remove}
81
+ />
82
+ </div>
83
+ ))
84
+ : null}
85
+ <div className={styles.actions}>
86
+ <Button
87
+ kind="ghost"
88
+ onClick={() =>
89
+ push({
90
+ relatedPersonUuid: '',
91
+ action: 'ADD',
92
+ })
93
+ }>
94
+ {t('addRelationshipButtonText', 'Add Relationship')}
95
+ </Button>
96
+ </div>
97
+ </div>
98
+ )}
99
+ </FieldArray>
100
+ </section>
101
+ );
102
+ };
103
+
104
+ interface RelationshipViewProps {
105
+ relationship: RelationshipValue;
106
+ index: number;
107
+ displayRelationshipTypes: RelationshipType[];
108
+ remove: <T>(index: number) => T;
109
+ }
110
+
111
+ const RelationshipView: React.FC<RelationshipViewProps> = ({
112
+ relationship,
113
+ index,
114
+ displayRelationshipTypes,
115
+ remove,
116
+ }) => {
117
+ const { t } = useTranslation();
118
+ const { setFieldValue } = React.useContext(PatientRegistrationContext);
119
+
120
+ const newRelationship = !relationship.uuid;
121
+
122
+ const handleRelationshipTypeChange = useCallback((event) => {
123
+ const { target } = event;
124
+ const field = target.name;
125
+ const value = target.options[target.selectedIndex].value;
126
+ setFieldValue(field, value);
127
+ if (!relationship?.action) {
128
+ setFieldValue(`relationships[${index}].action`, 'UPDATE');
129
+ }
130
+ }, []);
131
+
132
+ const handleSuggestionSelected = useCallback((field: string, selectedSuggestion: string) => {
133
+ setFieldValue(field, selectedSuggestion);
134
+ }, []);
135
+
136
+ const searchPerson = async (query: string) => {
137
+ const abortController = new AbortController();
138
+ const searchResults = await fetchPerson(query);
139
+ return searchResults.data.results;
140
+ };
141
+
142
+ const deleteRelationship = useCallback(() => {
143
+ if (relationship.action === 'ADD') {
144
+ remove(index);
145
+ } else {
146
+ setFieldValue(`relationships[${index}].action`, 'DELETE');
147
+ }
148
+ }, [relationship, index]);
149
+
150
+ const restoreRelationship = useCallback(() => {
151
+ setFieldValue(`relationships[${index}]`, {
152
+ ...relationship,
153
+ action: undefined,
154
+ relationshipType: relationship.initialrelationshipTypeValue,
155
+ });
156
+ }, [index]);
157
+
158
+ return relationship.action !== 'DELETE' ? (
159
+ <div className={styles.relationship}>
160
+ <div className={styles.searchBox}>
161
+ <div className={styles.relationshipHeader}>
162
+ <h4 className={styles.productiveHeading}>
163
+ {relationship?.relation ?? t('relationshipPlaceholder', 'Relationship')}
164
+ </h4>
165
+ <Button
166
+ kind="ghost"
167
+ iconDescription={t('deleteRelationshipTooltipText', 'Delete')}
168
+ hasIconOnly
169
+ onClick={deleteRelationship}>
170
+ <TrashCan size={16} className={styles.trashCan} />
171
+ </Button>
172
+ </div>
173
+ <div>
174
+ {newRelationship ? (
175
+ <Autosuggest
176
+ name={`relationships[${index}].relatedPersonUuid`}
177
+ labelText={t('relativeFullNameLabelText', 'Full name')}
178
+ placeholder={t('relativeNamePlaceholder', 'Firstname Familyname')}
179
+ defaultValue={relationship.relatedPersonName}
180
+ onSuggestionSelected={handleSuggestionSelected}
181
+ getSearchResults={searchPerson}
182
+ getDisplayValue={(item) => item.display}
183
+ getFieldValue={(item) => item.uuid}
184
+ required
185
+ />
186
+ ) : (
187
+ <>
188
+ <span className={styles.labelText}>{t('relativeFullNameLabelText', 'Full name')}</span>
189
+ <p className={styles.bodyShort02}>{relationship.relatedPersonName}</p>
190
+ </>
191
+ )}
192
+ </div>
193
+ </div>
194
+ <div className={`${styles.selectRelationshipType}`} style={{ marginBottom: '1rem' }}>
195
+ <Layer>
196
+ <Select
197
+ id="select"
198
+ labelText={t('relationship', 'Relationship')}
199
+ onChange={handleRelationshipTypeChange}
200
+ name={`relationships[${index}].relationshipType`}
201
+ defaultValue={relationship?.relationshipType ?? 'placeholder-item'}>
202
+ <SelectItem
203
+ disabled
204
+ hidden
205
+ value="placeholder-item"
206
+ text={t('relationshipToPatient', 'Relationship to patient')}
207
+ />
208
+ {displayRelationshipTypes.map((type) => (
209
+ <SelectItem text={type.display} value={`${type.uuid}/${type.direction}`} key={type.display} />
210
+ ))}
211
+ </Select>
212
+ </Layer>
213
+ </div>
214
+ </div>
215
+ ) : (
216
+ <InlineNotification
217
+ kind="info"
218
+ title={t('relationshipRemovedText', 'Relationship removed')}
219
+ actions={
220
+ <NotificationActionButton onClick={restoreRelationship}>
221
+ {t('restoreRelationshipActionButton', 'Undo')}
222
+ </NotificationActionButton>
223
+ }
224
+ />
225
+ );
226
+ };
@@ -0,0 +1,78 @@
1
+ import { FetchResponse, openmrsFetch, showToast } from '@openmrs/esm-framework';
2
+ import { RelationshipValue } from '../../patient-registration-types';
3
+ import useSWR from 'swr';
4
+ import { useMemo } from 'react';
5
+ import { personRelationshipRepresentation } from '../../../constants';
6
+
7
+ export function useInitialPatientRelationships(patientUuid: string): {
8
+ data: Array<RelationshipValue>;
9
+ isLoading: boolean;
10
+ } {
11
+ const shouldFetch = !!patientUuid;
12
+ const { data, error, isLoading } = useSWR<FetchResponse<RelationshipsResponse>, Error>(
13
+ shouldFetch ? `/ws/rest/v1/relationship?v=${personRelationshipRepresentation}&person=${patientUuid}` : null,
14
+ openmrsFetch,
15
+ );
16
+
17
+ const result = useMemo(() => {
18
+ const relationships: Array<RelationshipValue> | undefined = data?.data?.results.map((r) =>
19
+ r.personA.uuid === patientUuid
20
+ ? {
21
+ relatedPersonName: r.personB.display,
22
+ relatedPersonUuid: r.personB.uuid,
23
+ relation: r.relationshipType.bIsToA,
24
+ relationshipType: `${r.relationshipType.uuid}/bIsToA`,
25
+ /**
26
+ * Value kept for restoring initial value
27
+ */
28
+ initialrelationshipTypeValue: `${r.relationshipType.uuid}/bIsToA`,
29
+ uuid: r.uuid,
30
+ }
31
+ : {
32
+ relatedPersonName: r.personA.display,
33
+ relatedPersonUuid: r.personA.uuid,
34
+ relation: r.relationshipType.aIsToB,
35
+ relationshipType: `${r.relationshipType.uuid}/aIsToB`,
36
+ /**
37
+ * Value kept for restoring initial value
38
+ */
39
+ initialrelationshipTypeValue: `${r.relationshipType.uuid}/aIsToB`,
40
+ uuid: r.uuid,
41
+ },
42
+ );
43
+ return {
44
+ data: relationships,
45
+ error,
46
+ isLoading,
47
+ };
48
+ }, [patientUuid, data, error]);
49
+
50
+ return result;
51
+ }
52
+
53
+ export interface Relationship {
54
+ display: string;
55
+ uuid: string;
56
+ personA: {
57
+ age: number;
58
+ display: string;
59
+ birthdate: string;
60
+ uuid: string;
61
+ };
62
+ personB: {
63
+ age: number;
64
+ display: string;
65
+ birthdate: string;
66
+ uuid: string;
67
+ };
68
+ relationshipType: {
69
+ uuid: string;
70
+ display: string;
71
+ aIsToB: string;
72
+ bIsToA: string;
73
+ };
74
+ }
75
+
76
+ interface RelationshipsResponse {
77
+ results: Array<Relationship>;
78
+ }
@@ -0,0 +1,35 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @import '../../patient-registration.scss';
4
+
5
+ .labelText {
6
+ @include type.type-style('label-01');
7
+ }
8
+
9
+ .bodyShort02 {
10
+ @include type.type-style('body-compact-02');
11
+ }
12
+
13
+ .searchBox {
14
+ margin-bottom: spacing.$spacing-05;
15
+ }
16
+
17
+ .relationshipHeader {
18
+ display: flex;
19
+ align-items: center;
20
+ }
21
+
22
+ .productiveHeading {
23
+ @include type.type-style('heading-compact-02');
24
+ color: $text-02;
25
+ }
26
+
27
+ .trashCan {
28
+ color: $danger !important;
29
+ }
30
+
31
+ :global(.omrs-breakpoint-lt-desktop) {
32
+ .relationshipHeader {
33
+ justify-content: space-between;
34
+ }
35
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import styles from '../patient-registration.scss';
3
+ import { Tile } from '@carbon/react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { SectionDefinition } from '../../config-schema';
6
+ import { Section } from './section.component';
7
+
8
+ export interface SectionWrapperProps {
9
+ sectionDefinition: SectionDefinition;
10
+ index: number;
11
+ }
12
+
13
+ export const SectionWrapper = ({ sectionDefinition, index }: SectionWrapperProps) => {
14
+ const { t } = useTranslation();
15
+
16
+ /*
17
+ * This comment exists to provide translation keys for the default section names.
18
+ *
19
+ * DO NOT REMOVE THESE UNLESS A DEFAULT SECTION IS REMOVED
20
+ * t('demographicsSection', 'Basic Info')
21
+ * t('contactSection', 'Contact Details')
22
+ * t('deathSection', 'Death Info')
23
+ * t('relationshipsSection', 'Relationships')
24
+ */
25
+ return (
26
+ <div id={sectionDefinition.id}>
27
+ <h3 className={styles.productiveHeading02} style={{ color: '#161616' }}>
28
+ {index + 1}. {t(`${sectionDefinition.id}Section`, sectionDefinition.name)}
29
+ </h3>
30
+ <span className={styles.label01}>
31
+ {t('allFieldsRequiredText', 'All fields are required unless marked optional')}
32
+ </span>
33
+ <div style={{ margin: '1rem 0 1rem' }}>
34
+ <Tile>
35
+ <Section sectionDefinition={sectionDefinition} />
36
+ </Tile>
37
+ </div>
38
+ </div>
39
+ );
40
+ };
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { SectionDefinition } from '../../config-schema';
3
+ import { GenericSection } from './generic-section.component';
4
+ import { DeathInfoSection } from './death-info/death-info-section.component';
5
+ import { DemographicsSection } from './demographics/demographics-section.component';
6
+ import { RelationshipsSection } from './patient-relationships/relationships-section.component';
7
+
8
+ export interface SectionProps {
9
+ sectionDefinition: SectionDefinition;
10
+ }
11
+
12
+ export function Section({ sectionDefinition }: SectionProps) {
13
+ switch (sectionDefinition.id) {
14
+ case 'demographics':
15
+ return <DemographicsSection fields={sectionDefinition.fields} />;
16
+ case 'death':
17
+ return <DeathInfoSection />;
18
+ case 'relationships':
19
+ return <RelationshipsSection />;
20
+ default: // includes 'contact'
21
+ return <GenericSection sectionDefinition={sectionDefinition} />;
22
+ }
23
+ }
@@ -0,0 +1 @@
1
+ @import '../patient-registration.scss';
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button, Header } from '@carbon/react';
4
+ import { ArrowLeft, Close } from '@carbon/react/icons';
5
+ import { useLayoutType, isDesktop } from '@openmrs/esm-framework';
6
+ import styles from './overlay.scss';
7
+
8
+ interface OverlayProps {
9
+ close: () => void;
10
+ header: string;
11
+ buttonsGroup?: React.ReactElement;
12
+ children?: React.ReactNode;
13
+ }
14
+
15
+ const Overlay: React.FC<OverlayProps> = ({ close, children, header, buttonsGroup }) => {
16
+ const { t } = useTranslation();
17
+ const layout = useLayoutType();
18
+
19
+ return (
20
+ <div className={isDesktop(layout) ? styles.desktopOverlay : styles.tabletOverlay}>
21
+ {isDesktop(layout) ? (
22
+ <div className={styles.desktopHeader}>
23
+ <div className={styles.headerContent}>{header}</div>
24
+ <Button
25
+ className={styles.closeButton}
26
+ iconDescription={t('closeOverlay', 'Close overlay')}
27
+ onClick={close}
28
+ kind="ghost"
29
+ hasIconOnly
30
+ renderIcon={(props) => <Close size={16} {...props} />}
31
+ />
32
+ </div>
33
+ ) : (
34
+ <Header className={styles.tabletOverlayHeader}>
35
+ <Button
36
+ kind="ghost"
37
+ onClick={close}
38
+ hasIconOnly
39
+ iconDescription={t('closeOverlay', 'Close overlay')}
40
+ renderIcon={(props) => <ArrowLeft size={16} onClick={close} {...props} />}
41
+ />
42
+ <div className={styles.headerContent}>{header}</div>
43
+ </Header>
44
+ )}
45
+ <div className={styles.overlayContent}>{children}</div>
46
+ <div>{buttonsGroup}</div>
47
+ </div>
48
+ );
49
+ };
50
+
51
+ export default Overlay;
@@ -0,0 +1,63 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @import '../../patient-registration.scss';
4
+
5
+ .desktopOverlay {
6
+ position: fixed;
7
+ top: spacing.$spacing-09;
8
+ right: 0;
9
+ height: calc(100vh - 3rem);
10
+ min-width: 27rem;
11
+ background-color: $ui-02;
12
+ border-left: 1px solid $text-03;
13
+ overflow: hidden;
14
+ display: grid;
15
+ grid-template-rows: auto 1fr auto;
16
+ z-index: 999;
17
+ }
18
+
19
+ .tabletOverlay {
20
+ position: fixed;
21
+ top: 0;
22
+ bottom: 0;
23
+ left: 0;
24
+ right: 0;
25
+ z-index: 9999;
26
+ background-color: $ui-02;
27
+ overflow: hidden;
28
+ padding-top: spacing.$spacing-09;
29
+ display: grid;
30
+ grid-template-rows: 1fr auto;
31
+ }
32
+
33
+ .tabletOverlayHeader {
34
+ button {
35
+ background-color: $brand-01 !important;
36
+ }
37
+ .headerContent {
38
+ color: $ui-02;
39
+ }
40
+ }
41
+
42
+ .desktopHeader {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ align-items: center;
46
+ background-color: $ui-03;
47
+ border-bottom: 1px solid $text-03;
48
+ }
49
+
50
+ .headerContent {
51
+ @include type.type-style('heading-compact-02');
52
+ padding: 0 spacing.$spacing-05;
53
+ color: $ui-05;
54
+ }
55
+
56
+ .closeButton {
57
+ background-color: $ui-02;
58
+ }
59
+
60
+ .overlayContent {
61
+ padding: spacing.$spacing-05;
62
+ overflow-y: auto;
63
+ }
@@ -0,0 +1,129 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { Formik, Form } from 'formik';
5
+ import { useConfig } from '@openmrs/esm-framework';
6
+ import { validationSchema } from './patient-registration-validation';
7
+ import { NameField } from '../field/name/name-field.component';
8
+ import { PatientRegistrationContext } from '../patient-registration-context';
9
+ import { initialFormValues } from '../patient-registration.component';
10
+ import { FormValues } from '../patient-registration-types';
11
+
12
+ const mockUseConfig = useConfig as jest.Mock;
13
+ const mockFieldConfigs = {
14
+ fieldConfigurations: {
15
+ name: {
16
+ displayMiddleName: true,
17
+ },
18
+ },
19
+ };
20
+
21
+ jest.mock('@openmrs/esm-framework', () => {
22
+ const originalModule = jest.requireActual('@openmrs/esm-framework');
23
+
24
+ return {
25
+ ...originalModule,
26
+ useConfig: jest.fn(),
27
+ validator: jest.fn(),
28
+ };
29
+ });
30
+
31
+ describe('Name input', () => {
32
+ const formValues: FormValues = initialFormValues;
33
+
34
+ const testValidName = (givenNameValue: string, middleNameValue: string, familyNameValue: string) => {
35
+ it(
36
+ 'does not display error message when givenNameValue: ' +
37
+ givenNameValue +
38
+ ', middleNameValue: ' +
39
+ middleNameValue +
40
+ ', familyNameValue: ' +
41
+ familyNameValue,
42
+ async () => {
43
+ const error = await updateNameAndReturnError(givenNameValue, middleNameValue, familyNameValue);
44
+ Object.values(error).map((currentError) => expect(currentError).toBeNull());
45
+ },
46
+ );
47
+ };
48
+
49
+ const testInvalidName = (
50
+ givenNameValue: string,
51
+ middleNameValue: string,
52
+ familyNameValue: string,
53
+ expectedError: string,
54
+ errorType: string,
55
+ ) => {
56
+ it.skip(
57
+ 'displays error message when givenNameValue: ' +
58
+ givenNameValue +
59
+ ', middleNameValue: ' +
60
+ middleNameValue +
61
+ ', familyNameValue: ' +
62
+ familyNameValue,
63
+ async () => {
64
+ const error = (await updateNameAndReturnError(givenNameValue, middleNameValue, familyNameValue))[errorType];
65
+ expect(error.textContent).toEqual(expectedError);
66
+ },
67
+ );
68
+ };
69
+
70
+ const updateNameAndReturnError = async (givenNameValue: string, middleNameValue: string, familyNameValue: string) => {
71
+ const user = userEvent.setup();
72
+
73
+ mockUseConfig.mockReturnValue(mockFieldConfigs);
74
+
75
+ render(
76
+ <Formik
77
+ initialValues={{
78
+ givenName: '',
79
+ middleName: '',
80
+ familyName: '',
81
+ }}
82
+ onSubmit={null}
83
+ validationSchema={validationSchema}>
84
+ <Form>
85
+ <PatientRegistrationContext.Provider
86
+ value={{
87
+ initialFormValues: null,
88
+ identifierTypes: [],
89
+ validationSchema,
90
+ setValidationSchema: () => {},
91
+ values: formValues,
92
+ inEditMode: false,
93
+ setFieldValue: () => null,
94
+ currentPhoto: 'TEST',
95
+ isOffline: true,
96
+ setCapturePhotoProps: (value) => {},
97
+ }}>
98
+ <NameField />
99
+ </PatientRegistrationContext.Provider>
100
+ </Form>
101
+ </Formik>,
102
+ );
103
+ const givenNameInput = screen.getByLabelText('First Name') as HTMLInputElement;
104
+ const middleNameInput = screen.getByLabelText(/Middle Name/i) as HTMLInputElement;
105
+ const familyNameInput = screen.getByLabelText('Family Name') as HTMLInputElement;
106
+
107
+ await user.click(givenNameInput);
108
+
109
+ fireEvent.change(givenNameInput, { target: { value: givenNameValue } });
110
+ fireEvent.blur(givenNameInput);
111
+ fireEvent.change(middleNameInput, { target: { value: middleNameValue } });
112
+ fireEvent.blur(middleNameInput);
113
+ fireEvent.change(familyNameInput, { target: { value: familyNameValue } });
114
+ fireEvent.blur(familyNameInput);
115
+
116
+ return {
117
+ givenNameError: screen.queryByText('Given name is required'),
118
+ middleNameError: screen.queryByText('Middle name is required'),
119
+ familyNameError: screen.queryByText('Family name is required'),
120
+ };
121
+ };
122
+
123
+ testValidName('Aaron', 'A', 'Aaronson');
124
+ testValidName('No', '', 'Middle Name');
125
+ testInvalidName('', '', '', 'Given name is required', 'givenNameError');
126
+ testInvalidName('', '', '', 'Family name is required', 'familyNameError');
127
+ testInvalidName('', 'No', 'Given Name', 'Given name is required', 'givenNameError');
128
+ testInvalidName('No', 'Family Name', '', 'Family name is required', 'familyNameError');
129
+ });