@ampath/esm-patient-registration-app 6.0.1-pre.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/.turbo/turbo-build.log +41 -0
  2. package/README.md +7 -0
  3. package/dist/130.js +2 -0
  4. package/dist/130.js.LICENSE.txt +3 -0
  5. package/dist/130.js.map +1 -0
  6. package/dist/152.js +1 -0
  7. package/dist/152.js.map +1 -0
  8. package/dist/249.js +2 -0
  9. package/dist/249.js.LICENSE.txt +46 -0
  10. package/dist/249.js.map +1 -0
  11. package/dist/255.js +2 -0
  12. package/dist/255.js.LICENSE.txt +9 -0
  13. package/dist/255.js.map +1 -0
  14. package/dist/271.js +1 -0
  15. package/dist/303.js +1 -0
  16. package/dist/303.js.map +1 -0
  17. package/dist/319.js +1 -0
  18. package/dist/365.js +1 -0
  19. package/dist/365.js.map +1 -0
  20. package/dist/460.js +1 -0
  21. package/dist/525.js +1 -0
  22. package/dist/525.js.map +1 -0
  23. package/dist/537.js +1 -0
  24. package/dist/537.js.map +1 -0
  25. package/dist/574.js +1 -0
  26. package/dist/591.js +2 -0
  27. package/dist/591.js.LICENSE.txt +32 -0
  28. package/dist/591.js.map +1 -0
  29. package/dist/621.js +1 -0
  30. package/dist/621.js.map +1 -0
  31. package/dist/644.js +1 -0
  32. package/dist/729.js +1 -0
  33. package/dist/729.js.map +1 -0
  34. package/dist/735.js +1 -0
  35. package/dist/735.js.map +1 -0
  36. package/dist/757.js +1 -0
  37. package/dist/784.js +2 -0
  38. package/dist/784.js.LICENSE.txt +9 -0
  39. package/dist/784.js.map +1 -0
  40. package/dist/788.js +1 -0
  41. package/dist/807.js +1 -0
  42. package/dist/833.js +1 -0
  43. package/dist/879.js +1 -0
  44. package/dist/879.js.map +1 -0
  45. package/dist/ampath-esm-patient-registration-app.js +1 -0
  46. package/dist/ampath-esm-patient-registration-app.js.buildmanifest.json +649 -0
  47. package/dist/ampath-esm-patient-registration-app.js.map +1 -0
  48. package/dist/main.js +2 -0
  49. package/dist/main.js.LICENSE.txt +56 -0
  50. package/dist/main.js.map +1 -0
  51. package/dist/routes.json +1 -0
  52. package/docs/images/patient-registration-hierarchy.png +0 -0
  53. package/jest.config.js +3 -0
  54. package/package.json +61 -0
  55. package/src/add-patient-link.scss +3 -0
  56. package/src/add-patient-link.test.tsx +20 -0
  57. package/src/add-patient-link.tsx +21 -0
  58. package/src/config-schema.ts +410 -0
  59. package/src/constants.ts +14 -0
  60. package/src/declarations.d.ts +6 -0
  61. package/src/index.ts +71 -0
  62. package/src/nav-link.test.tsx +13 -0
  63. package/src/nav-link.tsx +10 -0
  64. package/src/offline.resources.ts +155 -0
  65. package/src/offline.ts +91 -0
  66. package/src/patient-registration/before-save-prompt.tsx +73 -0
  67. package/src/patient-registration/date-util.ts +52 -0
  68. package/src/patient-registration/field/__mocks__/field.resource.ts +60 -0
  69. package/src/patient-registration/field/address/address-field.component.tsx +153 -0
  70. package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +73 -0
  71. package/src/patient-registration/field/address/address-hierarchy.resource.tsx +157 -0
  72. package/src/patient-registration/field/address/address-search.component.tsx +85 -0
  73. package/src/patient-registration/field/address/address-search.scss +53 -0
  74. package/src/patient-registration/field/address/custom-address-field.component.tsx +31 -0
  75. package/src/patient-registration/field/address/tests/address-hierarchy.test.tsx +214 -0
  76. package/src/patient-registration/field/address/tests/address-search-component.test.tsx +135 -0
  77. package/src/patient-registration/field/custom-field.component.tsx +25 -0
  78. package/src/patient-registration/field/dob/dob.component.tsx +159 -0
  79. package/src/patient-registration/field/dob/dob.test.tsx +75 -0
  80. package/src/patient-registration/field/field.component.tsx +47 -0
  81. package/src/patient-registration/field/field.resource.ts +35 -0
  82. package/src/patient-registration/field/field.scss +127 -0
  83. package/src/patient-registration/field/field.test.tsx +294 -0
  84. package/src/patient-registration/field/gender/gender-field.component.tsx +49 -0
  85. package/src/patient-registration/field/gender/gender-field.test.tsx +59 -0
  86. package/src/patient-registration/field/id/id-field.component.tsx +144 -0
  87. package/src/patient-registration/field/id/id-field.test.tsx +107 -0
  88. package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +198 -0
  89. package/src/patient-registration/field/id/identifier-selection.scss +37 -0
  90. package/src/patient-registration/field/name/name-field.component.tsx +142 -0
  91. package/src/patient-registration/field/obs/obs-field.component.tsx +204 -0
  92. package/src/patient-registration/field/obs/obs-field.test.tsx +205 -0
  93. package/src/patient-registration/field/person-attributes/coded-attributes.component.tsx +60 -0
  94. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +116 -0
  95. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +127 -0
  96. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +88 -0
  97. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +187 -0
  98. package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +20 -0
  99. package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +58 -0
  100. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +88 -0
  101. package/src/patient-registration/field/phone/phone-field.component.tsx +16 -0
  102. package/src/patient-registration/form-manager.test.ts +67 -0
  103. package/src/patient-registration/form-manager.ts +414 -0
  104. package/src/patient-registration/input/basic-input/input/input.component.tsx +179 -0
  105. package/src/patient-registration/input/basic-input/input/input.test.tsx +72 -0
  106. package/src/patient-registration/input/basic-input/select/select-input.component.tsx +32 -0
  107. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
  108. package/src/patient-registration/input/combo-input/combo-input.component.tsx +128 -0
  109. package/src/patient-registration/input/combo-input/selection-tick.component.tsx +20 -0
  110. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +187 -0
  111. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +62 -0
  112. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +132 -0
  113. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +156 -0
  114. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +107 -0
  115. package/src/patient-registration/input/custom-input/identifier/utils.test.ts +81 -0
  116. package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
  117. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +53 -0
  118. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +43 -0
  119. package/src/patient-registration/input/input.scss +118 -0
  120. package/src/patient-registration/patient-registration-context.ts +24 -0
  121. package/src/patient-registration/patient-registration-hooks.ts +287 -0
  122. package/src/patient-registration/patient-registration-utils.ts +216 -0
  123. package/src/patient-registration/patient-registration.component.tsx +240 -0
  124. package/src/patient-registration/patient-registration.resource.test.tsx +26 -0
  125. package/src/patient-registration/patient-registration.resource.ts +250 -0
  126. package/src/patient-registration/patient-registration.scss +122 -0
  127. package/src/patient-registration/patient-registration.test.tsx +471 -0
  128. package/src/patient-registration/patient-registration.types.ts +318 -0
  129. package/src/patient-registration/section/death-info/death-info-section.component.tsx +31 -0
  130. package/src/patient-registration/section/death-info/death-info-section.test.tsx +64 -0
  131. package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
  132. package/src/patient-registration/section/demographics/demographics-section.test.tsx +83 -0
  133. package/src/patient-registration/section/generic-section.component.tsx +17 -0
  134. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +235 -0
  135. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +100 -0
  136. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +78 -0
  137. package/src/patient-registration/section/patient-relationships/relationships.scss +35 -0
  138. package/src/patient-registration/section/section-wrapper.component.tsx +40 -0
  139. package/src/patient-registration/section/section.component.tsx +23 -0
  140. package/src/patient-registration/section/section.scss +1 -0
  141. package/src/patient-registration/ui-components/overlay/overlay.component.tsx +51 -0
  142. package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
  143. package/src/patient-registration/validation/patient-registration-validation.test.tsx +157 -0
  144. package/src/patient-registration/validation/patient-registration-validation.tsx +60 -0
  145. package/src/patient-verification/client-registry-constants.ts +13 -0
  146. package/src/patient-verification/client-registry.component.tsx +66 -0
  147. package/src/patient-verification/client-registry.scss +1 -0
  148. package/src/patient-verification/utils.tsx +56 -0
  149. package/src/patient-verification/verification-modal.scss +20 -0
  150. package/src/patient-verification/verification.component.tsx +48 -0
  151. package/src/resource.ts +12 -0
  152. package/src/root.component.tsx +63 -0
  153. package/src/root.scss +7 -0
  154. package/src/root.test.tsx +32 -0
  155. package/src/routes.json +66 -0
  156. package/src/widgets/cancel-patient-edit.component.tsx +37 -0
  157. package/src/widgets/cancel-patient-edit.test.tsx +27 -0
  158. package/src/widgets/delete-identifier-confirmation-modal.test.tsx +34 -0
  159. package/src/widgets/delete-identifier-confirmation-modal.tsx +41 -0
  160. package/src/widgets/delete-identifier-modal.scss +34 -0
  161. package/src/widgets/display-photo.component.tsx +30 -0
  162. package/src/widgets/display-photo.test.tsx +37 -0
  163. package/src/widgets/edit-patient-details-button.component.tsx +34 -0
  164. package/src/widgets/edit-patient-details-button.scss +3 -0
  165. package/src/widgets/edit-patient-details-button.test.tsx +41 -0
  166. package/translations/am.json +97 -0
  167. package/translations/ar.json +97 -0
  168. package/translations/en.json +103 -0
  169. package/translations/es.json +97 -0
  170. package/translations/fr.json +97 -0
  171. package/translations/he.json +97 -0
  172. package/translations/km.json +97 -0
  173. package/translations/zh.json +89 -0
  174. package/translations/zh_CN.json +89 -0
  175. package/tsconfig.json +5 -0
  176. package/webpack.config.js +1 -0
@@ -0,0 +1,157 @@
1
+ import { defineConfigSchema, getConfig } from '@openmrs/esm-framework';
2
+ import { getValidationSchema } from './patient-registration-validation';
3
+ import { type RegistrationConfig, esmPatientRegistrationSchema } from '../../config-schema';
4
+ describe('Patient Registration Validation', () => {
5
+ beforeAll(() => {
6
+ defineConfigSchema('@openmrs/esm-patient-registration-app', esmPatientRegistrationSchema);
7
+ });
8
+
9
+ const validFormValues = {
10
+ givenName: 'John',
11
+ familyName: 'Doe',
12
+ additionalGivenName: '',
13
+ additionalFamilyName: '',
14
+ gender: 'male',
15
+ birthdate: new Date('1990-01-01'),
16
+ birthdateEstimated: false,
17
+ deathDate: null,
18
+ email: 'john.doe@example.com',
19
+ identifiers: {
20
+ nationalId: {
21
+ required: true,
22
+ identifierValue: '123456789',
23
+ },
24
+ passportId: {
25
+ required: false,
26
+ identifierValue: '',
27
+ },
28
+ },
29
+ };
30
+
31
+ const validateFormValues = async (formValues) => {
32
+ const config = (await getConfig('@openmrs/esm-patient-registration-app')) as any as RegistrationConfig;
33
+ const validationSchema = getValidationSchema(config);
34
+ try {
35
+ await validationSchema.validate(formValues, { abortEarly: false });
36
+ } catch (err) {
37
+ return err;
38
+ }
39
+ };
40
+
41
+ it('should allow valid form values', async () => {
42
+ const validationError = await validateFormValues(validFormValues);
43
+ expect(validationError).toBeFalsy();
44
+ });
45
+
46
+ it('should require givenName', async () => {
47
+ const invalidFormValues = {
48
+ ...validFormValues,
49
+ givenName: '',
50
+ };
51
+ const validationError = await validateFormValues(invalidFormValues);
52
+ expect(validationError.errors).toContain('givenNameRequired');
53
+ });
54
+
55
+ it('should require familyName', async () => {
56
+ const invalidFormValues = {
57
+ ...validFormValues,
58
+ familyName: '',
59
+ };
60
+ const validationError = await validateFormValues(invalidFormValues);
61
+ expect(validationError.errors).toContain('familyNameRequired');
62
+ });
63
+
64
+ it('should require additionalGivenName when addNameInLocalLanguage is true', async () => {
65
+ const invalidFormValues = {
66
+ ...validFormValues,
67
+ addNameInLocalLanguage: true,
68
+ additionalGivenName: '',
69
+ };
70
+ const validationError = await validateFormValues(invalidFormValues);
71
+ expect(validationError.errors).toContain('givenNameRequired');
72
+ });
73
+
74
+ it('should require additionalFamilyName when addNameInLocalLanguage is true', async () => {
75
+ const invalidFormValues = {
76
+ ...validFormValues,
77
+ addNameInLocalLanguage: true,
78
+ additionalFamilyName: '',
79
+ };
80
+ const validationError = await validateFormValues(invalidFormValues);
81
+ expect(validationError.errors).toContain('familyNameRequired');
82
+ });
83
+
84
+ it('should require gender', async () => {
85
+ const invalidFormValues = {
86
+ ...validFormValues,
87
+ gender: '',
88
+ };
89
+ const validationError = await validateFormValues(invalidFormValues);
90
+ expect(validationError.errors).toContain('genderUnspecified');
91
+ });
92
+
93
+ it('should allow female as a valid gender', async () => {
94
+ const validFormValuesWithOtherGender = {
95
+ ...validFormValues,
96
+ gender: 'female',
97
+ };
98
+ const validationError = await validateFormValues(validFormValuesWithOtherGender);
99
+ expect(validationError).toBeFalsy();
100
+ });
101
+
102
+ it('should allow other as a valid gender', async () => {
103
+ const validFormValuesWithOtherGender = {
104
+ ...validFormValues,
105
+ gender: 'other',
106
+ };
107
+ const validationError = await validateFormValues(validFormValuesWithOtherGender);
108
+ expect(validationError).toBeFalsy();
109
+ });
110
+
111
+ it('should allow unknown as a valid gender', async () => {
112
+ const validFormValuesWithOtherGender = {
113
+ ...validFormValues,
114
+ gender: 'unknown',
115
+ };
116
+ const validationError = await validateFormValues(validFormValuesWithOtherGender);
117
+ expect(validationError).toBeFalsy();
118
+ });
119
+
120
+ it('should throw error when date of birth is a future date', async () => {
121
+ const invalidFormValues = {
122
+ ...validFormValues,
123
+ birthdate: new Date('2100-01-01'),
124
+ };
125
+ const validationError = await validateFormValues(invalidFormValues);
126
+ expect(validationError.errors).toContain('birthdayNotInTheFuture');
127
+ });
128
+
129
+ it('should require yearsEstimated when birthdateEstimated is true', async () => {
130
+ const invalidFormValues = {
131
+ ...validFormValues,
132
+ birthdateEstimated: true,
133
+ };
134
+ const validationError = await validateFormValues(invalidFormValues);
135
+ expect(validationError.errors).toContain('yearsEstimateRequired');
136
+ });
137
+
138
+ it('should throw error when monthEstimated is negative', async () => {
139
+ const invalidFormValues = {
140
+ ...validFormValues,
141
+ birthdateEstimated: true,
142
+ yearsEstimated: 0,
143
+ monthsEstimated: -1,
144
+ };
145
+ const validationError = await validateFormValues(invalidFormValues);
146
+ expect(validationError.errors).toContain('negativeMonths');
147
+ });
148
+
149
+ it('should throw error when deathDate is in future', async () => {
150
+ const invalidFormValues = {
151
+ ...validFormValues,
152
+ deathDate: new Date('2100-01-01'),
153
+ };
154
+ const validationError = await validateFormValues(invalidFormValues);
155
+ expect(validationError.errors).toContain('deathdayNotInTheFuture');
156
+ });
157
+ });
@@ -0,0 +1,60 @@
1
+ import * as Yup from 'yup';
2
+ import mapValues from 'lodash/mapValues';
3
+ import { type FormValues } from '../patient-registration.types';
4
+ import { type RegistrationConfig } from '../../config-schema';
5
+
6
+ export function getValidationSchema(config: RegistrationConfig) {
7
+ return Yup.object({
8
+ givenName: Yup.string().required('givenNameRequired'),
9
+ familyName: Yup.string().required('familyNameRequired'),
10
+ additionalGivenName: Yup.string().when('addNameInLocalLanguage', {
11
+ is: true,
12
+ then: Yup.string().required('givenNameRequired'),
13
+ otherwise: Yup.string().notRequired(),
14
+ }),
15
+ additionalFamilyName: Yup.string().when('addNameInLocalLanguage', {
16
+ is: true,
17
+ then: Yup.string().required('familyNameRequired'),
18
+ otherwise: Yup.string().notRequired(),
19
+ }),
20
+ gender: Yup.string()
21
+ .oneOf(
22
+ config.fieldConfigurations.gender.map((g) => g.value),
23
+ 'genderUnspecified',
24
+ )
25
+ .required('genderRequired'),
26
+ birthdate: Yup.date().when('birthdateEstimated', {
27
+ is: false,
28
+ then: Yup.date().required('birthdayRequired').max(Date(), 'birthdayNotInTheFuture').nullable(),
29
+ otherwise: Yup.date().nullable(),
30
+ }),
31
+ yearsEstimated: Yup.number().when('birthdateEstimated', {
32
+ is: true,
33
+ then: Yup.number().required('yearsEstimateRequired').min(0, 'negativeYears'),
34
+ otherwise: Yup.number().nullable(),
35
+ }),
36
+ monthsEstimated: Yup.number().min(0, 'negativeMonths'),
37
+ deathDate: Yup.date().max(Date(), 'deathdayNotInTheFuture').nullable(),
38
+ email: Yup.string().optional().email('invalidEmail'),
39
+ identifiers: Yup.lazy((obj: FormValues['identifiers']) =>
40
+ Yup.object(
41
+ mapValues(obj, () =>
42
+ Yup.object({
43
+ required: Yup.bool(),
44
+ identifierValue: Yup.string().when('required', {
45
+ is: true,
46
+ then: Yup.string().required('identifierValueRequired'),
47
+ otherwise: Yup.string().notRequired(),
48
+ }),
49
+ }),
50
+ ),
51
+ ),
52
+ ),
53
+ relationships: Yup.array().of(
54
+ Yup.object().shape({
55
+ relatedPersonUuid: Yup.string().required(),
56
+ relationshipType: Yup.string().required(),
57
+ }),
58
+ ),
59
+ });
60
+ }
@@ -0,0 +1,13 @@
1
+ export const countries = [
2
+ { text: 'Kenya', id: 'KE' },
3
+ { text: 'Uganda', id: 'UG' },
4
+ { text: 'Tanzania', id: 'TZ' },
5
+ ];
6
+
7
+ export const identifierTypes = [
8
+ { text: 'National ID number', id: '58a47054-1359-11df-a1f1-0026b9348838' },
9
+ { text: 'Passport number', id: 'ced014a1-068a-4a13-b6b3-17412f754af2' },
10
+ { text: 'Birth Certificate Entry Number', id: '7924e13b-131a-4da8-8efa-e294184a1b0d' },
11
+ ];
12
+
13
+ export const defaultSelectedCountry = 'KE';
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import { type FormValues } from '../patient-registration/patient-registration.types';
3
+ import {} from '@carbon/react';
4
+ import { Dropdown, TextInput, Button, InlineLoading } from '@carbon/react';
5
+ import styles from './client-registry.scss';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { useClientRegistryForm } from './utils';
8
+ import { countries, identifierTypes } from './client-registry-constants';
9
+
10
+ type ClientRegistryProps = {
11
+ setInitialFormValues: (initialValues) => void;
12
+ initialFormValues: FormValues;
13
+ };
14
+
15
+ export const ClientRegistry: React.FC<ClientRegistryProps> = ({ setInitialFormValues, initialFormValues }) => {
16
+ const { t } = useTranslation();
17
+ const { handleClientRegistryDataSubmit, handleOnChange, clientRegistryData } = useClientRegistryForm(
18
+ setInitialFormValues,
19
+ initialFormValues,
20
+ );
21
+ return (
22
+ <div className={styles.patientVerification}>
23
+ <h3 className={styles.productiveHeading02} style={{ color: '#161616' }}>
24
+ {t('patientVerification', 'Patient Verification')}
25
+ </h3>
26
+ <Dropdown
27
+ id="default"
28
+ titleText="Country"
29
+ label="Select country"
30
+ items={countries}
31
+ onChange={({ selectedItem }) => handleOnChange(selectedItem?.id, 'country')}
32
+ initialSelectedItem={countries[0]}
33
+ itemToString={(item) => (item ? item.text : '')}
34
+ />
35
+ <Dropdown
36
+ id="default"
37
+ titleText="Verification ID Type"
38
+ label="Select verification ID Type"
39
+ items={identifierTypes}
40
+ onChange={({ selectedItem }) => handleOnChange(selectedItem?.id, 'identifierType')}
41
+ itemToString={(item) => (item ? item.text : '')}
42
+ />
43
+ <TextInput
44
+ disabled={clientRegistryData.identifierType === ''}
45
+ onChange={(e) => handleOnChange(e.target.value, 'patientIdentifier')}
46
+ id="patientIdentifier"
47
+ type="text"
48
+ labelText="Patient identifier"
49
+ placeholder="Enter patient identifier"
50
+ />
51
+ {clientRegistryData.isSubmitting ? (
52
+ <InlineLoading
53
+ description={`${t('searchRegistry', 'Searching registry')} ...`}
54
+ iconDescription="searching registry ..."
55
+ status="active"
56
+ />
57
+ ) : (
58
+ <Button
59
+ disabled={clientRegistryData.isSubmitting || clientRegistryData.patientIdentifier === ''}
60
+ onClick={() => handleClientRegistryDataSubmit()}>
61
+ {t('searchRegistry', 'Search Registry')}
62
+ </Button>
63
+ )}
64
+ </div>
65
+ );
66
+ };
@@ -0,0 +1 @@
1
+ @import '../patient-registration/patient-registration.scss';
@@ -0,0 +1,56 @@
1
+ import { showModal } from '@openmrs/esm-framework';
2
+ import { useState } from 'react';
3
+ import { fetchPatientRecordFromClientRegistry } from '../patient-registration/patient-registration.resource';
4
+ import { defaultSelectedCountry } from './client-registry-constants';
5
+
6
+ export const handleSetFormValueFromClientRegistry = (clientData, setInitialFormValues, initialFormValues) => {
7
+ setInitialFormValues({
8
+ ...initialFormValues,
9
+ familyName: clientData.lastName,
10
+ givenName: clientData.firstName,
11
+ middleName: clientData.middleName,
12
+ gender: clientData.gender,
13
+ birthdate: clientData.dateOfBirth,
14
+ address: {
15
+ address2: clientData.subCounty,
16
+ },
17
+ });
18
+ };
19
+
20
+ export const useClientRegistryForm = (setInitialFormValues, initialFormValues) => {
21
+ const [clientRegistryData, setClientRegistryData] = useState<{
22
+ country: string;
23
+ identifierType: string;
24
+ patientIdentifier: string;
25
+ isSubmitting: boolean;
26
+ }>({ country: defaultSelectedCountry, identifierType: '', patientIdentifier: '', isSubmitting: false });
27
+
28
+ const handleOnChange = (data, key: 'country' | 'identifierType' | 'patientIdentifier') => {
29
+ setClientRegistryData({ ...clientRegistryData, [key]: data });
30
+ };
31
+
32
+ const handleClientRegistryDataSubmit = () => {
33
+ setClientRegistryData({ ...clientRegistryData, isSubmitting: true });
34
+ fetchPatientRecordFromClientRegistry(
35
+ clientRegistryData.patientIdentifier,
36
+ clientRegistryData.identifierType,
37
+ clientRegistryData.country,
38
+ ).then((res) => {
39
+ setClientRegistryData({ ...clientRegistryData, isSubmitting: false });
40
+ if (res.clientExists) {
41
+ const clientData = res.client;
42
+ const closeModal = showModal('client-registry-modal', {
43
+ clientData,
44
+ closeModal: () => {
45
+ closeModal();
46
+ handleSetFormValueFromClientRegistry(clientData, setInitialFormValues, initialFormValues);
47
+ },
48
+ });
49
+ } else {
50
+ const clientData = res.client;
51
+ }
52
+ });
53
+ };
54
+
55
+ return { handleClientRegistryDataSubmit, handleOnChange, clientRegistryData };
56
+ };
@@ -0,0 +1,20 @@
1
+ @use '@carbon/type';
2
+ @use '@carbon/colors';
3
+
4
+ .cardContainer {
5
+ display: grid;
6
+ grid-template-columns: 0.5fr 1fr;
7
+ column-gap: 0.25rem;
8
+ margin: 0.5rem 0;
9
+ }
10
+
11
+ .label {
12
+ @include type.type-style('body-compact-02');
13
+ color: colors.$cool-gray-100;
14
+ font-weight: bold;
15
+ }
16
+
17
+ .value {
18
+ @include type.type-style('legal-01');
19
+ color: colors.$cool-gray-80;
20
+ }
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+ import { ModalHeader, ModalBody, ModalFooter, Button } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import styles from './verification-modal.scss';
5
+
6
+ type VerificationModalProps = { clientData; closeModal };
7
+
8
+ const VerificationModal: React.FC<VerificationModalProps> = ({ clientData, closeModal }) => {
9
+ const { t } = useTranslation();
10
+ return (
11
+ <div>
12
+ <ModalHeader closeModal={closeModal} title={t('clientRegistry', 'Client registry')} />
13
+ <ModalBody>
14
+ <p>{t('clientRegistry', 'Client registry')}</p>
15
+ <Card label="Name" value={`${clientData.firstName} ${clientData.middleName} ${clientData.lastName}`} />
16
+ <Card
17
+ label="Identification Type"
18
+ value={clientData.identifications.map((id) => id.identificationType).join(' ')}
19
+ />
20
+ <Card
21
+ label="Identication Number"
22
+ value={clientData.identifications.map((id) => id.identificationNumber).join(' ')}
23
+ />
24
+ <Card label="Date of Birth" value={clientData.dateOfBirth} />
25
+ <Card label={t('gender', 'Gender')} value={clientData.gender} />
26
+ </ModalBody>
27
+ <ModalFooter>
28
+ <Button kind="secondary" onClick={() => {}}>
29
+ {t('contactSupport', 'Contact supportt')}
30
+ </Button>
31
+ <Button kind="danger" onClick={closeModal}>
32
+ {t('continue', 'Continue')}
33
+ </Button>
34
+ </ModalFooter>
35
+ </div>
36
+ );
37
+ };
38
+
39
+ export default VerificationModal;
40
+
41
+ export const Card: React.FC<{ label: string; value: string }> = ({ label, value }) => {
42
+ return (
43
+ <div className={styles.cardContainer}>
44
+ <span>{label}</span>
45
+ <span>{value}</span>
46
+ </div>
47
+ );
48
+ };
@@ -0,0 +1,12 @@
1
+ import { openmrsFetch } from '@openmrs/esm-framework';
2
+
3
+ const AddressHierarchyBaseURL = '/module/addresshierarchy/ajax/getPossibleAddressHierarchyEntriesWithParents.form';
4
+
5
+ export function performAdressHierarchyWithParentSearch(addressField, parentid, query) {
6
+ return openmrsFetch(
7
+ `${AddressHierarchyBaseURL}?addressField=${addressField}&limit=20&searchString=${query}&parentUuid=${parentid}`,
8
+ {
9
+ method: 'GET',
10
+ },
11
+ );
12
+ }
@@ -0,0 +1,63 @@
1
+ import React, { useMemo } from 'react';
2
+ import classNames from 'classnames';
3
+ import useSWRImmutable from 'swr/immutable';
4
+ import { BrowserRouter, Route, Routes } from 'react-router-dom';
5
+ import { Grid, Row } from '@carbon/react';
6
+ import { ExtensionSlot, useConnectivity, useSession } from '@openmrs/esm-framework';
7
+ import {
8
+ ResourcesContext,
9
+ fetchAddressTemplate,
10
+ fetchAllRelationshipTypes,
11
+ fetchPatientIdentifierTypesWithSources,
12
+ } from './offline.resources';
13
+ import { FormManager } from './patient-registration/form-manager';
14
+ import { PatientRegistration } from './patient-registration/patient-registration.component';
15
+ import styles from './root.scss';
16
+
17
+ export default function Root() {
18
+ const isOnline = useConnectivity();
19
+ const currentSession = useSession();
20
+ const { data: addressTemplate } = useSWRImmutable('patientRegistrationAddressTemplate', fetchAddressTemplate);
21
+ const { data: relationshipTypes } = useSWRImmutable(
22
+ 'patientRegistrationRelationshipTypes',
23
+ fetchAllRelationshipTypes,
24
+ );
25
+ const { data: identifierTypes } = useSWRImmutable(
26
+ 'patientRegistrationPatientIdentifiers',
27
+ fetchPatientIdentifierTypesWithSources,
28
+ );
29
+ const savePatientForm = useMemo(
30
+ () => (isOnline ? FormManager.savePatientFormOnline : FormManager.savePatientFormOffline),
31
+ [isOnline],
32
+ );
33
+
34
+ return (
35
+ <main className={classNames('omrs-main-content', styles.root)}>
36
+ <Grid className={styles.grid}>
37
+ <Row>
38
+ <ExtensionSlot name="breadcrumbs-slot" />
39
+ </Row>
40
+ <ResourcesContext.Provider
41
+ value={{
42
+ addressTemplate,
43
+ relationshipTypes,
44
+ identifierTypes,
45
+ currentSession,
46
+ }}>
47
+ <BrowserRouter basename={window.getOpenmrsSpaBase()}>
48
+ <Routes>
49
+ <Route
50
+ path="patient-registration"
51
+ element={<PatientRegistration savePatientForm={savePatientForm} isOffline={!isOnline} />}
52
+ />
53
+ <Route
54
+ path="patient/:patientUuid/edit"
55
+ element={<PatientRegistration savePatientForm={savePatientForm} isOffline={!isOnline} />}
56
+ />
57
+ </Routes>
58
+ </BrowserRouter>
59
+ </ResourcesContext.Provider>
60
+ </Grid>
61
+ </main>
62
+ );
63
+ }
package/src/root.scss ADDED
@@ -0,0 +1,7 @@
1
+ .root {
2
+ background-color: white;
3
+ }
4
+
5
+ .grid {
6
+ grid-template-columns: repeat(1, minmax(0, 1fr));
7
+ }
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import Root from './root.component';
4
+
5
+ window['getOpenmrsSpaBase'] = jest.fn().mockImplementation(() => '/');
6
+
7
+ jest.mock('@openmrs/esm-framework', () => {
8
+ const originalModule = jest.requireActual('@openmrs/esm-framework');
9
+
10
+ return {
11
+ ...originalModule,
12
+ validator: jest.fn(),
13
+ };
14
+ });
15
+
16
+ describe('root component', () => {
17
+ it('renders without crashing', () => {
18
+ const div = document.createElement('div');
19
+ const root = createRoot(div);
20
+
21
+ root.render(
22
+ <Root
23
+ savePatientForm={jest.fn()}
24
+ addressTemplate={{ results: [] }}
25
+ currentSession={{} as any}
26
+ relationshipTypes={{ results: [] }}
27
+ identifierTypes={[]}
28
+ isOffline={false}
29
+ />,
30
+ );
31
+ });
32
+ });
@@ -0,0 +1,66 @@
1
+ {
2
+ "$schema": "https://json.openmrs.org/routes.schema.json",
3
+ "backendDependencies": {
4
+ "webservices.rest": "^2.24.0"
5
+ },
6
+ "pages": [
7
+ {
8
+ "component": "root",
9
+ "route": "patient-registration",
10
+ "online": true,
11
+ "offline": true
12
+ },
13
+ {
14
+ "component": "editPatient",
15
+ "routeRegex": "patient\\/([a-zA-Z0-9\\-]+)\\/edit",
16
+ "online": true,
17
+ "offline": true
18
+ }
19
+ ],
20
+ "extensions": [
21
+ {
22
+ "component": "addPatientLink",
23
+ "name": "add-patient-action",
24
+ "slot": "top-nav-actions-slot",
25
+ "online": true,
26
+ "offline": true
27
+ },
28
+ {
29
+ "component": "cancelPatientEditModal",
30
+ "name": "cancel-patient-edit-modal",
31
+ "online": true,
32
+ "offline": true
33
+ },
34
+ {
35
+ "component": "patientPhoto",
36
+ "name": "patient-photo-widget",
37
+ "slot": "patient-photo-slot",
38
+ "online": true,
39
+ "offline": true
40
+ },
41
+ {
42
+ "component": "editPatientDetailsButton",
43
+ "name": "edit-patient-details-button",
44
+ "slot": "patient-actions-slot",
45
+ "online": true,
46
+ "offline": true
47
+ },
48
+ {
49
+ "component": "editPatientDetailsButton",
50
+ "name": "edit-patient-details-button",
51
+ "slot": "patient-search-actions-slot",
52
+ "online": true,
53
+ "offline": true
54
+ },
55
+ {
56
+ "component": "deleteIdentifierConfirmationModal",
57
+ "name": "delete-identifier-confirmation-modal",
58
+ "online": true,
59
+ "offline": true
60
+ },
61
+ {
62
+ "name": "client-registry-modal",
63
+ "component": "clientRegistryModal"
64
+ }
65
+ ]
66
+ }
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import { Button } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ interface CancelPatientEditProps {
6
+ close(): void;
7
+ onConfirm(): void;
8
+ }
9
+
10
+ const CancelPatientEdit: React.FC<CancelPatientEditProps> = ({ close, onConfirm }) => {
11
+ const { t } = useTranslation();
12
+ return (
13
+ <>
14
+ <div className="cds--modal-header">
15
+ <h3 className="cds--modal-header__heading">{t('discardModalHeader', 'Confirm Discard Changes')}</h3>
16
+ </div>
17
+ <div className="cds--modal-content">
18
+ <p>
19
+ {t(
20
+ 'discardModalBody',
21
+ "The changes you made to this patient's details have not been saved. Discard changes?",
22
+ )}
23
+ </p>
24
+ </div>
25
+ <div className="cds--modal-footer">
26
+ <Button kind="secondary" onClick={close}>
27
+ {t('cancel', 'Cancel')}
28
+ </Button>
29
+ <Button kind="danger" onClick={onConfirm}>
30
+ {t('discard', 'Discard')}
31
+ </Button>
32
+ </div>
33
+ </>
34
+ );
35
+ };
36
+
37
+ export default CancelPatientEdit;
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { screen, render } from '@testing-library/react';
4
+ import CancelPatientEdit from './cancel-patient-edit.component';
5
+
6
+ describe('CancelPatientEdit component', () => {
7
+ const mockClose = jest.fn();
8
+ const mockOnConfirm = jest.fn();
9
+
10
+ beforeEach(() => {
11
+ jest.clearAllMocks();
12
+ });
13
+
14
+ it('renders the modal and triggers close and onConfirm functions', async () => {
15
+ const user = userEvent.setup();
16
+
17
+ render(<CancelPatientEdit close={mockClose} onConfirm={mockOnConfirm} />);
18
+
19
+ const cancelButton = screen.getByRole('button', { name: /Cancel/i });
20
+ await user.click(cancelButton);
21
+ expect(mockClose).toHaveBeenCalledTimes(1);
22
+
23
+ const discardButton = screen.getByRole('button', { name: /discard/i });
24
+ await user.click(discardButton);
25
+ expect(mockOnConfirm).toHaveBeenCalledTimes(1);
26
+ });
27
+ });