@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.
Files changed (54) hide show
  1. package/.turbo/turbo-build.log +33 -35
  2. package/dist/238.js +2 -0
  3. package/dist/{707.js.LICENSE.txt → 238.js.LICENSE.txt} +0 -0
  4. package/dist/402.js +1 -1
  5. package/dist/435.js +1 -0
  6. package/dist/461.js +1 -0
  7. package/dist/574.js +1 -1
  8. package/dist/641.js +1 -0
  9. package/dist/709.js +1 -0
  10. package/dist/733.js +1 -0
  11. package/dist/734.js +2 -0
  12. package/dist/734.js.LICENSE.txt +53 -0
  13. package/dist/784.js +1 -1
  14. package/dist/784.js.LICENSE.txt +2 -7
  15. package/dist/821.js +1 -0
  16. package/dist/858.js +1 -1
  17. package/dist/main.js +1 -0
  18. package/dist/openmrs-esm-patient-programs-app.js +1 -1
  19. package/dist/openmrs-esm-patient-programs-app.js.buildmanifest.json +134 -205
  20. package/dist/openmrs-esm-patient-programs-app.old +1 -1
  21. package/package.json +12 -14
  22. package/src/config-schema.ts +12 -0
  23. package/src/dashboard.meta.tsx +1 -1
  24. package/src/index.ts +8 -3
  25. package/src/programs/program-action-button/program-action-button.component.tsx +50 -0
  26. package/src/programs/programs-detailed-summary.component.tsx +68 -37
  27. package/src/programs/programs-detailed-summary.scss +1 -4
  28. package/src/programs/programs-detailed-summary.test.tsx +26 -7
  29. package/src/programs/programs-form.component.tsx +246 -109
  30. package/src/programs/programs-form.scss +1 -9
  31. package/src/programs/programs-form.test.tsx +113 -110
  32. package/src/programs/programs-overview.component.tsx +60 -46
  33. package/src/programs/programs-overview.scss +1 -3
  34. package/src/programs/programs-overview.test.tsx +6 -7
  35. package/src/programs/programs.resource.tsx +63 -8
  36. package/src/types/index.ts +16 -2
  37. package/translations/en.json +10 -2
  38. package/dist/13.js +0 -1
  39. package/dist/142.js +0 -1
  40. package/dist/145.js +0 -1
  41. package/dist/186.js +0 -2
  42. package/dist/186.js.LICENSE.txt +0 -23
  43. package/dist/252.js +0 -2
  44. package/dist/252.js.LICENSE.txt +0 -23
  45. package/dist/336.js +0 -2
  46. package/dist/336.js.LICENSE.txt +0 -20
  47. package/dist/373.js +0 -1
  48. package/dist/635.js +0 -2
  49. package/dist/635.js.LICENSE.txt +0 -16
  50. package/dist/692.js +0 -1
  51. package/dist/701.js +0 -1
  52. package/dist/707.js +0 -2
  53. package/dist/76.js +0 -1
  54. 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, useLayoutType } from '@openmrs/esm-framework';
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
- beforeEach(() => {
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
- await screen.findByRole('group', { name: /Program/i });
60
- expect(screen.getByRole('group', { name: /Date enrolled/i })).toBeInTheDocument();
61
- expect(screen.getByRole('group', { name: /Date completed/i })).toBeInTheDocument();
62
- expect(screen.getByRole('group', { name: /Enrollment location/i })).toBeInTheDocument();
63
- expect(screen.getByRole('option', { name: /Choose a program/i })).toBeInTheDocument();
64
- expect(screen.getByRole('option', { name: /Oncology Screening and Diagnosis/i })).toBeInTheDocument();
65
- expect(screen.getByRole('option', { name: /Choose a location/i })).toBeInTheDocument();
66
- expect(screen.getByRole('option', { name: /Amani Hospital/i })).toBeInTheDocument();
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
- const cancelButton = screen.getByRole('button', { name: /cancel/i });
72
- const enrollButton = screen.getByRole('button', { name: /save and close/i });
73
- expect(cancelButton).toBeInTheDocument();
74
- expect(enrollButton).toBeInTheDocument();
75
- expect(enrollButton).toBeDisabled();
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('closes the form and the workspace when the cancel button is clicked', () => {
79
- renderProgramsForm();
99
+ it('updates a program enrollment', async () => {
100
+ const user = userEvent.setup();
80
101
 
81
- const cancelButton = screen.getByRole('button', { name: /Cancel/i });
82
- userEvent.click(cancelButton);
102
+ renderProgramsForm(mockEnrolledProgramsResponse[0].uuid);
83
103
 
84
- expect(testProps.closeWorkspace).toHaveBeenCalledTimes(1);
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
- it('renders a light background for date inputs in the tablet viewport ', () => {
88
- mockUseLayoutType.mockReturnValueOnce('tablet').mockReturnValueOnce('tablet');
108
+ mockUpdateProgramEnrollment.mockReturnValueOnce(of({ status: 200, statusText: 'OK' }));
89
109
 
90
- renderProgramsForm();
110
+ await waitFor(() => user.type(dateCompletedInput, '05/05/2020'));
91
111
 
92
- expect(screen.getAllByPlaceholderText('dd/mm/yyyy')[0]).toHaveClass('bx--date-picker--light', { exact: false });
93
- expect(screen.getAllByPlaceholderText('dd/mm/yyyy')[1]).toHaveClass('bx--date-picker--light', { exact: false });
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
- describe('Form submission: ', () => {
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
- it('renders a success toast notification upon successfully recording a program enrollment', async () => {
116
- mockCreateProgramEnrollment.mockReturnValueOnce(of({ status: 201, statusText: 'Created' }));
117
-
118
- userEvent.selectOptions(selectProgramInput, [oncologyScreeningProgramUuid]);
119
- userEvent.selectOptions(selectLocationInput, [inpatientWardUuid]);
120
- userEvent.type(enrollmentDateInput, '2020-05-05');
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
- xit('renders an error notification if there was a problem recording a program enrollment', async () => {
151
- const error = {
152
- message: 'Internal Server Error',
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
- mockCreateProgramEnrollment.mockReturnValueOnce(throwError(error));
161
+ renderProgramsForm();
160
162
 
161
- userEvent.selectOptions(selectProgramInput, [oncologyScreeningProgramUuid]);
162
- userEvent.selectOptions(selectLocationInput, [inpatientWardUuid]);
163
- userEvent.type(enrollmentDateInput, '2020-05-05');
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
- expect(screen.getByDisplayValue('Oncology Screening and Diagnosis')).toBeInTheDocument();
166
- expect(screen.getByDisplayValue('2020-05-05')).toBeInTheDocument();
167
- expect(screen.getByDisplayValue('Inpatient Ward')).toBeInTheDocument();
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
- userEvent.click(enrollButton);
172
+ expect(enrollButton).not.toBeDisabled();
171
173
 
172
- expect(mockCreateErrorHandler).toHaveBeenCalledTimes(1);
173
- expect(mockShowNotification).toHaveBeenCalledWith({
174
- critical: true,
175
- description: 'Internal Server Error',
176
- kind: 'error',
177
- title: 'Error saving program enrollment',
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 Add16 from '@carbon/icons-react/es/add/16';
3
- import styles from './programs-overview.scss';
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
- InlineNotification,
18
- } from 'carbon-components-react';
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 { useTranslation } from 'react-i18next';
30
- import { useAvailablePrograms, useEnrollments } from './programs.resource';
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 = window.spaBase + basePath + '/programs';
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 eligiblePrograms = filter(
47
+ const {
48
+ activeEnrollments,
51
49
  availablePrograms,
52
- (program) => !includes(map(enrollments, 'program.uuid'), program.uuid),
53
- );
50
+ configurablePrograms,
51
+ eligiblePrograms,
52
+ enrollments,
53
+ isError,
54
+ isLoading,
55
+ isValidating,
56
+ } = usePrograms(patientUuid);
54
57
 
55
- const { results: paginatedEnrollments, goTo, currentPage } = usePagination(enrollments ?? [], programsCount);
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: enrollment.dateCompleted
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={Add16}
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
- <TableContainer>
108
- {availablePrograms?.length && eligiblePrograms?.length === 0 && (
109
- <InlineNotification
110
- style={{ minWidth: '100%', margin: '0rem', padding: '0rem' }}
111
- kind={'info'}
112
- lowContrast
113
- subtitle={t('noEligibleEnrollments', 'There are no more programs left to enroll this patient in')}
114
- title={t('fullyEnrolled', 'Enrolled in all programs')}
115
- />
116
- )}
117
- <DataTable rows={tableRows} headers={tableHeaders} isSortable={true} size="short">
118
- {({ rows, headers, getHeaderProps, getTableProps }) => (
119
- <Table {...getTableProps()} useZebraStyles>
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
- </DataTable>
147
- </TableContainer>
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={enrollments.length}
167
+ totalItems={isConfigurable ? configurablePrograms.length : enrollments?.length}
154
168
  dashboardLinkUrl={pageUrl}
155
169
  dashboardLinkLabel={urlLabel}
156
170
  />
@@ -1,6 +1,4 @@
1
- @import "~@openmrs/esm-styleguide/src/vars";
2
- @import "~carbon-components/src/globals/scss/vars";
3
- @import "~carbon-components/src/globals/scss/mixins";
1
+ @import '~@openmrs/esm-styleguide/src/vars';
4
2
 
5
3
  .widgetCard {
6
4
  border: 1px solid $ui-03;
@@ -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
- userEvent.click(addButton);
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: data?.data?.results?.length ? data.data.results : null,
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
- map(({ data }) => data),
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/${program}`, {
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
+ };
@@ -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: Date;
22
- dateCompleted: Date | null;
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
+ }