@openmrs/esm-patient-programs-app 3.2.1-pre.97 → 4.0.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/.turbo/turbo-build.log +33 -35
- package/dist/238.js +2 -0
- package/dist/{707.js.LICENSE.txt → 238.js.LICENSE.txt} +0 -0
- package/dist/402.js +1 -1
- package/dist/435.js +1 -0
- package/dist/461.js +1 -0
- package/dist/574.js +1 -1
- package/dist/641.js +1 -0
- package/dist/709.js +1 -0
- package/dist/733.js +1 -0
- package/dist/734.js +2 -0
- package/dist/734.js.LICENSE.txt +53 -0
- package/dist/784.js +1 -1
- package/dist/784.js.LICENSE.txt +2 -7
- package/dist/821.js +1 -0
- package/dist/858.js +1 -1
- package/dist/main.js +1 -0
- package/dist/openmrs-esm-patient-programs-app.js +1 -1
- package/dist/openmrs-esm-patient-programs-app.js.buildmanifest.json +134 -205
- package/dist/openmrs-esm-patient-programs-app.old +1 -1
- package/package.json +12 -14
- package/src/config-schema.ts +12 -0
- package/src/dashboard.meta.tsx +1 -1
- package/src/index.ts +8 -3
- package/src/programs/program-action-button/program-action-button.component.tsx +50 -0
- package/src/programs/programs-detailed-summary.component.tsx +68 -37
- package/src/programs/programs-detailed-summary.scss +1 -4
- package/src/programs/programs-detailed-summary.test.tsx +26 -7
- package/src/programs/programs-form.component.tsx +246 -109
- package/src/programs/programs-form.scss +1 -9
- package/src/programs/programs-form.test.tsx +113 -110
- package/src/programs/programs-overview.component.tsx +60 -46
- package/src/programs/programs-overview.scss +1 -3
- package/src/programs/programs-overview.test.tsx +6 -7
- package/src/programs/programs.resource.tsx +63 -8
- package/src/types/index.ts +16 -2
- package/translations/en.json +10 -2
- package/dist/13.js +0 -1
- package/dist/142.js +0 -1
- package/dist/145.js +0 -1
- package/dist/186.js +0 -2
- package/dist/186.js.LICENSE.txt +0 -23
- package/dist/252.js +0 -2
- package/dist/252.js.LICENSE.txt +0 -23
- package/dist/336.js +0 -2
- package/dist/336.js.LICENSE.txt +0 -20
- package/dist/373.js +0 -1
- package/dist/635.js +0 -2
- package/dist/635.js.LICENSE.txt +0 -16
- package/dist/692.js +0 -1
- package/dist/701.js +0 -1
- package/dist/707.js +0 -2
- package/dist/76.js +0 -1
- package/dist/932.js +0 -1
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { throwError } from 'rxjs';
|
|
3
3
|
import { of } from 'rxjs/internal/observable/of';
|
|
4
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import { render, screen, waitFor, within } from '@testing-library/react';
|
|
5
5
|
import userEvent from '@testing-library/user-event';
|
|
6
|
-
import { createErrorHandler, openmrsFetch, showNotification, showToast
|
|
6
|
+
import { createErrorHandler, openmrsFetch, showNotification, showToast } from '@openmrs/esm-framework';
|
|
7
7
|
import { mockPatient } from '../../../../__mocks__/patient.mock';
|
|
8
8
|
import {
|
|
9
9
|
mockCareProgramsResponse,
|
|
10
10
|
mockEnrolledProgramsResponse,
|
|
11
11
|
mockLocationsResponse,
|
|
12
12
|
} from '../../../../__mocks__/programs.mock';
|
|
13
|
-
import { createProgramEnrollment } from './programs.resource';
|
|
13
|
+
import { createProgramEnrollment, updateProgramEnrollment } from './programs.resource';
|
|
14
14
|
import ProgramsForm from './programs-form.component';
|
|
15
15
|
|
|
16
|
+
jest.setTimeout(20000);
|
|
17
|
+
|
|
16
18
|
const testProps = {
|
|
17
19
|
closeWorkspace: jest.fn(),
|
|
18
20
|
patientUuid: mockPatient.id,
|
|
@@ -21,10 +23,10 @@ const testProps = {
|
|
|
21
23
|
|
|
22
24
|
const mockCreateErrorHandler = createErrorHandler as jest.Mock;
|
|
23
25
|
const mockCreateProgramEnrollment = createProgramEnrollment as jest.Mock;
|
|
26
|
+
const mockUpdateProgramEnrollment = updateProgramEnrollment as jest.Mock;
|
|
24
27
|
const mockOpenmrsFetch = openmrsFetch as jest.Mock;
|
|
25
28
|
const mockShowNotification = showNotification as jest.Mock;
|
|
26
29
|
const mockShowToast = showToast as jest.Mock;
|
|
27
|
-
const mockUseLayoutType = useLayoutType as jest.Mock;
|
|
28
30
|
|
|
29
31
|
jest.mock('@openmrs/esm-framework', () => {
|
|
30
32
|
const originalModule = jest.requireActual('@openmrs/esm-framework');
|
|
@@ -44,142 +46,143 @@ jest.mock('./programs.resource', () => {
|
|
|
44
46
|
return {
|
|
45
47
|
...originalModule,
|
|
46
48
|
createProgramEnrollment: jest.fn(),
|
|
49
|
+
updateProgramEnrollment: jest.fn(),
|
|
47
50
|
};
|
|
48
51
|
});
|
|
49
52
|
|
|
50
|
-
describe('ProgramsForm
|
|
51
|
-
|
|
53
|
+
describe('ProgramsForm', () => {
|
|
54
|
+
it('renders a success toast notification upon successfully recording a program enrollment', async () => {
|
|
55
|
+
const user = userEvent.setup();
|
|
56
|
+
|
|
57
|
+
const inpatientWardUuid = 'b1a8b05e-3542-4037-bbd3-998ee9c40574';
|
|
58
|
+
const oncologyScreeningProgramUuid = '11b129ca-a5e7-4025-84bf-b92a173e20de';
|
|
59
|
+
|
|
52
60
|
mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockCareProgramsResponse } });
|
|
53
61
|
mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockEnrolledProgramsResponse } });
|
|
54
|
-
|
|
62
|
+
mockCreateProgramEnrollment.mockReturnValueOnce(of({ status: 201, statusText: 'Created' }));
|
|
55
63
|
|
|
56
|
-
it('renders the programs form with all the relevant fields and values', async () => {
|
|
57
64
|
renderProgramsForm();
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
const enrollButton = screen.getByRole('button', { name: /save and close/i });
|
|
67
|
+
const enrollmentDateInput = screen.getAllByRole('textbox', { name: '' })[0];
|
|
68
|
+
const selectLocationInput = screen.getAllByRole('combobox', { name: '' })[1];
|
|
69
|
+
const selectProgramInput = screen.getAllByRole('combobox', { name: '' })[0];
|
|
70
|
+
|
|
71
|
+
await waitFor(() => user.type(enrollmentDateInput, '2020-05-05'));
|
|
72
|
+
await waitFor(() => user.selectOptions(selectProgramInput, [oncologyScreeningProgramUuid]));
|
|
73
|
+
await waitFor(() => user.selectOptions(selectLocationInput, [inpatientWardUuid]));
|
|
74
|
+
|
|
67
75
|
expect(screen.getByRole('option', { name: /Inpatient Ward/i })).toBeInTheDocument();
|
|
68
|
-
expect(screen.getByRole('option', { name: /Isolation Ward/i })).toBeInTheDocument();
|
|
69
|
-
expect(screen.getByRole('option', { name: /Laboratory/i })).toBeInTheDocument();
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
expect(
|
|
74
|
-
expect(
|
|
75
|
-
|
|
77
|
+
await waitFor(() => user.click(enrollButton));
|
|
78
|
+
|
|
79
|
+
expect(mockCreateProgramEnrollment).toHaveBeenCalledTimes(1);
|
|
80
|
+
expect(mockCreateProgramEnrollment).toHaveBeenCalledWith(
|
|
81
|
+
expect.objectContaining({
|
|
82
|
+
dateCompleted: null,
|
|
83
|
+
location: inpatientWardUuid,
|
|
84
|
+
patient: mockPatient.id,
|
|
85
|
+
program: oncologyScreeningProgramUuid,
|
|
86
|
+
}),
|
|
87
|
+
new AbortController(),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(mockShowToast).toHaveBeenCalledTimes(1);
|
|
91
|
+
expect(mockShowToast).toHaveBeenCalledWith({
|
|
92
|
+
critical: true,
|
|
93
|
+
description: 'It is now visible in the Programs table',
|
|
94
|
+
kind: 'success',
|
|
95
|
+
title: 'Program enrollment saved',
|
|
96
|
+
});
|
|
76
97
|
});
|
|
77
98
|
|
|
78
|
-
it('
|
|
79
|
-
|
|
99
|
+
it('updates a program enrollment', async () => {
|
|
100
|
+
const user = userEvent.setup();
|
|
80
101
|
|
|
81
|
-
|
|
82
|
-
userEvent.click(cancelButton);
|
|
102
|
+
renderProgramsForm(mockEnrolledProgramsResponse[0].uuid);
|
|
83
103
|
|
|
84
|
-
|
|
85
|
-
|
|
104
|
+
const enrollButton = screen.getByRole('button', { name: /save and close/i });
|
|
105
|
+
const dateCompletedGroup = screen.getByRole('group', { name: /Date completed/i });
|
|
106
|
+
const dateCompletedInput = within(dateCompletedGroup).getByRole('textbox');
|
|
86
107
|
|
|
87
|
-
|
|
88
|
-
mockUseLayoutType.mockReturnValueOnce('tablet').mockReturnValueOnce('tablet');
|
|
108
|
+
mockUpdateProgramEnrollment.mockReturnValueOnce(of({ status: 200, statusText: 'OK' }));
|
|
89
109
|
|
|
90
|
-
|
|
110
|
+
await waitFor(() => user.type(dateCompletedInput, '05/05/2020'));
|
|
91
111
|
|
|
92
|
-
expect(
|
|
93
|
-
|
|
112
|
+
expect(dateCompletedInput).toHaveValue('05/05/2020');
|
|
113
|
+
|
|
114
|
+
await waitFor(() => user.tab());
|
|
115
|
+
|
|
116
|
+
expect(enrollButton).not.toBeDisabled();
|
|
117
|
+
|
|
118
|
+
await waitFor(() => user.click(enrollButton));
|
|
119
|
+
|
|
120
|
+
expect(mockUpdateProgramEnrollment).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(mockUpdateProgramEnrollment).toHaveBeenCalledWith(
|
|
122
|
+
mockEnrolledProgramsResponse[0].uuid,
|
|
123
|
+
expect.objectContaining({
|
|
124
|
+
dateEnrolled: '2020-01-16T00:00:00+00:00',
|
|
125
|
+
dateCompleted: '2020-05-05T00:00:00+00:00',
|
|
126
|
+
location: mockEnrolledProgramsResponse[0].location.uuid,
|
|
127
|
+
patient: mockPatient.id,
|
|
128
|
+
program: mockEnrolledProgramsResponse[0].program.uuid,
|
|
129
|
+
}),
|
|
130
|
+
new AbortController(),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(mockShowToast).toHaveBeenCalledWith(
|
|
134
|
+
expect.objectContaining({
|
|
135
|
+
critical: true,
|
|
136
|
+
description: 'Changes to the program are now visible in the Programs table',
|
|
137
|
+
kind: 'success',
|
|
138
|
+
title: 'Program enrollment updated',
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
94
141
|
});
|
|
95
142
|
|
|
96
|
-
|
|
143
|
+
it('renders an error notification if there was a problem recording a program enrollment', async () => {
|
|
144
|
+
const user = userEvent.setup();
|
|
145
|
+
|
|
97
146
|
const inpatientWardUuid = 'b1a8b05e-3542-4037-bbd3-998ee9c40574';
|
|
98
147
|
const oncologyScreeningProgramUuid = '11b129ca-a5e7-4025-84bf-b92a173e20de';
|
|
99
|
-
let cancelButton: HTMLElement;
|
|
100
|
-
let enrollButton: HTMLElement;
|
|
101
|
-
let enrollmentDateInput: HTMLElement;
|
|
102
|
-
let selectLocationInput: HTMLElement;
|
|
103
|
-
let selectProgramInput: HTMLElement;
|
|
104
|
-
|
|
105
|
-
beforeEach(() => {
|
|
106
|
-
renderProgramsForm();
|
|
107
|
-
|
|
108
|
-
cancelButton = screen.getByRole('button', { name: /cancel/i });
|
|
109
|
-
enrollButton = screen.getByRole('button', { name: /save and close/i });
|
|
110
|
-
enrollmentDateInput = screen.getAllByRole('textbox', { name: '' })[0];
|
|
111
|
-
selectLocationInput = screen.getAllByRole('combobox', { name: '' })[1];
|
|
112
|
-
selectProgramInput = screen.getAllByRole('combobox', { name: '' })[0];
|
|
113
|
-
});
|
|
114
148
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
expect(screen.getByDisplayValue('Oncology Screening and Diagnosis')).toBeInTheDocument();
|
|
123
|
-
expect(screen.getByDisplayValue('Inpatient Ward')).toBeInTheDocument();
|
|
124
|
-
expect(enrollButton).not.toBeDisabled();
|
|
125
|
-
|
|
126
|
-
await waitFor(() => userEvent.click(enrollButton));
|
|
127
|
-
|
|
128
|
-
expect(mockCreateProgramEnrollment).toHaveBeenCalledTimes(1);
|
|
129
|
-
expect(mockCreateProgramEnrollment).toHaveBeenCalledWith(
|
|
130
|
-
expect.objectContaining({
|
|
131
|
-
dateCompleted: null,
|
|
132
|
-
location: inpatientWardUuid,
|
|
133
|
-
patient: mockPatient.id,
|
|
134
|
-
program: oncologyScreeningProgramUuid,
|
|
135
|
-
}),
|
|
136
|
-
new AbortController(),
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
expect(mockShowToast).toHaveBeenCalledTimes(1);
|
|
140
|
-
expect(mockShowToast).toHaveBeenCalledWith(
|
|
141
|
-
expect.objectContaining({
|
|
142
|
-
critical: true,
|
|
143
|
-
description: 'It is now visible on the Programs page',
|
|
144
|
-
kind: 'success',
|
|
145
|
-
title: 'Program enrollment saved',
|
|
146
|
-
}),
|
|
147
|
-
);
|
|
148
|
-
});
|
|
149
|
+
const error = {
|
|
150
|
+
message: 'Internal Server Error',
|
|
151
|
+
response: {
|
|
152
|
+
status: 500,
|
|
153
|
+
statusText: 'Internal Server Error',
|
|
154
|
+
},
|
|
155
|
+
};
|
|
149
156
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
response: {
|
|
154
|
-
status: 500,
|
|
155
|
-
statusText: 'Internal Server Error',
|
|
156
|
-
},
|
|
157
|
-
};
|
|
157
|
+
mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockCareProgramsResponse } });
|
|
158
|
+
mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockEnrolledProgramsResponse } });
|
|
159
|
+
mockCreateProgramEnrollment.mockReturnValueOnce(throwError(error));
|
|
158
160
|
|
|
159
|
-
|
|
161
|
+
renderProgramsForm();
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
const enrollButton = screen.getByRole('button', { name: /save and close/i });
|
|
164
|
+
const enrollmentDateInput = screen.getAllByRole('textbox', { name: '' })[0];
|
|
165
|
+
const selectLocationInput = screen.getAllByRole('combobox', { name: '' })[1];
|
|
166
|
+
const selectProgramInput = screen.getAllByRole('combobox', { name: '' })[0];
|
|
164
167
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
expect(enrollButton).not.toBeDisabled();
|
|
168
|
+
await waitFor(() => user.type(enrollmentDateInput, '2020-05-05'));
|
|
169
|
+
await waitFor(() => user.selectOptions(selectProgramInput, [oncologyScreeningProgramUuid]));
|
|
170
|
+
await waitFor(() => user.selectOptions(selectLocationInput, [inpatientWardUuid]));
|
|
169
171
|
|
|
170
|
-
|
|
172
|
+
expect(enrollButton).not.toBeDisabled();
|
|
171
173
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
await waitFor(() => user.click(enrollButton));
|
|
175
|
+
|
|
176
|
+
expect(mockCreateErrorHandler).toHaveBeenCalledTimes(1);
|
|
177
|
+
expect(mockShowNotification).toHaveBeenCalledWith({
|
|
178
|
+
critical: true,
|
|
179
|
+
description: 'Internal Server Error',
|
|
180
|
+
kind: 'error',
|
|
181
|
+
title: 'Error saving program enrollment',
|
|
179
182
|
});
|
|
180
183
|
});
|
|
181
184
|
});
|
|
182
185
|
|
|
183
|
-
function renderProgramsForm() {
|
|
184
|
-
render(<ProgramsForm {...testProps} />);
|
|
186
|
+
function renderProgramsForm(programEnrollmentUuidToEdit?: string) {
|
|
187
|
+
render(<ProgramsForm {...testProps} programEnrollmentId={programEnrollmentUuidToEdit} />);
|
|
185
188
|
}
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import { formatDate, formatDatetime, usePagination } from '@openmrs/esm-framework';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import capitalize from 'lodash-es/capitalize';
|
|
5
4
|
import {
|
|
5
|
+
Button,
|
|
6
6
|
DataTable,
|
|
7
7
|
DataTableSkeleton,
|
|
8
|
-
Button,
|
|
9
8
|
InlineLoading,
|
|
9
|
+
InlineNotification,
|
|
10
10
|
Table,
|
|
11
|
+
TableBody,
|
|
11
12
|
TableCell,
|
|
12
13
|
TableContainer,
|
|
13
|
-
TableBody,
|
|
14
14
|
TableHead,
|
|
15
15
|
TableHeader,
|
|
16
16
|
TableRow,
|
|
17
|
-
|
|
18
|
-
} from 'carbon
|
|
19
|
-
import filter from 'lodash-es/filter';
|
|
20
|
-
import includes from 'lodash-es/includes';
|
|
21
|
-
import map from 'lodash-es/map';
|
|
17
|
+
} from '@carbon/react';
|
|
18
|
+
import { Add } from '@carbon/react/icons';
|
|
22
19
|
import {
|
|
20
|
+
launchPatientWorkspace,
|
|
23
21
|
CardHeader,
|
|
24
22
|
EmptyState,
|
|
25
23
|
ErrorState,
|
|
26
24
|
PatientChartPagination,
|
|
27
|
-
launchPatientWorkspace,
|
|
28
25
|
} from '@openmrs/esm-patient-common-lib';
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
26
|
+
import { ConfigObject, formatDate, formatDatetime, useConfig, usePagination } from '@openmrs/esm-framework';
|
|
27
|
+
import { usePrograms } from './programs.resource';
|
|
28
|
+
import ProgramActionButton from './program-action-button/program-action-button.component';
|
|
29
|
+
import { ConfigurableProgram } from '../types';
|
|
30
|
+
import styles from './programs-overview.scss';
|
|
31
31
|
|
|
32
32
|
interface ProgramsOverviewProps {
|
|
33
33
|
basePath: string;
|
|
@@ -37,22 +37,29 @@ interface ProgramsOverviewProps {
|
|
|
37
37
|
const ProgramsOverview: React.FC<ProgramsOverviewProps> = ({ basePath, patientUuid }) => {
|
|
38
38
|
const programsCount = 5;
|
|
39
39
|
const { t } = useTranslation();
|
|
40
|
+
const config = useConfig() as ConfigObject;
|
|
40
41
|
const displayText = t('programs', 'Program enrollments');
|
|
41
42
|
const headerTitle = t('carePrograms', 'Care Programs');
|
|
42
43
|
const urlLabel = t('seeAll', 'See all');
|
|
43
|
-
const pageUrl =
|
|
44
|
-
|
|
45
|
-
const { data: enrollments, isError, isLoading, isValidating } = useEnrollments(patientUuid);
|
|
46
|
-
const activeEnrollments = enrollments?.filter((enrollment) => !enrollment.dateCompleted);
|
|
47
|
-
|
|
48
|
-
const { data: availablePrograms } = useAvailablePrograms();
|
|
44
|
+
const pageUrl = `\${openmrsSpaBase}/patient/${patientUuid}/chart/Programs`;
|
|
45
|
+
const isConfigurable = config.customUrl ? true : false;
|
|
49
46
|
|
|
50
|
-
const
|
|
47
|
+
const {
|
|
48
|
+
activeEnrollments,
|
|
51
49
|
availablePrograms,
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
configurablePrograms,
|
|
51
|
+
eligiblePrograms,
|
|
52
|
+
enrollments,
|
|
53
|
+
isError,
|
|
54
|
+
isLoading,
|
|
55
|
+
isValidating,
|
|
56
|
+
} = usePrograms(patientUuid);
|
|
54
57
|
|
|
55
|
-
const {
|
|
58
|
+
const {
|
|
59
|
+
results: paginatedEnrollments,
|
|
60
|
+
goTo,
|
|
61
|
+
currentPage,
|
|
62
|
+
} = usePagination(isConfigurable ? configurablePrograms : enrollments ?? [], programsCount);
|
|
56
63
|
|
|
57
64
|
const launchProgramsForm = React.useCallback(() => launchPatientWorkspace('programs-form-workspace'), []);
|
|
58
65
|
|
|
@@ -73,30 +80,37 @@ const ProgramsOverview: React.FC<ProgramsOverviewProps> = ({ basePath, patientUu
|
|
|
73
80
|
key: 'status',
|
|
74
81
|
header: t('status', 'Status'),
|
|
75
82
|
},
|
|
83
|
+
{
|
|
84
|
+
key: 'actions',
|
|
85
|
+
header: t('actions', 'Actions'),
|
|
86
|
+
},
|
|
76
87
|
];
|
|
77
88
|
|
|
78
89
|
const tableRows = React.useMemo(() => {
|
|
79
|
-
return paginatedEnrollments?.map((enrollment) => ({
|
|
90
|
+
return paginatedEnrollments?.map((enrollment: ConfigurableProgram) => ({
|
|
80
91
|
id: enrollment.uuid,
|
|
81
92
|
display: enrollment.display,
|
|
82
93
|
location: enrollment.location?.display,
|
|
83
|
-
dateEnrolled: formatDatetime(new Date(enrollment.dateEnrolled)),
|
|
84
|
-
status:
|
|
94
|
+
dateEnrolled: enrollment.dateEnrolled ? formatDatetime(new Date(enrollment.dateEnrolled)) : '--',
|
|
95
|
+
status: isConfigurable
|
|
96
|
+
? capitalize(enrollment.enrollmentStatus)
|
|
97
|
+
: enrollment.dateCompleted
|
|
85
98
|
? `${t('completedOn', 'Completed On')} ${formatDate(new Date(enrollment.dateCompleted))}`
|
|
86
99
|
: t('active', 'Active'),
|
|
100
|
+
actions: <ProgramActionButton enrollment={enrollment} />,
|
|
87
101
|
}));
|
|
88
|
-
}, [paginatedEnrollments, t]);
|
|
102
|
+
}, [isConfigurable, paginatedEnrollments, t]);
|
|
89
103
|
|
|
90
104
|
if (isLoading) return <DataTableSkeleton role="progressbar" />;
|
|
91
105
|
if (isError) return <ErrorState error={isError} headerTitle={headerTitle} />;
|
|
92
|
-
if (activeEnrollments?.length) {
|
|
106
|
+
if (isConfigurable ? configurablePrograms.length : activeEnrollments?.length) {
|
|
93
107
|
return (
|
|
94
108
|
<div className={styles.widgetCard}>
|
|
95
109
|
<CardHeader title={headerTitle}>
|
|
96
110
|
<span>{isValidating ? <InlineLoading /> : null}</span>
|
|
97
111
|
<Button
|
|
98
112
|
kind="ghost"
|
|
99
|
-
renderIcon={
|
|
113
|
+
renderIcon={(props) => <Add size={16} {...props} />}
|
|
100
114
|
iconDescription="Add programs"
|
|
101
115
|
onClick={launchProgramsForm}
|
|
102
116
|
disabled={availablePrograms?.length && eligiblePrograms?.length === 0}
|
|
@@ -104,19 +118,19 @@ const ProgramsOverview: React.FC<ProgramsOverviewProps> = ({ basePath, patientUu
|
|
|
104
118
|
{t('add', 'Add')}
|
|
105
119
|
</Button>
|
|
106
120
|
</CardHeader>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<Table {...getTableProps()}
|
|
121
|
+
{availablePrograms?.length && eligiblePrograms?.length === 0 && (
|
|
122
|
+
<InlineNotification
|
|
123
|
+
style={{ minWidth: '100%', margin: '0rem', padding: '0rem' }}
|
|
124
|
+
kind={'info'}
|
|
125
|
+
lowContrast
|
|
126
|
+
subtitle={t('noEligibleEnrollments', 'There are no more programs left to enroll this patient in')}
|
|
127
|
+
title={t('fullyEnrolled', 'Enrolled in all programs')}
|
|
128
|
+
/>
|
|
129
|
+
)}
|
|
130
|
+
<DataTable rows={tableRows} headers={tableHeaders} isSortable size="sm" useZebraStyles>
|
|
131
|
+
{({ rows, headers, getHeaderProps, getTableProps }) => (
|
|
132
|
+
<TableContainer>
|
|
133
|
+
<Table {...getTableProps()}>
|
|
120
134
|
<TableHead>
|
|
121
135
|
<TableRow>
|
|
122
136
|
{headers.map((header) => (
|
|
@@ -142,15 +156,15 @@ const ProgramsOverview: React.FC<ProgramsOverviewProps> = ({ basePath, patientUu
|
|
|
142
156
|
))}
|
|
143
157
|
</TableBody>
|
|
144
158
|
</Table>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
</
|
|
159
|
+
</TableContainer>
|
|
160
|
+
)}
|
|
161
|
+
</DataTable>
|
|
148
162
|
<PatientChartPagination
|
|
149
163
|
currentItems={paginatedEnrollments.length}
|
|
150
164
|
onPageNumberChange={({ page }) => goTo(page)}
|
|
151
165
|
pageNumber={currentPage}
|
|
152
166
|
pageSize={programsCount}
|
|
153
|
-
totalItems={
|
|
167
|
+
totalItems={isConfigurable ? configurablePrograms.length : enrollments?.length}
|
|
154
168
|
dashboardLinkUrl={pageUrl}
|
|
155
169
|
dashboardLinkLabel={urlLabel}
|
|
156
170
|
/>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { screen } from '@testing-library/react';
|
|
2
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
4
|
import { openmrsFetch, usePagination } from '@openmrs/esm-framework';
|
|
5
5
|
import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';
|
|
@@ -67,14 +67,12 @@ describe('ProgramsOverview', () => {
|
|
|
67
67
|
await waitForLoadingToFinish();
|
|
68
68
|
|
|
69
69
|
expect(screen.getByText(/Care Programs/i)).toBeInTheDocument();
|
|
70
|
-
expect(
|
|
71
|
-
screen.getByText(
|
|
72
|
-
/Sorry, there was a problem displaying this information. You can try to reload this page, or contact the site administrator and quote the error code above./,
|
|
73
|
-
),
|
|
74
|
-
).toBeInTheDocument();
|
|
70
|
+
expect(screen.getByText(/Sorry, there was a problem displaying this information./)).toBeInTheDocument();
|
|
75
71
|
});
|
|
76
72
|
|
|
77
73
|
it("renders a tabular overview of the patient's active program enrollments when available", async () => {
|
|
74
|
+
const user = userEvent.setup();
|
|
75
|
+
|
|
78
76
|
mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockEnrolledProgramsResponse } });
|
|
79
77
|
mockUsePagination.mockImplementation(() => ({
|
|
80
78
|
currentPage: 1,
|
|
@@ -103,7 +101,8 @@ describe('ProgramsOverview', () => {
|
|
|
103
101
|
expect(screen.getByRole('row', { name: /HIV Care and Treatment/i })).toBeInTheDocument();
|
|
104
102
|
|
|
105
103
|
// Clicking "Add" launches the programs form in a workspace
|
|
106
|
-
|
|
104
|
+
await waitFor(() => user.click(addButton));
|
|
105
|
+
|
|
107
106
|
expect(launchPatientWorkspace).toHaveBeenCalledWith('programs-form-workspace');
|
|
108
107
|
});
|
|
109
108
|
});
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import useSWR from 'swr';
|
|
2
|
-
import { map } from 'rxjs/operators';
|
|
3
|
-
import { openmrsFetch, openmrsObservableFetch } from '@openmrs/esm-framework';
|
|
4
|
-
import { PatientProgram, Program, ProgramsFetchResponse } from '../types';
|
|
2
|
+
import { map as rxjsMap } from 'rxjs/operators';
|
|
3
|
+
import { openmrsFetch, openmrsObservableFetch, useConfig } from '@openmrs/esm-framework';
|
|
4
|
+
import { ConfigurableProgram, PatientProgram, Program, ProgramsFetchResponse } from '../types';
|
|
5
5
|
import uniqBy from 'lodash-es/uniqBy';
|
|
6
|
+
import filter from 'lodash-es/filter';
|
|
7
|
+
import includes from 'lodash-es/includes';
|
|
8
|
+
import map from 'lodash-es/map';
|
|
9
|
+
import { ConfigObject } from '../config-schema';
|
|
6
10
|
|
|
7
11
|
export const customRepresentation = `custom:(uuid,display,program,dateEnrolled,dateCompleted,location:(uuid,display))`;
|
|
8
12
|
|
|
@@ -17,30 +21,41 @@ export function useEnrollments(patientUuid: string) {
|
|
|
17
21
|
? data?.data.results.sort((a, b) => (b.dateEnrolled > a.dateEnrolled ? 1 : -1))
|
|
18
22
|
: null;
|
|
19
23
|
|
|
24
|
+
const activeEnrollments = formattedEnrollments?.filter((enrollment) => !enrollment.dateCompleted);
|
|
25
|
+
|
|
20
26
|
return {
|
|
21
27
|
data: data ? uniqBy(formattedEnrollments, (program) => program?.program?.uuid) : null,
|
|
22
28
|
isError: error,
|
|
23
29
|
isLoading: !data && !error,
|
|
24
30
|
isValidating,
|
|
31
|
+
activeEnrollments,
|
|
25
32
|
};
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
export function useAvailablePrograms() {
|
|
35
|
+
export function useAvailablePrograms(enrollments?: Array<PatientProgram>) {
|
|
29
36
|
const { data, error } = useSWR<{ data: { results: Array<Program> } }, Error>(
|
|
30
37
|
`/ws/rest/v1/program?v=custom:(uuid,display,allWorkflows,concept:(uuid,display))`,
|
|
31
38
|
openmrsFetch,
|
|
32
39
|
);
|
|
33
40
|
|
|
41
|
+
const availablePrograms = data?.data?.results ?? null;
|
|
42
|
+
|
|
43
|
+
const eligiblePrograms = filter(
|
|
44
|
+
availablePrograms,
|
|
45
|
+
(program) => !includes(map(enrollments, 'program.uuid'), program.uuid),
|
|
46
|
+
);
|
|
47
|
+
|
|
34
48
|
return {
|
|
35
|
-
data:
|
|
49
|
+
data: availablePrograms,
|
|
36
50
|
isError: error,
|
|
37
51
|
isLoading: !data && !error,
|
|
52
|
+
eligiblePrograms,
|
|
38
53
|
};
|
|
39
54
|
}
|
|
40
55
|
|
|
41
56
|
export function getPatientProgramByUuid(programUuid: string) {
|
|
42
57
|
return openmrsObservableFetch<PatientProgram>(`/ws/rest/v1/programenrollment/${programUuid}`).pipe(
|
|
43
|
-
|
|
58
|
+
rxjsMap(({ data }) => data),
|
|
44
59
|
);
|
|
45
60
|
}
|
|
46
61
|
|
|
@@ -59,12 +74,12 @@ export function createProgramEnrollment(payload, abortController) {
|
|
|
59
74
|
});
|
|
60
75
|
}
|
|
61
76
|
|
|
62
|
-
export function updateProgramEnrollment(payload, abortController) {
|
|
77
|
+
export function updateProgramEnrollment(programEnrollmentUuid: string, payload, abortController) {
|
|
63
78
|
if (!payload && !payload.program) {
|
|
64
79
|
return null;
|
|
65
80
|
}
|
|
66
81
|
const { program, dateEnrolled, dateCompleted, location } = payload;
|
|
67
|
-
return openmrsObservableFetch(`/ws/rest/v1/programenrollment/${
|
|
82
|
+
return openmrsObservableFetch(`/ws/rest/v1/programenrollment/${programEnrollmentUuid}`, {
|
|
68
83
|
method: 'POST',
|
|
69
84
|
headers: {
|
|
70
85
|
'Content-Type': 'application/json',
|
|
@@ -73,3 +88,43 @@ export function updateProgramEnrollment(payload, abortController) {
|
|
|
73
88
|
signal: abortController.signal,
|
|
74
89
|
});
|
|
75
90
|
}
|
|
91
|
+
|
|
92
|
+
export const useConfigurableProgram = (patientUuid: string) => {
|
|
93
|
+
const { customUrl } = useConfig() as ConfigObject;
|
|
94
|
+
const { data, error } = useSWR<{ data: Array<ConfigurableProgram> }>(
|
|
95
|
+
customUrl ? `${customUrl}${patientUuid}` : null,
|
|
96
|
+
openmrsFetch,
|
|
97
|
+
);
|
|
98
|
+
const configurablePrograms = data?.data ?? [];
|
|
99
|
+
return {
|
|
100
|
+
configurablePrograms,
|
|
101
|
+
isLoading: !data && !error,
|
|
102
|
+
error: error,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const usePrograms = (patientUuid: string) => {
|
|
107
|
+
const { customUrl } = useConfig() as ConfigObject;
|
|
108
|
+
const {
|
|
109
|
+
data: enrollments,
|
|
110
|
+
isError: enrollError,
|
|
111
|
+
isLoading: enrolLoading,
|
|
112
|
+
isValidating,
|
|
113
|
+
activeEnrollments,
|
|
114
|
+
} = useEnrollments(patientUuid);
|
|
115
|
+
const { data: availablePrograms, eligiblePrograms } = useAvailablePrograms(enrollments);
|
|
116
|
+
const { configurablePrograms, isLoading: configLoading, error: configError } = useConfigurableProgram(patientUuid);
|
|
117
|
+
|
|
118
|
+
const status = customUrl
|
|
119
|
+
? { isLoading: configLoading, isError: configError }
|
|
120
|
+
: { isLoading: enrolLoading, isError: enrollError };
|
|
121
|
+
return {
|
|
122
|
+
enrollments,
|
|
123
|
+
...status,
|
|
124
|
+
isValidating,
|
|
125
|
+
activeEnrollments,
|
|
126
|
+
availablePrograms,
|
|
127
|
+
eligiblePrograms,
|
|
128
|
+
configurablePrograms,
|
|
129
|
+
};
|
|
130
|
+
};
|
package/src/types/index.ts
CHANGED
|
@@ -15,11 +15,15 @@ export interface PatientProgram {
|
|
|
15
15
|
states: Array<{}>;
|
|
16
16
|
links?: Links;
|
|
17
17
|
}>;
|
|
18
|
+
concept: {
|
|
19
|
+
display: string;
|
|
20
|
+
uuid: string;
|
|
21
|
+
};
|
|
18
22
|
links: Links;
|
|
19
23
|
};
|
|
20
24
|
display: string;
|
|
21
|
-
dateEnrolled:
|
|
22
|
-
dateCompleted:
|
|
25
|
+
dateEnrolled: string;
|
|
26
|
+
dateCompleted: string | null;
|
|
23
27
|
location?: {
|
|
24
28
|
uuid: string;
|
|
25
29
|
display: string;
|
|
@@ -95,3 +99,13 @@ export interface SessionData {
|
|
|
95
99
|
retired: false;
|
|
96
100
|
links: Links;
|
|
97
101
|
}
|
|
102
|
+
|
|
103
|
+
export interface ConfigurableProgram extends PatientProgram {
|
|
104
|
+
uuid: string;
|
|
105
|
+
display: string;
|
|
106
|
+
enrollmentFormUuid: string;
|
|
107
|
+
discontinuationFormUuid: string;
|
|
108
|
+
enrollmentStatus: string;
|
|
109
|
+
dateEnrolled: string;
|
|
110
|
+
dateCompleted: string;
|
|
111
|
+
}
|