@olaboot/esm-patient-registration-app 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADDRESS_CONFIGURATION.md +152 -0
- package/IDENTIFIER_CONFIGURATION.md +142 -0
- package/IMPLEMENTATION_SUMMARY.md +111 -0
- package/QUICK_START.md +95 -0
- package/README.md +7 -0
- package/address-required-fields-config.json +26 -0
- package/dist/126.js +1 -0
- package/dist/15.js +1 -0
- package/dist/1564.js +1 -0
- package/dist/1567.js +1 -0
- package/dist/1845.js +1 -0
- package/dist/1953.js +1 -0
- package/dist/200.js +1 -0
- package/dist/200.js.map +1 -0
- package/dist/215.js +1 -0
- package/dist/2178.js +1 -0
- package/dist/250.js +1 -0
- package/dist/250.js.map +1 -0
- package/dist/2523.js +1 -0
- package/dist/2523.js.map +1 -0
- package/dist/2566.js +1 -0
- package/dist/2586.js +1 -0
- package/dist/2586.js.map +1 -0
- package/dist/2716.js +1 -0
- package/dist/2716.js.map +1 -0
- package/dist/2759.js +1 -0
- package/dist/2821.js +6 -0
- package/dist/2821.js.map +1 -0
- package/dist/3089.js +1 -0
- package/dist/3089.js.map +1 -0
- package/dist/3230.js +1 -0
- package/dist/3441.js +1 -0
- package/dist/3565.js +1 -0
- package/dist/3571.js +1 -0
- package/dist/3571.js.map +1 -0
- package/dist/3746.js +1 -0
- package/dist/3925.js +1 -0
- package/dist/3946.js +1 -0
- package/dist/4024.js +1 -0
- package/dist/4024.js.map +1 -0
- package/dist/4744.js +1 -0
- package/dist/4744.js.map +1 -0
- package/dist/4809.js +1 -0
- package/dist/4894.js +1 -0
- package/dist/4970.js +1 -0
- package/dist/4970.js.map +1 -0
- package/dist/5130.js +1 -0
- package/dist/5187.js +1 -0
- package/dist/5491.js +1 -0
- package/dist/5491.js.map +1 -0
- package/dist/5595.js +1 -0
- package/dist/5961.js +1 -0
- package/dist/6133.js +1 -0
- package/dist/634.js +1 -0
- package/dist/634.js.map +1 -0
- package/dist/6456.js +1 -0
- package/dist/6466.js +1 -0
- package/dist/6613.js +1 -0
- package/dist/6783.js +1 -0
- package/dist/7073.js +38 -0
- package/dist/7073.js.map +1 -0
- package/dist/7154.js +1 -0
- package/dist/7154.js.map +1 -0
- package/dist/7348.js +1 -0
- package/dist/7439.js +1 -0
- package/dist/7439.js.map +1 -0
- package/dist/7543.js +1 -0
- package/dist/7607.js +1 -0
- package/dist/772.js +1 -0
- package/dist/7984.js +1 -0
- package/dist/7984.js.map +1 -0
- package/dist/8538.js +1 -0
- package/dist/8538.js.map +1 -0
- package/dist/8599.js +1 -0
- package/dist/8727.js +1 -0
- package/dist/8847.js +1 -0
- package/dist/9015.js +1 -0
- package/dist/906.js +1 -0
- package/dist/9065.js +1 -0
- package/dist/9182.js +1 -0
- package/dist/9339.js +1 -0
- package/dist/9453.js +1 -0
- package/dist/9833.js +1 -0
- package/dist/9833.js.map +1 -0
- package/dist/9856.js +1 -0
- package/dist/9856.js.map +1 -0
- package/dist/9920.js +1 -0
- package/dist/9938.js +1 -0
- package/dist/9943.js +1 -0
- package/dist/9943.js.map +1 -0
- package/dist/main.js +10 -0
- package/dist/main.js.map +1 -0
- package/dist/olaboot-esm-patient-registration-app.js +5 -0
- package/dist/olaboot-esm-patient-registration-app.js.buildmanifest.json +1627 -0
- package/dist/olaboot-esm-patient-registration-app.js.map +1 -0
- package/dist/routes.json +1 -0
- package/docs/images/patient-registration-hierarchy.png +0 -0
- package/example-config.json +14 -0
- package/jest.config.js +3 -0
- package/package.json +60 -0
- package/rspack.config.js +1 -0
- package/src/add-patient-link.extension.tsx +21 -0
- package/src/add-patient-link.scss +3 -0
- package/src/add-patient-link.test.tsx +16 -0
- package/src/config-schema.ts +507 -0
- package/src/constants.ts +14 -0
- package/src/declarations.d.ts +4 -0
- package/src/index.ts +59 -0
- package/src/nav-link.test.tsx +13 -0
- package/src/nav-link.tsx +10 -0
- package/src/offline.resources.ts +157 -0
- package/src/offline.ts +93 -0
- package/src/patient-photo.extension.tsx +11 -0
- package/src/patient-registration/before-save-prompt.component.tsx +72 -0
- package/src/patient-registration/field/__mocks__/field.resource.ts +60 -0
- package/src/patient-registration/field/address/address-field.component.tsx +186 -0
- package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +71 -0
- package/src/patient-registration/field/address/address-hierarchy.resource.tsx +157 -0
- package/src/patient-registration/field/address/address-hierarchy.test.tsx +296 -0
- package/src/patient-registration/field/address/address-search.component.tsx +87 -0
- package/src/patient-registration/field/address/address-search.scss +53 -0
- package/src/patient-registration/field/address/address-search.test.tsx +141 -0
- package/src/patient-registration/field/address/custom-address-field.component.tsx +32 -0
- package/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx +98 -0
- package/src/patient-registration/field/custom-field.component.tsx +25 -0
- package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx +79 -0
- package/src/patient-registration/field/dob/dob.component.tsx +167 -0
- package/src/patient-registration/field/dob/dob.test.tsx +90 -0
- package/src/patient-registration/field/field.component.tsx +53 -0
- package/src/patient-registration/field/field.resource.ts +42 -0
- package/src/patient-registration/field/field.scss +171 -0
- package/src/patient-registration/field/field.test.tsx +330 -0
- package/src/patient-registration/field/gender/gender-field.component.tsx +54 -0
- package/src/patient-registration/field/gender/gender-field.test.tsx +99 -0
- package/src/patient-registration/field/id/id-field.component.tsx +136 -0
- package/src/patient-registration/field/id/id-field.test.tsx +121 -0
- package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +200 -0
- package/src/patient-registration/field/id/identifier-selection.scss +41 -0
- package/src/patient-registration/field/name/name-field.component.tsx +148 -0
- package/src/patient-registration/field/obs/obs-field.component.tsx +261 -0
- package/src/patient-registration/field/obs/obs-field.test.tsx +299 -0
- package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +120 -0
- package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +141 -0
- package/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx +105 -0
- package/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx +48 -0
- package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +100 -0
- package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +193 -0
- package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +20 -0
- package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +58 -0
- package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +90 -0
- package/src/patient-registration/field/phone/phone-field.component.tsx +17 -0
- package/src/patient-registration/form-manager.test.ts +91 -0
- package/src/patient-registration/form-manager.ts +443 -0
- package/src/patient-registration/input/basic-input/input/input.component.tsx +183 -0
- package/src/patient-registration/input/basic-input/input/input.test.tsx +72 -0
- package/src/patient-registration/input/basic-input/select/select-input.component.tsx +32 -0
- package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
- package/src/patient-registration/input/combo-input/combo-input.component.tsx +130 -0
- package/src/patient-registration/input/combo-input/selection-tick.component.tsx +20 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +187 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +62 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +164 -0
- package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +193 -0
- package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +335 -0
- package/src/patient-registration/input/custom-input/identifier/utils.test.ts +81 -0
- package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
- package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +56 -0
- package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +34 -0
- package/src/patient-registration/input/input.scss +122 -0
- package/src/patient-registration/patient-registration-context.ts +35 -0
- package/src/patient-registration/patient-registration-hooks.ts +376 -0
- package/src/patient-registration/patient-registration-utils.test.ts +33 -0
- package/src/patient-registration/patient-registration-utils.ts +214 -0
- package/src/patient-registration/patient-registration.component.tsx +266 -0
- package/src/patient-registration/patient-registration.resource.test.tsx +22 -0
- package/src/patient-registration/patient-registration.resource.ts +198 -0
- package/src/patient-registration/patient-registration.scss +103 -0
- package/src/patient-registration/patient-registration.test.tsx +580 -0
- package/src/patient-registration/patient-registration.types.ts +322 -0
- package/src/patient-registration/section/death-info/death-info-section.component.tsx +36 -0
- package/src/patient-registration/section/death-info/death-info-section.test.tsx +47 -0
- package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
- package/src/patient-registration/section/demographics/demographics-section.test.tsx +98 -0
- package/src/patient-registration/section/generic-section.component.tsx +17 -0
- package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +234 -0
- package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +113 -0
- package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +78 -0
- package/src/patient-registration/section/patient-relationships/relationships.scss +35 -0
- package/src/patient-registration/section/section-wrapper.component.tsx +40 -0
- package/src/patient-registration/section/section.component.tsx +23 -0
- package/src/patient-registration/section/section.scss +21 -0
- package/src/patient-registration/ui-components/overlay/overlay.component.tsx +51 -0
- package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
- package/src/patient-registration/validation/patient-registration-validation.test.ts +205 -0
- package/src/patient-registration/validation/patient-registration-validation.ts +123 -0
- package/src/resource.ts +12 -0
- package/src/resources-context.ts +14 -0
- package/src/root.component.tsx +63 -0
- package/src/root.scss +7 -0
- package/src/routes.json +61 -0
- package/src/widgets/cancel-patient-edit.modal.tsx +33 -0
- package/src/widgets/cancel-patient-edit.test.tsx +22 -0
- package/src/widgets/delete-identifier-confirmation.modal.tsx +48 -0
- package/src/widgets/delete-identifier-confirmation.test.tsx +32 -0
- package/src/widgets/edit-patient-details-button.component.tsx +33 -0
- package/src/widgets/edit-patient-details-button.scss +3 -0
- package/src/widgets/edit-patient-details-button.test.tsx +35 -0
- package/translations/am.json +120 -0
- package/translations/ar.json +120 -0
- package/translations/ar_SY.json +120 -0
- package/translations/bn.json +120 -0
- package/translations/cs.json +120 -0
- package/translations/de.json +120 -0
- package/translations/en.json +120 -0
- package/translations/en_US.json +120 -0
- package/translations/es.json +120 -0
- package/translations/es_MX.json +120 -0
- package/translations/fr.json +120 -0
- package/translations/he.json +120 -0
- package/translations/hi.json +120 -0
- package/translations/hi_IN.json +120 -0
- package/translations/id.json +120 -0
- package/translations/it.json +120 -0
- package/translations/ka.json +120 -0
- package/translations/km.json +120 -0
- package/translations/ku.json +120 -0
- package/translations/ky.json +120 -0
- package/translations/lg.json +120 -0
- package/translations/ne.json +120 -0
- package/translations/pl.json +120 -0
- package/translations/pt.json +120 -0
- package/translations/pt_BR.json +120 -0
- package/translations/qu.json +120 -0
- package/translations/ro_RO.json +120 -0
- package/translations/ru_RU.json +120 -0
- package/translations/si.json +120 -0
- package/translations/sq.json +120 -0
- package/translations/sw.json +120 -0
- package/translations/sw_KE.json +120 -0
- package/translations/tr.json +120 -0
- package/translations/tr_TR.json +120 -0
- package/translations/uk.json +120 -0
- package/translations/uz.json +120 -0
- package/translations/uz@Latn.json +120 -0
- package/translations/uz_UZ.json +120 -0
- package/translations/vi.json +120 -0
- package/translations/zh.json +120 -0
- package/translations/zh_CN.json +120 -0
- package/translations/zh_TW.json +120 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import { getConfig } from '@openmrs/esm-framework';
|
|
3
|
+
import { type RegistrationConfig } from '../../config-schema';
|
|
4
|
+
import { getValidationSchema } from './patient-registration-validation';
|
|
5
|
+
|
|
6
|
+
const mockGetConfig = jest.mocked(getConfig);
|
|
7
|
+
|
|
8
|
+
describe('Patient registration validation', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockGetConfig.mockResolvedValue({
|
|
11
|
+
fieldConfigurations: {
|
|
12
|
+
gender: [
|
|
13
|
+
{
|
|
14
|
+
label: 'M',
|
|
15
|
+
value: 'male',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
label: 'F',
|
|
19
|
+
value: 'female',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: 'O',
|
|
23
|
+
value: 'other',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
label: 'U',
|
|
27
|
+
value: 'unknown',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const validFormValues = {
|
|
35
|
+
additionalFamilyName: '',
|
|
36
|
+
additionalGivenName: '',
|
|
37
|
+
birthdate: new Date('1990-01-01'),
|
|
38
|
+
birthdateEstimated: false,
|
|
39
|
+
isDead: false,
|
|
40
|
+
causeOfDeath: null,
|
|
41
|
+
deathDate: null,
|
|
42
|
+
email: 'john.doe@example.com',
|
|
43
|
+
familyName: 'Doe',
|
|
44
|
+
gender: 'male',
|
|
45
|
+
givenName: 'John',
|
|
46
|
+
identifiers: {
|
|
47
|
+
nationalId: {
|
|
48
|
+
required: true,
|
|
49
|
+
identifierValue: '123456789',
|
|
50
|
+
},
|
|
51
|
+
passportId: {
|
|
52
|
+
required: false,
|
|
53
|
+
identifierValue: '',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const validateFormValues = async (formValues) => {
|
|
59
|
+
const config = (await getConfig('@openmrs/esm-patient-registration-app')) as unknown as RegistrationConfig;
|
|
60
|
+
const mockT = (key: string, defaultValue: string) => defaultValue;
|
|
61
|
+
|
|
62
|
+
const validationSchema = getValidationSchema(config, mockT);
|
|
63
|
+
try {
|
|
64
|
+
await validationSchema.validate(formValues, { abortEarly: false });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return err;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
it('should allow valid form values', async () => {
|
|
71
|
+
const validationError = await validateFormValues(validFormValues);
|
|
72
|
+
expect(validationError).toBeFalsy();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should require givenName', async () => {
|
|
76
|
+
const invalidFormValues = {
|
|
77
|
+
...validFormValues,
|
|
78
|
+
givenName: '',
|
|
79
|
+
};
|
|
80
|
+
const validationError = await validateFormValues(invalidFormValues);
|
|
81
|
+
expect(validationError.errors).toContain('Given name is required');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should require familyName', async () => {
|
|
85
|
+
const invalidFormValues = {
|
|
86
|
+
...validFormValues,
|
|
87
|
+
familyName: '',
|
|
88
|
+
};
|
|
89
|
+
const validationError = await validateFormValues(invalidFormValues);
|
|
90
|
+
expect(validationError.errors).toContain('Family name is required');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should require additionalGivenName when addNameInLocalLanguage is true', async () => {
|
|
94
|
+
const invalidFormValues = {
|
|
95
|
+
...validFormValues,
|
|
96
|
+
addNameInLocalLanguage: true,
|
|
97
|
+
additionalGivenName: '',
|
|
98
|
+
};
|
|
99
|
+
const validationError = await validateFormValues(invalidFormValues);
|
|
100
|
+
expect(validationError.errors).toContain('Given name is required');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should require additionalFamilyName when addNameInLocalLanguage is true', async () => {
|
|
104
|
+
const invalidFormValues = {
|
|
105
|
+
...validFormValues,
|
|
106
|
+
addNameInLocalLanguage: true,
|
|
107
|
+
additionalFamilyName: '',
|
|
108
|
+
};
|
|
109
|
+
const validationError = await validateFormValues(invalidFormValues);
|
|
110
|
+
expect(validationError.errors).toContain('Family name is required');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should require gender', async () => {
|
|
114
|
+
const invalidFormValues = {
|
|
115
|
+
...validFormValues,
|
|
116
|
+
gender: '',
|
|
117
|
+
};
|
|
118
|
+
const validationError = await validateFormValues(invalidFormValues);
|
|
119
|
+
expect(validationError.errors).toContain('Gender unspecified');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should allow female as a valid gender', async () => {
|
|
123
|
+
const validFormValuesWithOtherGender = {
|
|
124
|
+
...validFormValues,
|
|
125
|
+
gender: 'female',
|
|
126
|
+
};
|
|
127
|
+
const validationError = await validateFormValues(validFormValuesWithOtherGender);
|
|
128
|
+
expect(validationError).toBeFalsy();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should allow other as a valid gender', async () => {
|
|
132
|
+
const validFormValuesWithOtherGender = {
|
|
133
|
+
...validFormValues,
|
|
134
|
+
gender: 'other',
|
|
135
|
+
};
|
|
136
|
+
const validationError = await validateFormValues(validFormValuesWithOtherGender);
|
|
137
|
+
expect(validationError).toBeFalsy();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should allow unknown as a valid gender', async () => {
|
|
141
|
+
const validFormValuesWithOtherGender = {
|
|
142
|
+
...validFormValues,
|
|
143
|
+
gender: 'unknown',
|
|
144
|
+
};
|
|
145
|
+
const validationError = await validateFormValues(validFormValuesWithOtherGender);
|
|
146
|
+
expect(validationError).toBeFalsy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should throw an error when date of birth is a future date', async () => {
|
|
150
|
+
const invalidFormValues = {
|
|
151
|
+
...validFormValues,
|
|
152
|
+
birthdate: new Date('2100-01-01'),
|
|
153
|
+
};
|
|
154
|
+
const validationError = await validateFormValues(invalidFormValues);
|
|
155
|
+
expect(validationError.errors).toContain('Birthday cannot be in future');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should throw an error when date of birth is more than 140 years ago', async () => {
|
|
159
|
+
const invalidFormValues = {
|
|
160
|
+
...validFormValues,
|
|
161
|
+
birthdate: dayjs().subtract(141, 'years').toDate(),
|
|
162
|
+
};
|
|
163
|
+
const validationError = await validateFormValues(invalidFormValues);
|
|
164
|
+
expect(validationError.errors).toContain('Birthday cannot be more than 140 years ago');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should require yearsEstimated when birthdateEstimated is true', async () => {
|
|
168
|
+
const invalidFormValues = {
|
|
169
|
+
...validFormValues,
|
|
170
|
+
birthdateEstimated: true,
|
|
171
|
+
};
|
|
172
|
+
const validationError = await validateFormValues(invalidFormValues);
|
|
173
|
+
expect(validationError.errors).toContain('Estimated years required');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should throw an error when monthEstimated is negative', async () => {
|
|
177
|
+
const invalidFormValues = {
|
|
178
|
+
...validFormValues,
|
|
179
|
+
birthdateEstimated: true,
|
|
180
|
+
yearsEstimated: 0,
|
|
181
|
+
monthsEstimated: -1,
|
|
182
|
+
};
|
|
183
|
+
const validationError = await validateFormValues(invalidFormValues);
|
|
184
|
+
expect(validationError.errors).toContain('Estimated months cannot be negative');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should throw an error when yearsEstimated is more than 140', async () => {
|
|
188
|
+
const invalidFormValues = {
|
|
189
|
+
...validFormValues,
|
|
190
|
+
birthdateEstimated: true,
|
|
191
|
+
yearsEstimated: 141,
|
|
192
|
+
};
|
|
193
|
+
const validationError = await validateFormValues(invalidFormValues);
|
|
194
|
+
expect(validationError.errors).toContain('Estimated years cannot be more than 140');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should throw an error when deathDate is in future', async () => {
|
|
198
|
+
const invalidFormValues = {
|
|
199
|
+
...validFormValues,
|
|
200
|
+
deathDate: new Date('2100-01-01'),
|
|
201
|
+
};
|
|
202
|
+
const validationError = await validateFormValues(invalidFormValues);
|
|
203
|
+
expect(validationError.errors).toContain('Death date cannot be in future');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import * as Yup from 'yup';
|
|
3
|
+
import mapValues from 'lodash/mapValues';
|
|
4
|
+
import { type RegistrationConfig } from '../../config-schema';
|
|
5
|
+
import { type FormValues } from '../patient-registration.types';
|
|
6
|
+
import { getDatetime } from '../patient-registration.resource';
|
|
7
|
+
|
|
8
|
+
export function getValidationSchema(config: RegistrationConfig, t: (key: string, defaultValue: string) => string) {
|
|
9
|
+
return Yup.object({
|
|
10
|
+
givenName: Yup.string().required(t('givenNameRequired', 'Given name is required')),
|
|
11
|
+
familyName: Yup.string().required(t('familyNameRequired', 'Family name is required')),
|
|
12
|
+
additionalGivenName: Yup.string().when('addNameInLocalLanguage', {
|
|
13
|
+
is: true,
|
|
14
|
+
then: Yup.string().required(t('givenNameRequired', 'Given name is required')),
|
|
15
|
+
otherwise: Yup.string().notRequired(),
|
|
16
|
+
}),
|
|
17
|
+
additionalFamilyName: Yup.string().when('addNameInLocalLanguage', {
|
|
18
|
+
is: true,
|
|
19
|
+
then: Yup.string().required(t('familyNameRequired', 'Family name is required')),
|
|
20
|
+
otherwise: Yup.string().notRequired(),
|
|
21
|
+
}),
|
|
22
|
+
gender: Yup.string()
|
|
23
|
+
.oneOf(
|
|
24
|
+
config.fieldConfigurations.gender.map((g) => g.value),
|
|
25
|
+
t('genderUnspecified', 'Gender unspecified'),
|
|
26
|
+
)
|
|
27
|
+
.required(t('genderRequired', 'Gender is required')),
|
|
28
|
+
birthdate: Yup.date().when('birthdateEstimated', {
|
|
29
|
+
is: false,
|
|
30
|
+
then: Yup.date()
|
|
31
|
+
.required(t('birthdayRequired', 'Birthday is required'))
|
|
32
|
+
.max(Date(), t('birthdayNotInTheFuture', 'Birthday cannot be in future'))
|
|
33
|
+
.min(
|
|
34
|
+
dayjs().subtract(140, 'years').toDate(),
|
|
35
|
+
t('birthdayNotOver140YearsAgo', 'Birthday cannot be more than 140 years ago'),
|
|
36
|
+
)
|
|
37
|
+
.nullable(),
|
|
38
|
+
otherwise: Yup.date().nullable(),
|
|
39
|
+
}),
|
|
40
|
+
yearsEstimated: Yup.number().when('birthdateEstimated', {
|
|
41
|
+
is: true,
|
|
42
|
+
then: Yup.number()
|
|
43
|
+
.required(t('yearsEstimateRequired', 'Estimated years required'))
|
|
44
|
+
.min(0, t('negativeYears', 'Estimated years cannot be negative'))
|
|
45
|
+
.max(140, t('nonsensicalYears', 'Estimated years cannot be more than 140')),
|
|
46
|
+
otherwise: Yup.number().nullable(),
|
|
47
|
+
}),
|
|
48
|
+
monthsEstimated: Yup.number().min(0, t('negativeMonths', 'Estimated months cannot be negative')),
|
|
49
|
+
isDead: Yup.boolean(),
|
|
50
|
+
deathDate: Yup.date()
|
|
51
|
+
.when('isDead', {
|
|
52
|
+
is: true,
|
|
53
|
+
then: Yup.date().required(t('deathDateRequired', 'Death date is required')),
|
|
54
|
+
otherwise: Yup.date().nullable(),
|
|
55
|
+
})
|
|
56
|
+
.max(new Date(), t('deathDateInFuture', 'Death date cannot be in future'))
|
|
57
|
+
.test(
|
|
58
|
+
'deathDate-after-birthdate',
|
|
59
|
+
t('deathdayInvalidDate', 'Death date and time cannot be before the birthday'),
|
|
60
|
+
function (value) {
|
|
61
|
+
const { birthdate } = this.parent;
|
|
62
|
+
if (birthdate && value) {
|
|
63
|
+
return dayjs(value).isAfter(birthdate);
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
.test('deathDate-before-today', t('deathDateInFuture', 'Death date cannot be in future'), function (value) {
|
|
69
|
+
const { deathTime, deathTimeFormat } = this.parent;
|
|
70
|
+
if (value && deathTime && deathTimeFormat && /^(1[0-2]|0?[1-9]):([0-5]?[0-9])$/.test(deathTime)) {
|
|
71
|
+
return dayjs(getDatetime(value, deathTime, deathTimeFormat)).isBefore(dayjs());
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}),
|
|
75
|
+
deathTime: Yup.string()
|
|
76
|
+
.when('isDead', {
|
|
77
|
+
is: true,
|
|
78
|
+
then: Yup.string().required(t('deathTimeRequired', 'Death time is required')),
|
|
79
|
+
otherwise: Yup.string().nullable(),
|
|
80
|
+
})
|
|
81
|
+
.matches(/^(1[0-2]|0?[1-9]):([0-5]?[0-9])$/, t('deathTimeInvalid', "Time doesn't match the format 'hh:mm'")),
|
|
82
|
+
|
|
83
|
+
deathTimeFormat: Yup.string()
|
|
84
|
+
.when('isDead', {
|
|
85
|
+
is: true,
|
|
86
|
+
then: Yup.string().required(t('deathTimeFormatRequired', 'Time format is required')),
|
|
87
|
+
otherwise: Yup.string().nullable(),
|
|
88
|
+
})
|
|
89
|
+
.oneOf(['AM', 'PM'], t('deathTimeFormatInvalid', 'Time format is invalid')),
|
|
90
|
+
|
|
91
|
+
deathCause: Yup.string().when('isDead', {
|
|
92
|
+
is: true,
|
|
93
|
+
then: Yup.string().required(t('deathCauseRequired', 'Cause of death is required')),
|
|
94
|
+
otherwise: Yup.string().nullable(),
|
|
95
|
+
}),
|
|
96
|
+
nonCodedCauseOfDeath: Yup.string().when(['isDead', 'deathCause'], {
|
|
97
|
+
is: (isDead, deathCause) => isDead && deathCause === config.freeTextFieldConceptUuid,
|
|
98
|
+
then: Yup.string().required(t('nonCodedCauseOfDeathRequired', 'Cause of death is required')),
|
|
99
|
+
otherwise: Yup.string().nullable(),
|
|
100
|
+
}),
|
|
101
|
+
email: Yup.string().optional().email(t('invalidEmail', 'Invalid email')),
|
|
102
|
+
identifiers: Yup.lazy((obj: FormValues['identifiers']) =>
|
|
103
|
+
Yup.object(
|
|
104
|
+
mapValues(obj, () =>
|
|
105
|
+
Yup.object({
|
|
106
|
+
required: Yup.bool(),
|
|
107
|
+
identifierValue: Yup.string().when('required', {
|
|
108
|
+
is: true,
|
|
109
|
+
then: Yup.string().required(t('identifierValueRequired', 'Identifier value is required')),
|
|
110
|
+
otherwise: Yup.string().notRequired(),
|
|
111
|
+
}),
|
|
112
|
+
}),
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
relationships: Yup.array().of(
|
|
117
|
+
Yup.object().shape({
|
|
118
|
+
relatedPersonUuid: Yup.string().required(),
|
|
119
|
+
relationshipType: Yup.string().required(),
|
|
120
|
+
}),
|
|
121
|
+
),
|
|
122
|
+
});
|
|
123
|
+
}
|
package/src/resource.ts
ADDED
|
@@ -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,14 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { type Resources } from './offline.resources';
|
|
3
|
+
|
|
4
|
+
export const ResourcesContext = createContext<Resources>(null);
|
|
5
|
+
|
|
6
|
+
export const ResourcesContextProvider = ResourcesContext.Provider;
|
|
7
|
+
|
|
8
|
+
export const useResourcesContext = () => {
|
|
9
|
+
const context = useContext(ResourcesContext);
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error('useResourcesContext must be used within a ResourcesContextProvider');
|
|
12
|
+
}
|
|
13
|
+
return context;
|
|
14
|
+
};
|
|
@@ -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
|
+
fetchAddressTemplate,
|
|
9
|
+
fetchAllRelationshipTypes,
|
|
10
|
+
fetchPatientIdentifierTypesWithSources,
|
|
11
|
+
} from './offline.resources';
|
|
12
|
+
import { ResourcesContextProvider } from './resources-context';
|
|
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
|
+
<ResourcesContextProvider
|
|
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
|
+
</ResourcesContextProvider>
|
|
60
|
+
</Grid>
|
|
61
|
+
</main>
|
|
62
|
+
);
|
|
63
|
+
}
|
package/src/root.scss
ADDED
package/src/routes.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.openmrs.org/routes.schema.json",
|
|
3
|
+
"backendDependencies": {
|
|
4
|
+
"webservices.rest": ">=2.2.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
|
+
"order": 30
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"component": "patientPhotoExtension",
|
|
31
|
+
"name": "patient-photo-widget",
|
|
32
|
+
"slot": "patient-photo-slot",
|
|
33
|
+
"online": true,
|
|
34
|
+
"offline": true
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"component": "editPatientDetailsButton",
|
|
38
|
+
"name": "edit-patient-details-button",
|
|
39
|
+
"slot": "patient-actions-slot",
|
|
40
|
+
"online": true,
|
|
41
|
+
"offline": true
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"component": "editPatientDetailsButton",
|
|
45
|
+
"name": "edit-patient-details-button",
|
|
46
|
+
"slot": "patient-search-actions-slot",
|
|
47
|
+
"online": true,
|
|
48
|
+
"offline": true
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"modals": [
|
|
52
|
+
{
|
|
53
|
+
"name": "cancel-patient-edit-modal",
|
|
54
|
+
"component": "cancelPatientEditModal"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "delete-identifier-confirmation-modal",
|
|
58
|
+
"component": "deleteIdentifierConfirmationModal"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
|
|
4
|
+
|
|
5
|
+
interface CancelPatientEditPropsModal {
|
|
6
|
+
close(): void;
|
|
7
|
+
onConfirm(): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const CancelPatientEditModal: React.FC<CancelPatientEditPropsModal> = ({ close, onConfirm }) => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
<ModalHeader
|
|
15
|
+
closeModal={close}
|
|
16
|
+
title={t('confirmDiscardChangesTitle', 'Are you sure you want to discard these changes?')}
|
|
17
|
+
/>
|
|
18
|
+
<ModalBody>
|
|
19
|
+
<p>{t('confirmDiscardChangesBody', 'Your unsaved changes will be lost if you proceed to discard the form')}.</p>
|
|
20
|
+
</ModalBody>
|
|
21
|
+
<ModalFooter>
|
|
22
|
+
<Button kind="secondary" onClick={close}>
|
|
23
|
+
{t('cancel', 'Cancel')}
|
|
24
|
+
</Button>
|
|
25
|
+
<Button kind="danger" onClick={onConfirm}>
|
|
26
|
+
{t('discard', 'Discard')}
|
|
27
|
+
</Button>
|
|
28
|
+
</ModalFooter>
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default CancelPatientEditModal;
|
|
@@ -0,0 +1,22 @@
|
|
|
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.modal';
|
|
5
|
+
|
|
6
|
+
describe('CancelPatientEdit modal', () => {
|
|
7
|
+
const mockClose = jest.fn();
|
|
8
|
+
const mockOnConfirm = jest.fn();
|
|
9
|
+
|
|
10
|
+
it('renders the modal and triggers close and onConfirm functions', async () => {
|
|
11
|
+
const user = userEvent.setup();
|
|
12
|
+
render(<CancelPatientEdit close={mockClose} onConfirm={mockOnConfirm} />);
|
|
13
|
+
|
|
14
|
+
const cancelButton = screen.getByRole('button', { name: /Cancel/i });
|
|
15
|
+
await user.click(cancelButton);
|
|
16
|
+
expect(mockClose).toHaveBeenCalledTimes(1);
|
|
17
|
+
|
|
18
|
+
const discardButton = screen.getByRole('button', { name: /discard/i });
|
|
19
|
+
await user.click(discardButton);
|
|
20
|
+
expect(mockOnConfirm).toHaveBeenCalledTimes(1);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button, ModalBody, ModalHeader, ModalFooter } from '@carbon/react';
|
|
4
|
+
|
|
5
|
+
interface DeleteIdentifierConfirmationModalProps {
|
|
6
|
+
closeModal: () => void;
|
|
7
|
+
deleteIdentifier: (x: boolean) => void;
|
|
8
|
+
identifierName: string;
|
|
9
|
+
identifierValue: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DeleteIdentifierConfirmationModal: React.FC<DeleteIdentifierConfirmationModalProps> = ({
|
|
13
|
+
closeModal,
|
|
14
|
+
deleteIdentifier,
|
|
15
|
+
identifierName,
|
|
16
|
+
identifierValue,
|
|
17
|
+
}) => {
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<ModalHeader
|
|
23
|
+
closeModal={closeModal}
|
|
24
|
+
title={t('deleteIdentifierModalHeading', 'Delete identifier?')}></ModalHeader>
|
|
25
|
+
<ModalBody>
|
|
26
|
+
<p>
|
|
27
|
+
{identifierName && identifierValue && (
|
|
28
|
+
<span>
|
|
29
|
+
<strong>{identifierName}</strong>
|
|
30
|
+
{t('deleteIdentifierModalText', ' has a value of ')} <strong>{identifierValue}</strong>.{' '}
|
|
31
|
+
</span>
|
|
32
|
+
)}
|
|
33
|
+
{t('confirmIdentifierDeletionText', 'Are you sure you want to delete this identifier?')}
|
|
34
|
+
</p>
|
|
35
|
+
</ModalBody>
|
|
36
|
+
<ModalFooter>
|
|
37
|
+
<Button kind="secondary" size="lg" onClick={closeModal}>
|
|
38
|
+
{t('cancel', 'Cancel')}
|
|
39
|
+
</Button>
|
|
40
|
+
<Button kind="danger" size="lg" onClick={() => deleteIdentifier(true)}>
|
|
41
|
+
{t('removeIdentifierButton', 'Remove identifier')}
|
|
42
|
+
</Button>
|
|
43
|
+
</ModalFooter>
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default DeleteIdentifierConfirmationModal;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import DeleteIdentifierConfirmationModal from './delete-identifier-confirmation.modal';
|
|
5
|
+
|
|
6
|
+
describe('DeleteIdentifierConfirmationModal', () => {
|
|
7
|
+
const mockDeleteIdentifier = jest.fn();
|
|
8
|
+
const closeModal = jest.fn();
|
|
9
|
+
const mockIdentifierName = 'Identifier Name';
|
|
10
|
+
const mockIdentifierValue = 'Identifier Value';
|
|
11
|
+
|
|
12
|
+
it('renders the modal and triggers deleteIdentifier function', async () => {
|
|
13
|
+
const user = userEvent.setup();
|
|
14
|
+
|
|
15
|
+
render(
|
|
16
|
+
<DeleteIdentifierConfirmationModal
|
|
17
|
+
closeModal={closeModal}
|
|
18
|
+
deleteIdentifier={mockDeleteIdentifier}
|
|
19
|
+
identifierName={mockIdentifierName}
|
|
20
|
+
identifierValue={mockIdentifierValue}
|
|
21
|
+
/>,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
|
25
|
+
await user.click(cancelButton);
|
|
26
|
+
expect(closeModal).toHaveBeenCalledTimes(1);
|
|
27
|
+
|
|
28
|
+
const removeButton = screen.getByRole('button', { name: /remove identifier/i });
|
|
29
|
+
await user.click(removeButton);
|
|
30
|
+
expect(mockDeleteIdentifier).toHaveBeenCalledWith(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { navigate } from '@openmrs/esm-framework';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import styles from './edit-patient-details-button.scss';
|
|
5
|
+
|
|
6
|
+
interface EditPatientDetailsButtonProps {
|
|
7
|
+
onTransition?: () => void;
|
|
8
|
+
patientUuid: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const EditPatientDetailsButton: React.FC<EditPatientDetailsButtonProps> = ({ patientUuid, onTransition }) => {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const handleClick = React.useCallback(() => {
|
|
14
|
+
navigate({ to: `\${openmrsSpaBase}/patient/${patientUuid}/edit` });
|
|
15
|
+
onTransition?.();
|
|
16
|
+
}, [onTransition, patientUuid]);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<li className="cds--overflow-menu-options__option">
|
|
20
|
+
<button
|
|
21
|
+
className="cds--overflow-menu-options__btn"
|
|
22
|
+
role="menuitem"
|
|
23
|
+
data-floating-menu-primary-focus
|
|
24
|
+
onClick={handleClick}>
|
|
25
|
+
<span className="cds--overflow-menu-options__option-content">
|
|
26
|
+
{t('editPatientDetails', 'Edit patient details')}
|
|
27
|
+
</span>
|
|
28
|
+
</button>
|
|
29
|
+
</li>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default EditPatientDetailsButton;
|