@ampath/esm-patient-registration-app 6.0.1-pre.96 → 9.2.0-next.12
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/dist/1119.js +1 -0
- package/dist/1197.js +1 -0
- package/dist/21.js +1 -0
- package/dist/21.js.map +1 -0
- package/dist/2146.js +1 -0
- package/dist/2372.js +1 -0
- package/dist/2372.js.map +1 -0
- package/dist/2470.js +1 -0
- package/dist/2470.js.map +1 -0
- package/dist/2690.js +1 -0
- package/dist/2913.js +2 -0
- package/dist/{913.js.LICENSE.txt → 2913.js.LICENSE.txt} +3 -23
- package/dist/2913.js.map +1 -0
- package/dist/3093.js +1 -0
- package/dist/3093.js.map +1 -0
- package/dist/3099.js +1 -0
- package/dist/3144.js +2 -0
- package/dist/3144.js.LICENSE.txt +19 -0
- package/dist/3144.js.map +1 -0
- package/dist/320.js +2 -0
- package/dist/{876.js.LICENSE.txt → 320.js.LICENSE.txt} +2 -3
- package/dist/320.js.map +1 -0
- package/dist/3464.js +1 -0
- package/dist/3464.js.map +1 -0
- package/dist/3474.js +2 -0
- package/dist/3474.js.LICENSE.txt +8 -0
- package/dist/3474.js.map +1 -0
- package/dist/3584.js +1 -0
- package/dist/4041.js +2 -0
- package/dist/4041.js.map +1 -0
- package/dist/4055.js +1 -0
- package/dist/4132.js +1 -0
- package/dist/4300.js +1 -0
- package/dist/4335.js +1 -0
- package/dist/4463.js +1 -0
- package/dist/4463.js.map +1 -0
- package/dist/4618.js +1 -0
- package/dist/4652.js +1 -0
- package/dist/4944.js +1 -0
- package/dist/5173.js +1 -0
- package/dist/5220.js +2 -0
- package/dist/5220.js.LICENSE.txt +29 -0
- package/dist/5220.js.map +1 -0
- package/dist/5241.js +1 -0
- package/dist/5442.js +1 -0
- package/dist/5661.js +1 -0
- package/dist/6022.js +1 -0
- package/dist/6078.js +2 -0
- package/dist/6078.js.LICENSE.txt +9 -0
- package/dist/6078.js.map +1 -0
- package/dist/627.js +1 -0
- package/dist/627.js.map +1 -0
- package/dist/6276.js +1 -0
- package/dist/6276.js.map +1 -0
- package/dist/6468.js +1 -0
- package/dist/6679.js +1 -0
- package/dist/6737.js +2 -0
- package/dist/6737.js.LICENSE.txt +9 -0
- package/dist/6737.js.map +1 -0
- package/dist/6840.js +1 -0
- package/dist/6859.js +1 -0
- package/dist/7092.js +1 -0
- package/dist/7092.js.map +1 -0
- package/dist/7097.js +1 -0
- package/dist/7159.js +1 -0
- package/dist/723.js +1 -0
- package/dist/7495.js +2 -0
- package/dist/7495.js.LICENSE.txt +9 -0
- package/dist/7495.js.map +1 -0
- package/dist/7617.js +1 -0
- package/dist/795.js +1 -0
- package/dist/8163.js +1 -0
- package/dist/8349.js +1 -0
- package/dist/8404.js +2 -0
- package/dist/{629.js.LICENSE.txt → 8404.js.LICENSE.txt} +9 -3
- package/dist/8404.js.map +1 -0
- package/dist/8434.js +1 -0
- package/dist/8434.js.map +1 -0
- package/dist/8618.js +1 -0
- package/dist/89.js +2 -0
- package/dist/89.js.LICENSE.txt +9 -0
- package/dist/89.js.map +1 -0
- package/dist/890.js +1 -0
- package/dist/9214.js +1 -0
- package/dist/9538.js +1 -0
- package/dist/9569.js +1 -0
- package/dist/986.js +1 -0
- package/dist/9876.js +1 -0
- package/dist/9876.js.map +1 -0
- package/dist/9879.js +1 -0
- package/dist/9895.js +1 -0
- package/dist/9900.js +1 -0
- package/dist/9913.js +1 -0
- package/dist/main.js +1 -1
- package/dist/main.js.LICENSE.txt +36 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-patient-registration-app.js +1 -0
- package/dist/openmrs-esm-patient-registration-app.js.buildmanifest.json +1576 -0
- package/dist/openmrs-esm-patient-registration-app.js.map +1 -0
- package/dist/routes.json +1 -1
- package/package.json +16 -15
- package/src/{add-patient-link.tsx → add-patient-link.extension.tsx} +4 -2
- package/src/add-patient-link.test.tsx +6 -10
- package/src/config-schema.ts +109 -55
- package/src/constants.ts +1 -1
- package/src/declarations.d.ts +5 -4
- package/src/index.ts +10 -29
- package/src/nav-link.test.tsx +3 -3
- package/src/offline.resources.ts +26 -18
- package/src/patient-photo.extension.tsx +3 -1
- package/src/patient-registration/field/address/address-field.component.tsx +58 -37
- package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +16 -18
- package/src/patient-registration/field/address/address-hierarchy.resource.tsx +3 -3
- package/src/patient-registration/field/address/address-hierarchy.test.tsx +290 -0
- package/src/patient-registration/field/address/address-search.component.tsx +7 -5
- package/src/patient-registration/field/address/address-search.scss +5 -5
- package/src/patient-registration/field/address/address-search.test.tsx +140 -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 +3 -9
- package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx +84 -0
- package/src/patient-registration/field/dob/dob.component.tsx +55 -50
- package/src/patient-registration/field/dob/dob.test.tsx +90 -0
- package/src/patient-registration/field/field.component.tsx +12 -6
- package/src/patient-registration/field/field.resource.ts +11 -4
- package/src/patient-registration/field/field.scss +69 -25
- package/src/patient-registration/field/field.test.tsx +329 -0
- package/src/patient-registration/field/gender/gender-field.component.tsx +14 -9
- package/src/patient-registration/field/gender/gender-field.test.tsx +73 -33
- package/src/patient-registration/field/id/id-field.component.tsx +24 -23
- package/src/patient-registration/field/id/id-field.test.tsx +147 -0
- package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +12 -10
- package/src/patient-registration/field/id/identifier-selection.scss +12 -8
- package/src/patient-registration/field/name/name-field.component.tsx +10 -5
- package/src/patient-registration/field/obs/obs-field.component.tsx +59 -2
- package/src/patient-registration/field/obs/obs-field.test.tsx +133 -39
- package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +3 -3
- 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 +19 -22
- package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +193 -0
- package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +90 -0
- package/src/patient-registration/form-manager.test.ts +91 -0
- package/src/patient-registration/form-manager.ts +49 -23
- package/src/patient-registration/input/basic-input/input/input.component.tsx +6 -2
- package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +5 -5
- 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 +73 -36
- package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +335 -0
- package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +3 -0
- package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +2 -11
- package/src/patient-registration/input/input.scss +17 -13
- package/src/patient-registration/patient-registration-context.ts +22 -11
- package/src/patient-registration/patient-registration-hooks.ts +158 -193
- package/src/patient-registration/patient-registration-utils.test.ts +33 -0
- package/src/patient-registration/patient-registration-utils.ts +11 -13
- package/src/patient-registration/patient-registration.component.tsx +87 -103
- package/src/patient-registration/{patient-registration.resource.testt.tsx → patient-registration.resource.test.tsx} +0 -4
- package/src/patient-registration/patient-registration.resource.ts +27 -3
- package/src/patient-registration/patient-registration.scss +27 -38
- package/src/patient-registration/patient-registration.test.tsx +579 -0
- package/src/patient-registration/patient-registration.types.ts +23 -25
- package/src/patient-registration/section/death-info/death-info-section.component.tsx +22 -17
- package/src/patient-registration/section/death-info/death-info-section.test.tsx +47 -0
- package/src/patient-registration/section/demographics/demographics-section.component.tsx +5 -5
- package/src/patient-registration/section/demographics/demographics-section.test.tsx +98 -0
- package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +8 -7
- package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +113 -0
- package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +28 -28
- package/src/patient-registration/section/patient-relationships/relationships.scss +4 -4
- package/src/patient-registration/section/section-wrapper.component.tsx +1 -1
- package/src/patient-registration/section/section.component.tsx +1 -1
- package/src/patient-registration/section/section.scss +21 -1
- package/src/patient-registration/ui-components/overlay/overlay.scss +8 -8
- package/src/patient-registration/validation/{patient-registration-validation.test.tsx → patient-registration-validation.test.ts} +71 -23
- package/src/patient-registration/validation/patient-registration-validation.ts +123 -0
- package/src/resources-context.ts +14 -0
- package/src/root.component.tsx +3 -3
- package/src/routes.json +10 -24
- 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-modal.testt.tsx → delete-identifier-confirmation.test.tsx} +5 -7
- package/src/widgets/edit-patient-details-button.component.tsx +0 -1
- package/src/widgets/edit-patient-details-button.test.tsx +35 -0
- package/translations/am.json +43 -35
- package/translations/ar.json +41 -33
- package/translations/ar_SY.json +119 -0
- package/translations/bn.json +119 -0
- package/translations/de.json +119 -0
- package/translations/en.json +44 -42
- package/translations/en_US.json +119 -0
- package/translations/es.json +69 -57
- package/translations/es_MX.json +119 -0
- package/translations/fr.json +74 -58
- package/translations/he.json +44 -40
- package/translations/hi.json +119 -0
- package/translations/hi_IN.json +119 -0
- package/translations/id.json +119 -0
- package/translations/it.json +119 -0
- package/translations/ka.json +119 -0
- package/translations/km.json +44 -40
- package/translations/ku.json +119 -0
- package/translations/ky.json +119 -0
- package/translations/lg.json +119 -0
- package/translations/ne.json +119 -0
- package/translations/pl.json +119 -0
- package/translations/pt.json +119 -0
- package/translations/pt_BR.json +119 -0
- package/translations/qu.json +119 -0
- package/translations/ro_RO.json +119 -0
- package/translations/ru_RU.json +119 -0
- package/translations/si.json +119 -0
- package/translations/sw.json +119 -0
- package/translations/sw_KE.json +119 -0
- package/translations/tr.json +119 -0
- package/translations/tr_TR.json +119 -0
- package/translations/uk.json +119 -0
- package/translations/uz.json +119 -0
- package/translations/uz@Latn.json +119 -0
- package/translations/uz_UZ.json +119 -0
- package/translations/vi.json +119 -0
- package/translations/zh.json +45 -23
- package/translations/zh_CN.json +39 -17
- package/.turbo/turbo-build.log +0 -40
- package/dist/132.js +0 -1
- package/dist/197.js +0 -1
- package/dist/236.js +0 -1
- package/dist/236.js.map +0 -1
- package/dist/300.js +0 -1
- package/dist/335.js +0 -1
- package/dist/372.js +0 -1
- package/dist/372.js.map +0 -1
- package/dist/41.js +0 -2
- package/dist/41.js.map +0 -1
- package/dist/449.js +0 -1
- package/dist/449.js.map +0 -1
- package/dist/464.js +0 -1
- package/dist/464.js.map +0 -1
- package/dist/495.js +0 -1
- package/dist/495.js.map +0 -1
- package/dist/55.js +0 -1
- package/dist/56.js +0 -1
- package/dist/56.js.map +0 -1
- package/dist/621.js +0 -1
- package/dist/621.js.map +0 -1
- package/dist/629.js +0 -2
- package/dist/629.js.map +0 -1
- package/dist/652.js +0 -1
- package/dist/661.js +0 -1
- package/dist/757.js +0 -1
- package/dist/757.js.map +0 -1
- package/dist/828.js +0 -1
- package/dist/828.js.map +0 -1
- package/dist/830.js +0 -1
- package/dist/830.js.map +0 -1
- package/dist/831.js +0 -2
- package/dist/831.js.LICENSE.txt +0 -3
- package/dist/831.js.map +0 -1
- package/dist/876.js +0 -2
- package/dist/876.js.map +0 -1
- package/dist/879.js +0 -1
- package/dist/913.js +0 -2
- package/dist/913.js.map +0 -1
- package/dist/927.js +0 -1
- package/dist/927.js.map +0 -1
- package/dist/99.js +0 -1
- package/dist/ampath-esm-patient-registration-app.js +0 -1
- package/dist/ampath-esm-patient-registration-app.js.buildmanifest.json +0 -694
- package/dist/ampath-esm-patient-registration-app.js.map +0 -1
- package/src/patient-registration/date-util.ts +0 -52
- package/src/patient-registration/field/person-attributes/custom-person-attribute-field.component.tsx +0 -56
- package/src/patient-registration/validation/patient-registration-validation.tsx +0 -60
- package/src/patient-verification/assets/counties.json +0 -236
- package/src/patient-verification/assets/verification-assets.ts +0 -11
- package/src/patient-verification/patient-verification-hook.tsx +0 -176
- package/src/patient-verification/patient-verification-utils.ts +0 -179
- package/src/patient-verification/patient-verification.component.tsx +0 -124
- package/src/patient-verification/patient-verification.scss +0 -25
- package/src/patient-verification/verification-modal/confirm-prompt.component.tsx +0 -72
- package/src/patient-verification/verification-modal/empty-prompt.component.tsx +0 -35
- package/src/patient-verification/verification-types.ts +0 -50
- package/src/widgets/cancel-patient-edit.component.tsx +0 -37
- package/src/widgets/delete-identifier-confirmation-modal.tsx +0 -41
- package/src/widgets/delete-identifier-modal.scss +0 -34
- /package/dist/{41.js.LICENSE.txt → 4041.js.LICENSE.txt} +0 -0
- /package/src/patient-registration/input/custom-input/identifier/{utils.testt.ts → utils.test.ts} +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Form, Formik } from 'formik';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import { type FieldDefinition } from '../../../config-schema';
|
|
5
|
+
import { usePersonAttributeType } from './person-attributes.resource';
|
|
6
|
+
import { useConceptAnswers } from '../field.resource';
|
|
7
|
+
import { PersonAttributeField } from './person-attribute-field.component';
|
|
8
|
+
|
|
9
|
+
jest.mock('./person-attributes.resource', () => ({
|
|
10
|
+
...jest.requireActual('./person-attributes.resource'),
|
|
11
|
+
usePersonAttributeType: jest.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
jest.mock('../field.resource', () => ({
|
|
15
|
+
...jest.requireActual('../field.resource'),
|
|
16
|
+
useConceptAnswers: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const mockUsePersonAttributeType = jest.mocked(usePersonAttributeType);
|
|
20
|
+
const mockUseConceptAnswers = jest.mocked(useConceptAnswers);
|
|
21
|
+
|
|
22
|
+
const mockPersonAttributeType = {
|
|
23
|
+
format: 'java.lang.String',
|
|
24
|
+
display: 'Referred by',
|
|
25
|
+
uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0',
|
|
26
|
+
name: 'Referred by',
|
|
27
|
+
description: 'The person who referred the patient',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let fieldDefinition: FieldDefinition = {
|
|
31
|
+
id: 'referredby',
|
|
32
|
+
label: 'Referred by',
|
|
33
|
+
type: 'person attribute',
|
|
34
|
+
uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0',
|
|
35
|
+
answerConceptSetUuid: '6682d17f-0777-45e4-a39b-93f77eb3531c',
|
|
36
|
+
validation: {
|
|
37
|
+
matches: '',
|
|
38
|
+
required: true,
|
|
39
|
+
},
|
|
40
|
+
showHeading: true,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe('PersonAttributeField', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
mockUsePersonAttributeType.mockReturnValue({
|
|
46
|
+
data: mockPersonAttributeType,
|
|
47
|
+
isLoading: false,
|
|
48
|
+
error: null,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('renders the text input field for String format', () => {
|
|
53
|
+
render(
|
|
54
|
+
<Formik initialValues={{}} onSubmit={() => {}}>
|
|
55
|
+
<Form>
|
|
56
|
+
<PersonAttributeField fieldDefinition={fieldDefinition} />
|
|
57
|
+
</Form>
|
|
58
|
+
</Formik>,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const input = screen.getByLabelText(/Referred by/i) as HTMLInputElement;
|
|
62
|
+
expect(screen.getByRole('heading')).toBeInTheDocument();
|
|
63
|
+
expect(input).toBeInTheDocument();
|
|
64
|
+
expect(input.type).toBe('text');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should not show heading if showHeading is false', () => {
|
|
68
|
+
fieldDefinition = {
|
|
69
|
+
...fieldDefinition,
|
|
70
|
+
showHeading: false,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
render(
|
|
74
|
+
<Formik initialValues={{}} onSubmit={() => {}}>
|
|
75
|
+
<Form>
|
|
76
|
+
<PersonAttributeField fieldDefinition={fieldDefinition} />
|
|
77
|
+
</Form>
|
|
78
|
+
</Formik>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('renders the coded attribute field for Concept format', () => {
|
|
85
|
+
fieldDefinition = {
|
|
86
|
+
id: 'referredby',
|
|
87
|
+
...fieldDefinition,
|
|
88
|
+
label: 'Referred by',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
mockUsePersonAttributeType.mockReturnValue({
|
|
92
|
+
data: { ...mockPersonAttributeType, format: 'org.openmrs.Concept' },
|
|
93
|
+
isLoading: false,
|
|
94
|
+
error: null,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
mockUseConceptAnswers.mockReturnValueOnce({
|
|
98
|
+
data: [
|
|
99
|
+
{ uuid: '1', display: 'Option 1' },
|
|
100
|
+
{ uuid: '2', display: 'Option 2' },
|
|
101
|
+
],
|
|
102
|
+
error: null,
|
|
103
|
+
isLoading: false,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
render(
|
|
107
|
+
<Formik initialValues={{}} onSubmit={() => {}}>
|
|
108
|
+
<Form>
|
|
109
|
+
<PersonAttributeField fieldDefinition={fieldDefinition} />
|
|
110
|
+
</Form>
|
|
111
|
+
</Formik>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const input = screen.getByLabelText(/Referred by/i) as HTMLInputElement;
|
|
115
|
+
expect(input).toBeInTheDocument();
|
|
116
|
+
expect(input.type).toBe('select-one');
|
|
117
|
+
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
|
118
|
+
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('renders an error notification if attribute type has unknown format', () => {
|
|
122
|
+
mockUsePersonAttributeType.mockReturnValue({
|
|
123
|
+
data: { ...mockPersonAttributeType, format: 'unknown' },
|
|
124
|
+
isLoading: false,
|
|
125
|
+
error: null,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
render(
|
|
129
|
+
<Formik initialValues={{}} onSubmit={() => {}}>
|
|
130
|
+
<Form>
|
|
131
|
+
<PersonAttributeField fieldDefinition={fieldDefinition} />
|
|
132
|
+
</Form>
|
|
133
|
+
</Formik>,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(screen.getByText('Error')).toBeInTheDocument();
|
|
137
|
+
expect(screen.getByText(/Patient attribute type has unknown format/i)).toBeInTheDocument();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('renders an error notification if unable to fetch attribute type', () => {
|
|
141
|
+
mockUsePersonAttributeType.mockReturnValue({
|
|
142
|
+
data: null,
|
|
143
|
+
isLoading: false,
|
|
144
|
+
error: new Error('Failed to fetch attribute type'),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
fieldDefinition = {
|
|
148
|
+
id: 'referredBy',
|
|
149
|
+
uuid: 'attribute-uuid',
|
|
150
|
+
label: 'Attribute',
|
|
151
|
+
showHeading: false,
|
|
152
|
+
type: 'person attribute',
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
render(
|
|
156
|
+
<Formik initialValues={{}} onSubmit={() => {}}>
|
|
157
|
+
<Form>
|
|
158
|
+
<PersonAttributeField fieldDefinition={fieldDefinition} />
|
|
159
|
+
</Form>
|
|
160
|
+
</Formik>,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
expect(screen.getByText('Error')).toBeInTheDocument();
|
|
164
|
+
expect(screen.getByText(/Unable to fetch person attribute type/i)).toBeInTheDocument();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('renders a skeleton if attribute type is loading', async () => {
|
|
168
|
+
mockUsePersonAttributeType.mockReturnValue({
|
|
169
|
+
data: null,
|
|
170
|
+
isLoading: true,
|
|
171
|
+
error: null,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
fieldDefinition = {
|
|
175
|
+
id: 'referredBy',
|
|
176
|
+
uuid: 'attribute-uuid',
|
|
177
|
+
label: 'Attribute',
|
|
178
|
+
showHeading: true,
|
|
179
|
+
type: 'person attribute',
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
render(
|
|
183
|
+
<Formik initialValues={{}} onSubmit={() => {}}>
|
|
184
|
+
<Form>
|
|
185
|
+
<PersonAttributeField fieldDefinition={fieldDefinition} />
|
|
186
|
+
</Form>
|
|
187
|
+
</Formik>,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
await screen.findByRole('heading', { name: /attribute/i });
|
|
191
|
+
expect(screen.queryByLabelText(/referred by/i)).not.toBeInTheDocument();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { Form, Formik } from 'formik';
|
|
5
|
+
import { TextPersonAttributeField } from './text-person-attribute-field.component';
|
|
6
|
+
|
|
7
|
+
describe('TextPersonAttributeField', () => {
|
|
8
|
+
const mockPersonAttributeType = {
|
|
9
|
+
format: 'java.lang.String',
|
|
10
|
+
display: 'Referred by',
|
|
11
|
+
uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0',
|
|
12
|
+
description: 'Referred by',
|
|
13
|
+
name: 'Referred by',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
it('renders the input field with a label', () => {
|
|
17
|
+
render(
|
|
18
|
+
<Formik initialValues={{}} onSubmit={() => {}}>
|
|
19
|
+
<Form>
|
|
20
|
+
<TextPersonAttributeField
|
|
21
|
+
id="attributeId"
|
|
22
|
+
personAttributeType={mockPersonAttributeType}
|
|
23
|
+
label="Custom Label"
|
|
24
|
+
/>
|
|
25
|
+
</Form>
|
|
26
|
+
</Formik>,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
expect(screen.getByRole('textbox', { name: /custom label \(optional\)/i })).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders the input field with the default label if label prop is not provided', () => {
|
|
33
|
+
render(
|
|
34
|
+
<Formik initialValues={{}} onSubmit={() => {}}>
|
|
35
|
+
<Form>
|
|
36
|
+
<TextPersonAttributeField id="attributeId" personAttributeType={mockPersonAttributeType} />
|
|
37
|
+
</Form>
|
|
38
|
+
</Formik>,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(screen.getByRole('textbox', { name: /referred by \(optional\)/i })).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('validates the input with the provided validationRegex', async () => {
|
|
45
|
+
const user = userEvent.setup();
|
|
46
|
+
const validationRegex = '^[A-Z]+$'; // Accepts only uppercase letters
|
|
47
|
+
|
|
48
|
+
render(
|
|
49
|
+
<Formik initialValues={{}} onSubmit={() => {}}>
|
|
50
|
+
<Form>
|
|
51
|
+
<TextPersonAttributeField
|
|
52
|
+
id="attributeId"
|
|
53
|
+
personAttributeType={mockPersonAttributeType}
|
|
54
|
+
validationRegex={validationRegex}
|
|
55
|
+
/>
|
|
56
|
+
</Form>
|
|
57
|
+
</Formik>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const textbox = screen.getByRole('textbox', { name: /referred by \(optional\)/i });
|
|
61
|
+
expect(textbox).toBeInTheDocument();
|
|
62
|
+
|
|
63
|
+
// Valid input: "ABC"
|
|
64
|
+
await user.type(textbox, 'ABC');
|
|
65
|
+
await user.tab();
|
|
66
|
+
|
|
67
|
+
expect(screen.queryByText(/invalid input/i)).not.toBeInTheDocument();
|
|
68
|
+
await user.clear(textbox);
|
|
69
|
+
|
|
70
|
+
// // Invalid input: "abc" (contains lowercase letters)
|
|
71
|
+
await user.type(textbox, 'abc');
|
|
72
|
+
await user.tab();
|
|
73
|
+
expect(screen.getByText(/invalid input/i)).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('renders the input field as required when required prop is true', () => {
|
|
77
|
+
render(
|
|
78
|
+
<Formik initialValues={{}} onSubmit={() => {}}>
|
|
79
|
+
<Form>
|
|
80
|
+
<TextPersonAttributeField id="attributeId" personAttributeType={mockPersonAttributeType} required />
|
|
81
|
+
</Form>
|
|
82
|
+
</Formik>,
|
|
83
|
+
);
|
|
84
|
+
const textbox = screen.getByRole('textbox', { name: /referred by/i });
|
|
85
|
+
|
|
86
|
+
// Required attribute should be truthy on the input element
|
|
87
|
+
expect(textbox).toBeInTheDocument();
|
|
88
|
+
expect(textbox).toBeRequired();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { FormManager } from './form-manager';
|
|
2
|
+
import { type FormValues } from './patient-registration.types';
|
|
3
|
+
import { generateIdentifier } from './patient-registration.resource';
|
|
4
|
+
|
|
5
|
+
jest.mock('./patient-registration.resource', () => ({
|
|
6
|
+
...jest.requireActual('./patient-registration.resource'),
|
|
7
|
+
generateIdentifier: jest.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
const mockGenerateIdentifier = generateIdentifier as jest.Mock;
|
|
11
|
+
|
|
12
|
+
const formValues: FormValues = {
|
|
13
|
+
patientUuid: '',
|
|
14
|
+
givenName: '',
|
|
15
|
+
middleName: '',
|
|
16
|
+
familyName: '',
|
|
17
|
+
additionalGivenName: '',
|
|
18
|
+
additionalMiddleName: '',
|
|
19
|
+
additionalFamilyName: '',
|
|
20
|
+
addNameInLocalLanguage: false,
|
|
21
|
+
gender: '',
|
|
22
|
+
birthdate: '',
|
|
23
|
+
yearsEstimated: 1000,
|
|
24
|
+
monthsEstimated: 11,
|
|
25
|
+
birthdateEstimated: false,
|
|
26
|
+
telephoneNumber: '',
|
|
27
|
+
isDead: false,
|
|
28
|
+
deathDate: 'string',
|
|
29
|
+
deathTime: '',
|
|
30
|
+
deathTimeFormat: 'AM',
|
|
31
|
+
deathCause: 'string',
|
|
32
|
+
nonCodedCauseOfDeath: '',
|
|
33
|
+
relationships: [],
|
|
34
|
+
address: {
|
|
35
|
+
address1: '',
|
|
36
|
+
address2: '',
|
|
37
|
+
cityVillage: '',
|
|
38
|
+
stateProvince: 'New York',
|
|
39
|
+
country: 'string',
|
|
40
|
+
postalCode: 'string',
|
|
41
|
+
},
|
|
42
|
+
identifiers: {
|
|
43
|
+
foo: {
|
|
44
|
+
identifierUuid: 'aUuid',
|
|
45
|
+
identifierName: 'Foo',
|
|
46
|
+
required: false,
|
|
47
|
+
initialValue: 'foo',
|
|
48
|
+
identifierValue: 'foo',
|
|
49
|
+
identifierTypeUuid: 'identifierType',
|
|
50
|
+
preferred: true,
|
|
51
|
+
autoGeneration: false,
|
|
52
|
+
selectedSource: {
|
|
53
|
+
uuid: 'some-uuid',
|
|
54
|
+
name: 'unique',
|
|
55
|
+
autoGenerationOption: { manualEntryEnabled: true, automaticGenerationEnabled: false },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
describe('FormManager', () => {
|
|
62
|
+
describe('createIdentifiers', () => {
|
|
63
|
+
it('uses the uuid of a field name if it exists', async () => {
|
|
64
|
+
const result = await FormManager.savePatientIdentifiers(true, undefined, formValues.identifiers, {}, 'Nyc');
|
|
65
|
+
expect(result).toEqual([
|
|
66
|
+
{
|
|
67
|
+
uuid: 'aUuid',
|
|
68
|
+
identifier: 'foo',
|
|
69
|
+
identifierType: 'identifierType',
|
|
70
|
+
location: 'Nyc',
|
|
71
|
+
preferred: true,
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should generate identifier if it has autoGeneration and manual entry disabled', async () => {
|
|
77
|
+
formValues.identifiers.foo.autoGeneration = true;
|
|
78
|
+
formValues.identifiers.foo.selectedSource.autoGenerationOption.manualEntryEnabled = false;
|
|
79
|
+
mockGenerateIdentifier.mockResolvedValue({ data: { identifier: '10001V' } });
|
|
80
|
+
await FormManager.savePatientIdentifiers(true, undefined, formValues.identifiers, {}, 'Nyc');
|
|
81
|
+
expect(mockGenerateIdentifier.mock.calls).toHaveLength(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should not generate identifiers if manual entry enabled and identifier value given', async () => {
|
|
85
|
+
formValues.identifiers.foo.autoGeneration = true;
|
|
86
|
+
formValues.identifiers.foo.selectedSource.autoGenerationOption.manualEntryEnabled = true;
|
|
87
|
+
await FormManager.savePatientIdentifiers(true, undefined, formValues.identifiers, {}, 'Nyc');
|
|
88
|
+
expect(mockGenerateIdentifier.mock.calls).toHaveLength(0);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -1,35 +1,39 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type FetchResponse,
|
|
3
|
+
getConfig,
|
|
3
4
|
openmrsFetch,
|
|
4
5
|
queueSynchronizationItem,
|
|
5
|
-
type Session,
|
|
6
6
|
restBaseUrl,
|
|
7
|
-
|
|
7
|
+
type Session,
|
|
8
|
+
type StyleguideConfigObject,
|
|
9
|
+
toOmrsIsoString,
|
|
8
10
|
} from '@openmrs/esm-framework';
|
|
9
11
|
import { patientRegistration } from '../constants';
|
|
10
12
|
import {
|
|
11
|
-
type FormValues,
|
|
12
13
|
type AttributeValue,
|
|
13
|
-
type PatientUuidMapType,
|
|
14
|
-
type Patient,
|
|
15
14
|
type CapturePhotoProps,
|
|
15
|
+
type Encounter,
|
|
16
|
+
type FormValues,
|
|
17
|
+
type Patient,
|
|
16
18
|
type PatientIdentifier,
|
|
17
19
|
type PatientRegistration,
|
|
20
|
+
type PatientUuidMapType,
|
|
18
21
|
type RelationshipValue,
|
|
19
|
-
type Encounter,
|
|
20
22
|
} from './patient-registration.types';
|
|
21
23
|
import {
|
|
22
24
|
addPatientIdentifier,
|
|
23
25
|
deletePatientIdentifier,
|
|
24
26
|
deletePersonName,
|
|
25
27
|
deleteRelationship,
|
|
28
|
+
generateAmrsUniversalIdentifier,
|
|
26
29
|
generateIdentifier,
|
|
30
|
+
getDatetime,
|
|
31
|
+
saveEncounter,
|
|
27
32
|
savePatient,
|
|
28
33
|
savePatientPhoto,
|
|
29
34
|
saveRelationship,
|
|
30
|
-
updateRelationship,
|
|
31
35
|
updatePatientIdentifier,
|
|
32
|
-
|
|
36
|
+
updateRelationship,
|
|
33
37
|
} from './patient-registration.resource';
|
|
34
38
|
import { type RegistrationConfig } from '../config-schema';
|
|
35
39
|
|
|
@@ -61,7 +65,7 @@ export class FormManager {
|
|
|
61
65
|
) => {
|
|
62
66
|
const syncItem: PatientRegistration = {
|
|
63
67
|
fhirPatient: FormManager.mapPatientToFhirPatient(
|
|
64
|
-
FormManager.getPatientToCreate(isNewPatient, values, patientUuidMap, initialAddressFieldValues, []),
|
|
68
|
+
FormManager.getPatientToCreate(isNewPatient, values, patientUuidMap, initialAddressFieldValues, [], config),
|
|
65
69
|
),
|
|
66
70
|
_patientRegistrationData: {
|
|
67
71
|
isNewPatient,
|
|
@@ -114,6 +118,7 @@ export class FormManager {
|
|
|
114
118
|
patientUuidMap,
|
|
115
119
|
initialAddressFieldValues,
|
|
116
120
|
patientIdentifiers,
|
|
121
|
+
config,
|
|
117
122
|
);
|
|
118
123
|
|
|
119
124
|
FormManager.getDeletedNames(values.patientUuid, patientUuidMap).forEach(async (name) => {
|
|
@@ -131,14 +136,15 @@ export class FormManager {
|
|
|
131
136
|
|
|
132
137
|
await this.saveObservations(values.obs, savePatientResponse, currentLocation, currentUser, config);
|
|
133
138
|
|
|
134
|
-
const {
|
|
135
|
-
|
|
139
|
+
const { patientPhotoConceptUuid } = await getConfig<StyleguideConfigObject>('@openmrs/esm-styleguide');
|
|
140
|
+
|
|
141
|
+
if (patientPhotoConceptUuid && capturePhotoProps?.imageData) {
|
|
136
142
|
await savePatientPhoto(
|
|
137
143
|
savePatientResponse.data.uuid,
|
|
138
144
|
capturePhotoProps.imageData,
|
|
139
145
|
`${restBaseUrl}/obs`,
|
|
140
146
|
capturePhotoProps.dateTime || new Date().toISOString(),
|
|
141
|
-
|
|
147
|
+
patientPhotoConceptUuid,
|
|
142
148
|
);
|
|
143
149
|
}
|
|
144
150
|
}
|
|
@@ -189,6 +195,7 @@ export class FormManager {
|
|
|
189
195
|
);
|
|
190
196
|
} else {
|
|
191
197
|
const encounterToSave: Encounter = {
|
|
198
|
+
encounterDatetime: new Date(),
|
|
192
199
|
patient: savePatientResponse.data.uuid,
|
|
193
200
|
encounterType: config.registrationObs.encounterTypeUuid,
|
|
194
201
|
location: currentLocation,
|
|
@@ -234,11 +241,19 @@ export class FormManager {
|
|
|
234
241
|
initialValue,
|
|
235
242
|
} = patientIdentifier;
|
|
236
243
|
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
244
|
+
const autoGenerationManualEntry =
|
|
245
|
+
autoGeneration && selectedSource?.autoGenerationOption?.manualEntryEnabled && !!identifierValue;
|
|
246
|
+
let identifier;
|
|
247
|
+
if (identifierTypeUuid === '58a4732e-1359-11df-a1f1-0026b9348838') {
|
|
248
|
+
identifier = await generateAmrsUniversalIdentifier();
|
|
249
|
+
} else {
|
|
250
|
+
identifier =
|
|
251
|
+
!autoGeneration || autoGenerationManualEntry
|
|
252
|
+
? identifierValue
|
|
253
|
+
: await (
|
|
254
|
+
await generateIdentifier(selectedSource.uuid)
|
|
255
|
+
).data.identifier;
|
|
256
|
+
}
|
|
242
257
|
const identifierToCreate = {
|
|
243
258
|
uuid: identifierUuid,
|
|
244
259
|
identifier,
|
|
@@ -294,6 +309,7 @@ export class FormManager {
|
|
|
294
309
|
patientUuidMap: PatientUuidMapType,
|
|
295
310
|
initialAddressFieldValues: Record<string, any>,
|
|
296
311
|
identifiers: Array<PatientIdentifier>,
|
|
312
|
+
config?: RegistrationConfig,
|
|
297
313
|
): Patient {
|
|
298
314
|
let birthdate;
|
|
299
315
|
if (values.birthdate instanceof Date) {
|
|
@@ -314,7 +330,7 @@ export class FormManager {
|
|
|
314
330
|
birthdateEstimated: values.birthdateEstimated,
|
|
315
331
|
attributes: FormManager.getPatientAttributes(isNewPatient, values, patientUuidMap),
|
|
316
332
|
addresses: [values.address],
|
|
317
|
-
...FormManager.getPatientDeathInfo(values),
|
|
333
|
+
...FormManager.getPatientDeathInfo(values, config),
|
|
318
334
|
},
|
|
319
335
|
identifiers,
|
|
320
336
|
};
|
|
@@ -373,12 +389,22 @@ export class FormManager {
|
|
|
373
389
|
return attributes;
|
|
374
390
|
}
|
|
375
391
|
|
|
376
|
-
static getPatientDeathInfo(values: FormValues) {
|
|
377
|
-
const { isDead, deathDate, deathCause } = values;
|
|
392
|
+
static getPatientDeathInfo(values: FormValues, config?: RegistrationConfig) {
|
|
393
|
+
const { isDead, deathDate, deathTime, deathTimeFormat, deathCause, nonCodedCauseOfDeath } = values;
|
|
394
|
+
|
|
395
|
+
if (!isDead) {
|
|
396
|
+
return {
|
|
397
|
+
dead: false,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
const dateTimeOfDeath = toOmrsIsoString(getDatetime(deathDate, deathTime, deathTimeFormat));
|
|
401
|
+
|
|
378
402
|
return {
|
|
379
|
-
dead:
|
|
380
|
-
deathDate:
|
|
381
|
-
|
|
403
|
+
dead: true,
|
|
404
|
+
deathDate: dateTimeOfDeath,
|
|
405
|
+
...(deathCause === config?.freeTextFieldConceptUuid
|
|
406
|
+
? { causeOfDeathNonCoded: nonCodedCauseOfDeath, causeOfDeath: null }
|
|
407
|
+
: { causeOfDeath: deathCause, causeOfDeathNonCoded: null }),
|
|
382
408
|
};
|
|
383
409
|
}
|
|
384
410
|
|
|
@@ -66,7 +66,7 @@ export interface TextInputProps
|
|
|
66
66
|
* `true` to use the light version. For use on $ui-01 backgrounds only.
|
|
67
67
|
* Don't use this to make tile background color same as container background color.
|
|
68
68
|
* 'The `light` prop for `TextInput` has ' +
|
|
69
|
-
|
|
69
|
+
'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.'
|
|
70
70
|
*/
|
|
71
71
|
light?: boolean;
|
|
72
72
|
|
|
@@ -145,10 +145,14 @@ export const Input: React.FC<InputProps> = ({ checkWarning, ...props }) => {
|
|
|
145
145
|
t('invalidEmail')
|
|
146
146
|
t('numberInNameDubious')
|
|
147
147
|
t('yearsEstimateRequired')
|
|
148
|
+
t('deathdayIsRequired', 'Death date is required when the patient is marked as deceased.')
|
|
149
|
+
t('deathdayInvalidDate', 'Date of death is invalid')
|
|
150
|
+
t('deathCauseRequired', 'Cause of death is required')
|
|
151
|
+
t('nonCodedCauseOfDeathRequired', 'Non-coded cause of death is required')
|
|
148
152
|
*/
|
|
149
153
|
|
|
150
154
|
const value = field.value || '';
|
|
151
|
-
const invalidText = meta.error
|
|
155
|
+
const invalidText = meta.error || '';
|
|
152
156
|
const warnText = useMemo(() => {
|
|
153
157
|
if (!invalidText && typeof checkWarning === 'function') {
|
|
154
158
|
const warning = checkWarning(value);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { Formik, Form } from 'formik';
|
|
5
|
+
import { SelectInput } from './select-input.component';
|
|
6
|
+
|
|
7
|
+
describe('the select input', () => {
|
|
8
|
+
const setupSelect = async () => {
|
|
9
|
+
render(
|
|
10
|
+
<Formik initialValues={{ select: '' }} onSubmit={null}>
|
|
11
|
+
<Form>
|
|
12
|
+
<SelectInput label="Select" name="select" options={['A Option', 'B Option']} required />
|
|
13
|
+
</Form>
|
|
14
|
+
</Formik>,
|
|
15
|
+
);
|
|
16
|
+
return screen.getByLabelText('Select') as HTMLInputElement;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
it('exists', async () => {
|
|
20
|
+
const input = await setupSelect();
|
|
21
|
+
expect(input.type).toEqual('select-one');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('can input data', async () => {
|
|
25
|
+
const user = userEvent.setup();
|
|
26
|
+
const input = await setupSelect();
|
|
27
|
+
const expected = 'A Option';
|
|
28
|
+
|
|
29
|
+
await user.selectOptions(input, expected);
|
|
30
|
+
|
|
31
|
+
await expect(input.value).toEqual(expected);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should show optional label if the input is not required', async () => {
|
|
35
|
+
render(
|
|
36
|
+
<Formik initialValues={{ select: '' }} onSubmit={null}>
|
|
37
|
+
<Form>
|
|
38
|
+
<SelectInput label="Select" name="select" options={['A Option', 'B Option']} />
|
|
39
|
+
</Form>
|
|
40
|
+
</Formik>,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
await screen.findByRole('combobox');
|
|
44
|
+
|
|
45
|
+
const selectInput = screen.getByRole('combobox', { name: 'Select (optional)' }) as HTMLSelectElement;
|
|
46
|
+
expect(selectInput.labels).toHaveLength(1);
|
|
47
|
+
expect(selectInput.labels[0]).toHaveTextContent('Select (optional)');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
@use '@carbon/
|
|
2
|
-
@use '@carbon/
|
|
3
|
-
@
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
4
4
|
|
|
5
5
|
.label01 {
|
|
6
6
|
@include type.type-style('label-01');
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
position: absolute;
|
|
19
19
|
left: 0;
|
|
20
20
|
background-color: #fff;
|
|
21
|
-
margin-bottom:
|
|
21
|
+
margin-bottom: 1.25rem;
|
|
22
22
|
z-index: 99;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
.suggestions li {
|
|
26
|
-
padding:
|
|
26
|
+
padding: layout.$spacing-05;
|
|
27
27
|
line-height: 1.29;
|
|
28
28
|
color: #525252;
|
|
29
29
|
border-bottom: 1px solid #8d8d8d;
|