@openmrs/esm-patient-programs-app 3.0.3 → 3.0.4-pre.1082
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/169.js +2 -0
- package/dist/169.js.map +1 -0
- package/dist/206.js +3 -0
- package/dist/{314.openmrs-esm-patient-programs-app.js.LICENSE.txt → 206.js.LICENSE.txt} +0 -0
- package/dist/206.js.map +1 -0
- package/dist/313.js +2 -0
- package/dist/313.js.map +1 -0
- package/dist/339.js +3 -0
- package/dist/339.js.LICENSE.txt +1 -0
- package/dist/339.js.map +1 -0
- package/dist/364.js +3 -0
- package/dist/{545.openmrs-esm-patient-programs-app.js.LICENSE.txt → 364.js.LICENSE.txt} +0 -0
- package/dist/364.js.map +1 -0
- package/dist/373.js +2 -0
- package/dist/373.js.map +1 -0
- package/dist/402.js +2 -0
- package/dist/402.js.map +1 -0
- package/dist/482.js +2 -0
- package/dist/482.js.map +1 -0
- package/dist/539.js +2 -0
- package/dist/539.js.map +1 -0
- package/dist/574.js +1 -0
- package/dist/600.js +2 -0
- package/dist/600.js.map +1 -0
- package/dist/628.js +3 -0
- package/dist/628.js.LICENSE.txt +20 -0
- package/dist/628.js.map +1 -0
- package/dist/635.js +3 -0
- package/dist/635.js.LICENSE.txt +16 -0
- package/dist/635.js.map +1 -0
- package/dist/754.js +2 -0
- package/dist/754.js.map +1 -0
- package/dist/{757.openmrs-esm-patient-programs-app.js → 757.js} +0 -0
- package/dist/78.js +3 -0
- package/dist/{946.openmrs-esm-patient-programs-app.js.LICENSE.txt → 78.js.LICENSE.txt} +0 -0
- package/dist/78.js.map +1 -0
- package/dist/784.js +3 -0
- package/dist/784.js.LICENSE.txt +14 -0
- package/dist/784.js.map +1 -0
- package/dist/879.js +2 -0
- package/dist/879.js.map +1 -0
- package/dist/9.js +2 -0
- package/dist/9.js.map +1 -0
- package/dist/932.js +2 -0
- package/dist/932.js.map +1 -0
- package/dist/943.js +2 -0
- package/dist/943.js.map +1 -0
- package/dist/968.js +2 -0
- package/dist/968.js.map +1 -0
- package/dist/openmrs-esm-patient-programs-app.js +1 -2
- package/dist/openmrs-esm-patient-programs-app.js.buildmanifest.json +462 -54
- package/dist/openmrs-esm-patient-programs-app.js.map +1 -1
- package/package.json +4 -3
- package/src/index.ts +3 -5
- package/src/programs/programs-detailed-summary.component.tsx +37 -8
- package/src/programs/programs-detailed-summary.scss +0 -16
- package/src/programs/programs-detailed-summary.test.tsx +28 -1
- package/src/programs/programs-form.component.tsx +77 -74
- package/src/programs/programs-form.scss +42 -6
- package/src/programs/programs-form.test.tsx +2 -2
- package/src/programs/programs-overview.component.tsx +49 -15
- package/src/programs/programs-overview.scss +0 -16
- package/src/programs/programs-overview.test.tsx +2 -0
- package/src/programs/programs.component.tsx +157 -17
- package/src/programs/programs.resource.tsx +4 -2
- package/translations/en.json +4 -6
- package/dist/314.openmrs-esm-patient-programs-app.js +0 -3
- package/dist/314.openmrs-esm-patient-programs-app.js.map +0 -1
- package/dist/545.openmrs-esm-patient-programs-app.js +0 -3
- package/dist/545.openmrs-esm-patient-programs-app.js.map +0 -1
- package/dist/574.openmrs-esm-patient-programs-app.js +0 -1
- package/dist/639.openmrs-esm-patient-programs-app.js +0 -2
- package/dist/639.openmrs-esm-patient-programs-app.js.map +0 -1
- package/dist/839.openmrs-esm-patient-programs-app.js +0 -2
- package/dist/839.openmrs-esm-patient-programs-app.js.map +0 -1
- package/dist/943.openmrs-esm-patient-programs-app.js +0 -2
- package/dist/943.openmrs-esm-patient-programs-app.js.map +0 -1
- package/dist/946.openmrs-esm-patient-programs-app.js +0 -3
- package/dist/946.openmrs-esm-patient-programs-app.js.map +0 -1
- package/dist/openmrs-esm-patient-programs-app.js.LICENSE.txt +0 -5
- package/src/programs/program-record.component.tsx +0 -93
- package/src/programs/program-record.css +0 -42
- package/src/programs/program-record.test.outdated.tsx +0 -57
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-patient-programs-app",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4-pre.1082",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "Patient programs microfrontend for the OpenMRS SPA",
|
|
6
6
|
"browser": "dist/openmrs-esm-patient-programs-app.js",
|
|
@@ -36,13 +36,14 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@carbon/charts-react": "^0.49.4",
|
|
38
38
|
"@carbon/icons-react": "^10.18.0",
|
|
39
|
-
"@openmrs/esm-patient-common-lib": "^3.0.
|
|
39
|
+
"@openmrs/esm-patient-common-lib": "^3.0.4-pre.1082",
|
|
40
40
|
"carbon-components-react": "^7.25.0",
|
|
41
41
|
"d3": "^7.0.3",
|
|
42
42
|
"lodash-es": "^4.17.15"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@openmrs/esm-framework": "3.x",
|
|
46
|
+
"@openmrs/esm-patient-common-lib": "3.x",
|
|
46
47
|
"carbon-components": "10.x",
|
|
47
48
|
"carbon-icons": "7.x",
|
|
48
49
|
"dayjs": "1.x",
|
|
@@ -51,5 +52,5 @@
|
|
|
51
52
|
"react-router-dom": "5.x",
|
|
52
53
|
"rxjs": "6.x"
|
|
53
54
|
},
|
|
54
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "0ad5b888f283f2ce34c60024781b388a7783de5f"
|
|
55
56
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,10 +8,6 @@ const backendDependencies = {
|
|
|
8
8
|
'webservices.rest': '^2.2.0',
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
const frontendDependencies = {
|
|
12
|
-
'@openmrs/esm-framework': process.env.FRAMEWORK_VERSION,
|
|
13
|
-
};
|
|
14
|
-
|
|
15
11
|
function setupOpenMRS() {
|
|
16
12
|
const moduleName = '@openmrs/esm-patient-programs-app';
|
|
17
13
|
|
|
@@ -27,6 +23,7 @@ function setupOpenMRS() {
|
|
|
27
23
|
{
|
|
28
24
|
id: 'programs-overview-widget',
|
|
29
25
|
slot: 'patient-chart-summary-dashboard-slot',
|
|
26
|
+
order: 0,
|
|
30
27
|
load: getAsyncLifecycle(() => import('./programs/programs-overview.component'), options),
|
|
31
28
|
meta: {
|
|
32
29
|
columnSpan: 4,
|
|
@@ -43,6 +40,7 @@ function setupOpenMRS() {
|
|
|
43
40
|
{
|
|
44
41
|
id: 'programs-summary-dashboard',
|
|
45
42
|
slot: 'patient-chart-dashboard-slot',
|
|
43
|
+
order: 10,
|
|
46
44
|
load: getSyncLifecycle(createDashboardLink(dashboardMeta), options),
|
|
47
45
|
meta: dashboardMeta,
|
|
48
46
|
},
|
|
@@ -57,4 +55,4 @@ function setupOpenMRS() {
|
|
|
57
55
|
};
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
export { backendDependencies,
|
|
58
|
+
export { backendDependencies, importTranslation, setupOpenMRS };
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import dayjs from 'dayjs';
|
|
3
3
|
import Add16 from '@carbon/icons-react/es/add/16';
|
|
4
|
+
import filter from 'lodash-es/filter';
|
|
5
|
+
import includes from 'lodash-es/includes';
|
|
6
|
+
import map from 'lodash-es/map';
|
|
4
7
|
import styles from './programs-detailed-summary.scss';
|
|
5
|
-
import { EmptyState, ErrorState, launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';
|
|
8
|
+
import { CardHeader, EmptyState, ErrorState, launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';
|
|
6
9
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { useEnrollments } from './programs.resource';
|
|
10
|
+
import { useAvailablePrograms, useEnrollments } from './programs.resource';
|
|
8
11
|
import { useProgramsContext } from './programs.context';
|
|
9
12
|
import {
|
|
10
13
|
Button,
|
|
@@ -20,7 +23,9 @@ import {
|
|
|
20
23
|
TableRow,
|
|
21
24
|
DataTableHeader,
|
|
22
25
|
DataTableRow,
|
|
26
|
+
InlineNotification,
|
|
23
27
|
} from 'carbon-components-react';
|
|
28
|
+
import { formatDate, formatDatetime } from '@openmrs/esm-framework';
|
|
24
29
|
|
|
25
30
|
interface ProgramsDetailedSummaryProps {}
|
|
26
31
|
|
|
@@ -31,6 +36,11 @@ const ProgramsDetailedSummary: React.FC<ProgramsDetailedSummaryProps> = () => {
|
|
|
31
36
|
const headerTitle = t('carePrograms', 'Care Programs');
|
|
32
37
|
|
|
33
38
|
const { data: enrolledPrograms, isError, isLoading, isValidating } = useEnrollments(patientUuid);
|
|
39
|
+
const { data: availablePrograms } = useAvailablePrograms();
|
|
40
|
+
const eligiblePrograms = filter(
|
|
41
|
+
availablePrograms,
|
|
42
|
+
(program) => !includes(map(enrolledPrograms, 'program.uuid'), program.uuid),
|
|
43
|
+
);
|
|
34
44
|
|
|
35
45
|
const tableHeaders: Array<DataTableHeader> = React.useMemo(
|
|
36
46
|
() => [
|
|
@@ -38,6 +48,10 @@ const ProgramsDetailedSummary: React.FC<ProgramsDetailedSummaryProps> = () => {
|
|
|
38
48
|
key: 'display',
|
|
39
49
|
header: t('activePrograms', 'Active programs'),
|
|
40
50
|
},
|
|
51
|
+
{
|
|
52
|
+
key: 'location',
|
|
53
|
+
header: t('location', 'Location'),
|
|
54
|
+
},
|
|
41
55
|
{
|
|
42
56
|
key: 'dateEnrolled',
|
|
43
57
|
header: t('dateEnrolled', 'Date enrolled'),
|
|
@@ -55,9 +69,10 @@ const ProgramsDetailedSummary: React.FC<ProgramsDetailedSummaryProps> = () => {
|
|
|
55
69
|
return {
|
|
56
70
|
id: program.uuid,
|
|
57
71
|
display: program.display,
|
|
58
|
-
|
|
72
|
+
location: program.location?.display,
|
|
73
|
+
dateEnrolled: formatDatetime(new Date(program.dateEnrolled)),
|
|
59
74
|
status: program.dateCompleted
|
|
60
|
-
? `${t('completedOn', 'Completed On')} ${
|
|
75
|
+
? `${t('completedOn', 'Completed On')} ${formatDate(new Date(program.dateCompleted))}`
|
|
61
76
|
: t('active', 'Active'),
|
|
62
77
|
};
|
|
63
78
|
});
|
|
@@ -70,14 +85,28 @@ const ProgramsDetailedSummary: React.FC<ProgramsDetailedSummaryProps> = () => {
|
|
|
70
85
|
if (enrolledPrograms?.length) {
|
|
71
86
|
return (
|
|
72
87
|
<div className={styles.widgetCard}>
|
|
73
|
-
<
|
|
74
|
-
<h4 className={`${styles.productiveHeading03} ${styles.text02}`}>{headerTitle}</h4>
|
|
88
|
+
<CardHeader title={headerTitle}>
|
|
75
89
|
<span>{isValidating ? <InlineLoading /> : null}</span>
|
|
76
|
-
<Button
|
|
90
|
+
<Button
|
|
91
|
+
kind="ghost"
|
|
92
|
+
renderIcon={Add16}
|
|
93
|
+
iconDescription="Add programs"
|
|
94
|
+
onClick={launchProgramsForm}
|
|
95
|
+
disabled={availablePrograms?.length && eligiblePrograms?.length === 0}
|
|
96
|
+
>
|
|
77
97
|
{t('add', 'Add')}
|
|
78
98
|
</Button>
|
|
79
|
-
</
|
|
99
|
+
</CardHeader>
|
|
80
100
|
<TableContainer>
|
|
101
|
+
{availablePrograms?.length && eligiblePrograms?.length === 0 && (
|
|
102
|
+
<InlineNotification
|
|
103
|
+
style={{ minWidth: '100%', margin: '0rem', padding: '0rem' }}
|
|
104
|
+
kind={'info'}
|
|
105
|
+
lowContrast={true}
|
|
106
|
+
subtitle={t('noEligibleEnrollments', 'There are no more programs left to enroll this patient in')}
|
|
107
|
+
title={t('fullyEnrolled', 'Enrolled in all programs')}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
81
110
|
<DataTable rows={tableRows} headers={tableHeaders} isSortable={true} size="short">
|
|
82
111
|
{({ rows, headers, getHeaderProps, getTableProps }) => (
|
|
83
112
|
<Table {...getTableProps()} useZebraStyles>
|
|
@@ -6,19 +6,3 @@
|
|
|
6
6
|
.widgetCard {
|
|
7
7
|
border: 1px solid $ui-03;
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
.programsHeader {
|
|
11
|
-
display: flex;
|
|
12
|
-
justify-content: space-between;
|
|
13
|
-
align-items: center;
|
|
14
|
-
padding: $spacing-04 0 $spacing-04 $spacing-05;
|
|
15
|
-
background-color: $ui-background;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.programsHeader > h4:after {
|
|
19
|
-
content: "";
|
|
20
|
-
display: block;
|
|
21
|
-
width: 2rem;
|
|
22
|
-
padding-top: 0.188rem;
|
|
23
|
-
border-bottom: 0.375rem solid $brand-teal-01;
|
|
24
|
-
}
|
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { screen } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
|
-
import { openmrsFetch } from '@openmrs/esm-framework';
|
|
4
|
+
import { formatDate, formatDatetime, openmrsFetch, usePagination } from '@openmrs/esm-framework';
|
|
5
5
|
import { mockEnrolledProgramsResponse } from '../../../../__mocks__/programs.mock';
|
|
6
6
|
import { swrRender, waitForLoadingToFinish } from '../../../../tools/test-helpers';
|
|
7
7
|
import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';
|
|
8
8
|
import ProgramsDetailedSummary from './programs-detailed-summary.component';
|
|
9
|
+
import dayjs from 'dayjs';
|
|
9
10
|
|
|
10
11
|
const mockOpenmrsFetch = openmrsFetch as jest.Mock;
|
|
12
|
+
const mockUsePagination = usePagination as jest.Mock;
|
|
13
|
+
const mockFormatDate = formatDate as jest.Mock;
|
|
14
|
+
const mockFormatDateTime = formatDatetime as jest.Mock;
|
|
15
|
+
|
|
16
|
+
jest.mock('@openmrs/esm-framework', () => {
|
|
17
|
+
const originalModule = jest.requireActual('@openmrs/esm-framework');
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
...originalModule,
|
|
21
|
+
formatDate: jest.fn(),
|
|
22
|
+
formatDatetime: jest.fn(),
|
|
23
|
+
openmrsFetch: jest.fn(),
|
|
24
|
+
usePagination: jest.fn().mockImplementation(() => ({
|
|
25
|
+
currentPage: 1,
|
|
26
|
+
goTo: () => {},
|
|
27
|
+
results: [],
|
|
28
|
+
})),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
11
31
|
|
|
12
32
|
jest.mock('@openmrs/esm-patient-common-lib', () => {
|
|
13
33
|
const originalModule = jest.requireActual('@openmrs/esm-patient-common-lib');
|
|
@@ -56,6 +76,13 @@ describe('ProgramsDetailedSummary ', () => {
|
|
|
56
76
|
|
|
57
77
|
it("renders a detailed tabular summary of the patient's program enrollments", async () => {
|
|
58
78
|
mockOpenmrsFetch.mockReturnValueOnce({ data: { results: mockEnrolledProgramsResponse } });
|
|
79
|
+
mockUsePagination.mockImplementation(() => ({
|
|
80
|
+
currentPage: 1,
|
|
81
|
+
goTo: () => {},
|
|
82
|
+
results: mockEnrolledProgramsResponse.slice(0, 5),
|
|
83
|
+
}));
|
|
84
|
+
mockFormatDate.mockImplementation((dateTime) => dayjs(dateTime).format('MMM-YYYY'));
|
|
85
|
+
mockFormatDateTime.mockImplementation((dateTime) => dayjs(dateTime).format('DD-MMM-YYYY HH:mm'));
|
|
59
86
|
|
|
60
87
|
renderProgramsOverview();
|
|
61
88
|
|
|
@@ -14,7 +14,16 @@ import {
|
|
|
14
14
|
useSessionUser,
|
|
15
15
|
useLocations,
|
|
16
16
|
} from '@openmrs/esm-framework';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
Button,
|
|
19
|
+
DatePicker,
|
|
20
|
+
DatePickerInput,
|
|
21
|
+
Select,
|
|
22
|
+
SelectItem,
|
|
23
|
+
Form,
|
|
24
|
+
FormGroup,
|
|
25
|
+
ButtonSet,
|
|
26
|
+
} from 'carbon-components-react';
|
|
18
27
|
import { createProgramEnrollment, useAvailablePrograms, useEnrollments } from './programs.resource';
|
|
19
28
|
|
|
20
29
|
interface ProgramsFormProps {
|
|
@@ -95,89 +104,83 @@ const ProgramsForm: React.FC<ProgramsFormProps> = ({ patientUuid, isTablet }) =>
|
|
|
95
104
|
);
|
|
96
105
|
|
|
97
106
|
return (
|
|
98
|
-
<Form
|
|
99
|
-
<
|
|
100
|
-
<
|
|
107
|
+
<Form className={styles.form} onSubmit={handleSubmit}>
|
|
108
|
+
<div className={styles.formContainer}>
|
|
109
|
+
<FormGroup style={{ width: '50%' }} legendText={t('program', 'Program')}>
|
|
110
|
+
<div className={styles.selectContainer}>
|
|
111
|
+
<Select
|
|
112
|
+
id="program"
|
|
113
|
+
invalidText={t('required', 'Required')}
|
|
114
|
+
labelText=""
|
|
115
|
+
light={isTablet}
|
|
116
|
+
onChange={(event) => setSelectedProgram(event.target.value)}
|
|
117
|
+
>
|
|
118
|
+
{!selectedProgram ? <SelectItem text={t('chooseProgram', 'Choose a program')} value="" /> : null}
|
|
119
|
+
{eligiblePrograms?.length > 0 &&
|
|
120
|
+
eligiblePrograms.map((program) => (
|
|
121
|
+
<SelectItem key={program.uuid} text={program.display} value={program.uuid}>
|
|
122
|
+
{program.display}
|
|
123
|
+
</SelectItem>
|
|
124
|
+
))}
|
|
125
|
+
</Select>
|
|
126
|
+
</div>
|
|
127
|
+
</FormGroup>
|
|
128
|
+
<FormGroup legendText={t('dateEnrolled', 'Date enrolled')}>
|
|
129
|
+
<DatePicker
|
|
130
|
+
id="enrollmentDate"
|
|
131
|
+
datePickerType="single"
|
|
132
|
+
dateFormat="d/m/Y"
|
|
133
|
+
light={isTablet}
|
|
134
|
+
maxDate={new Date().toISOString()}
|
|
135
|
+
placeholder="dd/mm/yyyy"
|
|
136
|
+
onChange={([date]) => setEnrollmentDate(date)}
|
|
137
|
+
value={enrollmentDate}
|
|
138
|
+
>
|
|
139
|
+
<DatePickerInput id="enrollmentDateInput" labelText="" />
|
|
140
|
+
</DatePicker>
|
|
141
|
+
</FormGroup>
|
|
142
|
+
<FormGroup legendText={t('dateCompleted', 'Date completed')}>
|
|
143
|
+
<DatePicker
|
|
144
|
+
id="completionDate"
|
|
145
|
+
datePickerType="single"
|
|
146
|
+
dateFormat="d/m/Y"
|
|
147
|
+
light={isTablet}
|
|
148
|
+
minDate={new Date(enrollmentDate).toISOString()}
|
|
149
|
+
maxDate={new Date().toISOString()}
|
|
150
|
+
placeholder="dd/mm/yyyy"
|
|
151
|
+
onChange={([date]) => setCompletionDate(date)}
|
|
152
|
+
value={completionDate}
|
|
153
|
+
>
|
|
154
|
+
<DatePickerInput id="completionDateInput" labelText="" />
|
|
155
|
+
</DatePicker>
|
|
156
|
+
</FormGroup>
|
|
157
|
+
<FormGroup style={{ width: '50%' }} legendText={t('enrollmentLocation', 'Enrollment location')}>
|
|
101
158
|
<Select
|
|
102
|
-
id="
|
|
103
|
-
invalidText=
|
|
159
|
+
id="location"
|
|
160
|
+
invalidText="Required"
|
|
104
161
|
labelText=""
|
|
105
162
|
light={isTablet}
|
|
106
|
-
onChange={(event) =>
|
|
163
|
+
onChange={(event) => setUserLocation(event.target.value)}
|
|
164
|
+
value={userLocation}
|
|
107
165
|
>
|
|
108
|
-
{!
|
|
109
|
-
{
|
|
110
|
-
|
|
111
|
-
<SelectItem key={
|
|
112
|
-
{
|
|
166
|
+
{!userLocation ? <SelectItem text={t('chooseLocation', 'Choose a location')} value="" /> : null}
|
|
167
|
+
{availableLocations?.length > 0 &&
|
|
168
|
+
availableLocations.map((location) => (
|
|
169
|
+
<SelectItem key={location.uuid} text={location.display} value={location.uuid}>
|
|
170
|
+
{location.display}
|
|
113
171
|
</SelectItem>
|
|
114
172
|
))}
|
|
115
173
|
</Select>
|
|
116
|
-
</
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
title: t('error', 'Error'),
|
|
121
|
-
kind: 'error',
|
|
122
|
-
critical: true,
|
|
123
|
-
description: t('alreadyEnrolledText', 'This patient is already enrolled in all of the available programs'),
|
|
124
|
-
})}
|
|
125
|
-
</FormGroup>
|
|
126
|
-
<FormGroup legendText={t('dateEnrolled', 'Date enrolled')}>
|
|
127
|
-
<DatePicker
|
|
128
|
-
id="enrollmentDate"
|
|
129
|
-
datePickerType="single"
|
|
130
|
-
dateFormat="d/m/Y"
|
|
131
|
-
light={isTablet}
|
|
132
|
-
maxDate={new Date().toISOString()}
|
|
133
|
-
placeholder="dd/mm/yyyy"
|
|
134
|
-
onChange={([date]) => setEnrollmentDate(date)}
|
|
135
|
-
value={enrollmentDate}
|
|
136
|
-
>
|
|
137
|
-
<DatePickerInput id="enrollmentDateInput" labelText="" />
|
|
138
|
-
</DatePicker>
|
|
139
|
-
</FormGroup>
|
|
140
|
-
<FormGroup legendText={t('dateCompleted', 'Date completed')}>
|
|
141
|
-
<DatePicker
|
|
142
|
-
id="completionDate"
|
|
143
|
-
datePickerType="single"
|
|
144
|
-
dateFormat="d/m/Y"
|
|
145
|
-
light={isTablet}
|
|
146
|
-
minDate={new Date(enrollmentDate).toISOString()}
|
|
147
|
-
maxDate={new Date().toISOString()}
|
|
148
|
-
placeholder="dd/mm/yyyy"
|
|
149
|
-
onChange={([date]) => setCompletionDate(date)}
|
|
150
|
-
value={completionDate}
|
|
151
|
-
>
|
|
152
|
-
<DatePickerInput id="completionDateInput" labelText="" />
|
|
153
|
-
</DatePicker>
|
|
154
|
-
</FormGroup>
|
|
155
|
-
<FormGroup style={{ width: '50%' }} legendText={t('enrollmentLocation', 'Enrollment location')}>
|
|
156
|
-
<Select
|
|
157
|
-
id="location"
|
|
158
|
-
invalidText="Required"
|
|
159
|
-
labelText=""
|
|
160
|
-
light={isTablet}
|
|
161
|
-
onChange={(event) => setUserLocation(event.target.value)}
|
|
162
|
-
value={userLocation}
|
|
163
|
-
>
|
|
164
|
-
{!userLocation ? <SelectItem text={t('chooseLocation', 'Choose a location')} value="" /> : null}
|
|
165
|
-
{availableLocations?.length > 0 &&
|
|
166
|
-
availableLocations.map((location) => (
|
|
167
|
-
<SelectItem key={location.uuid} text={location.display} value={location.uuid}>
|
|
168
|
-
{location.display}
|
|
169
|
-
</SelectItem>
|
|
170
|
-
))}
|
|
171
|
-
</Select>
|
|
172
|
-
</FormGroup>
|
|
173
|
-
<div style={{ marginTop: '1.625rem' }}>
|
|
174
|
-
<Button style={{ width: '50%' }} kind="secondary" type="button" onClick={closeWorkspace}>
|
|
174
|
+
</FormGroup>
|
|
175
|
+
</div>
|
|
176
|
+
<ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
|
|
177
|
+
<Button className={styles.button} kind="secondary" onClick={closeWorkspace}>
|
|
175
178
|
{t('cancel', 'Cancel')}
|
|
176
179
|
</Button>
|
|
177
|
-
<Button
|
|
178
|
-
{t('
|
|
180
|
+
<Button className={styles.button} kind="primary" disabled={!selectedProgram} type="submit">
|
|
181
|
+
{t('saveAndClose', 'Save and close')}
|
|
179
182
|
</Button>
|
|
180
|
-
</
|
|
183
|
+
</ButtonSet>
|
|
181
184
|
</Form>
|
|
182
185
|
);
|
|
183
186
|
};
|
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
@import "~carbon-components/src/globals/scss/vars";
|
|
3
3
|
@import "~carbon-components/src/globals/scss/mixins";
|
|
4
4
|
|
|
5
|
-
.alreadyEnrolledText {
|
|
6
|
-
@include carbon--type-style("body-short-01");
|
|
7
|
-
color: $text-02;
|
|
8
|
-
min-height: 1rem;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
5
|
.selectContainer {
|
|
12
6
|
display: flex;
|
|
13
7
|
align-items: center;
|
|
@@ -17,4 +11,46 @@
|
|
|
17
11
|
margin-left: 1rem;
|
|
18
12
|
width: 2rem;
|
|
19
13
|
min-height: 2.5rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
.button {
|
|
18
|
+
height: 4rem;
|
|
19
|
+
display: flex;
|
|
20
|
+
align-content: flex-start;
|
|
21
|
+
align-items: baseline;
|
|
22
|
+
min-width: 50%;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.tablet {
|
|
26
|
+
padding: 1.5rem 1rem;
|
|
27
|
+
background-color: $ui-02;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.desktop {
|
|
31
|
+
padding: 0rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.formContainer {
|
|
35
|
+
margin: 1rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.form {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
justify-content: space-between;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Desktop */
|
|
45
|
+
:global(.omrs-breakpoint-gt-tablet) {
|
|
46
|
+
.form {
|
|
47
|
+
height: calc(100vh - 6rem);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Tablet */
|
|
52
|
+
:global(.omrs-breakpoint-lt-desktop) {
|
|
53
|
+
.form {
|
|
54
|
+
height: calc(100vh - 3rem);
|
|
55
|
+
}
|
|
20
56
|
}
|
|
@@ -78,7 +78,7 @@ describe('ProgramsForm: ', () => {
|
|
|
78
78
|
expect(screen.getByRole('option', { name: /Laboratory/i })).toBeInTheDocument();
|
|
79
79
|
|
|
80
80
|
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
|
81
|
-
const enrollButton = screen.getByRole('button', { name: /
|
|
81
|
+
const enrollButton = screen.getByRole('button', { name: /save and close/i });
|
|
82
82
|
expect(cancelButton).toBeInTheDocument();
|
|
83
83
|
expect(enrollButton).toBeInTheDocument();
|
|
84
84
|
expect(enrollButton).toBeDisabled();
|
|
@@ -116,7 +116,7 @@ describe('ProgramsForm: ', () => {
|
|
|
116
116
|
renderProgramsForm();
|
|
117
117
|
|
|
118
118
|
cancelButton = screen.getByRole('button', { name: /cancel/i });
|
|
119
|
-
enrollButton = screen.getByRole('button', { name: /
|
|
119
|
+
enrollButton = screen.getByRole('button', { name: /save and close/i });
|
|
120
120
|
enrollmentDateInput = screen.getAllByRole('textbox', { name: '' })[0];
|
|
121
121
|
selectLocationInput = screen.getAllByRole('combobox', { name: '' })[1];
|
|
122
122
|
selectProgramInput = screen.getAllByRole('combobox', { name: '' })[0];
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import dayjs from 'dayjs';
|
|
3
3
|
import Add16 from '@carbon/icons-react/es/add/16';
|
|
4
4
|
import styles from './programs-overview.scss';
|
|
5
|
-
import { usePagination } from '@openmrs/esm-framework';
|
|
5
|
+
import { formatDate, formatDatetime, usePagination } from '@openmrs/esm-framework';
|
|
6
6
|
import {
|
|
7
7
|
DataTable,
|
|
8
8
|
DataTableSkeleton,
|
|
@@ -15,15 +15,21 @@ import {
|
|
|
15
15
|
TableHead,
|
|
16
16
|
TableHeader,
|
|
17
17
|
TableRow,
|
|
18
|
+
InlineNotification,
|
|
18
19
|
} from 'carbon-components-react';
|
|
20
|
+
import filter from 'lodash-es/filter';
|
|
21
|
+
import includes from 'lodash-es/includes';
|
|
22
|
+
import map from 'lodash-es/map';
|
|
19
23
|
import {
|
|
24
|
+
CardHeader,
|
|
20
25
|
EmptyState,
|
|
21
26
|
ErrorState,
|
|
22
27
|
PatientChartPagination,
|
|
23
28
|
launchPatientWorkspace,
|
|
24
29
|
} from '@openmrs/esm-patient-common-lib';
|
|
25
30
|
import { useTranslation } from 'react-i18next';
|
|
26
|
-
import { useEnrollments } from './programs.resource';
|
|
31
|
+
import { useAvailablePrograms, useEnrollments } from './programs.resource';
|
|
32
|
+
|
|
27
33
|
const programsToShowCount = 5;
|
|
28
34
|
|
|
29
35
|
interface ProgramsOverviewProps {
|
|
@@ -41,11 +47,14 @@ const ProgramsOverview: React.FC<ProgramsOverviewProps> = ({ patientUuid, basePa
|
|
|
41
47
|
const { data: enrollments, isError, isLoading, isValidating } = useEnrollments(patientUuid);
|
|
42
48
|
const activeEnrollments = enrollments?.filter((enrollment) => !enrollment.dateCompleted);
|
|
43
49
|
|
|
44
|
-
const {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
const { data: availablePrograms } = useAvailablePrograms();
|
|
51
|
+
|
|
52
|
+
const eligiblePrograms = filter(
|
|
53
|
+
availablePrograms,
|
|
54
|
+
(program) => !includes(map(enrollments, 'program.uuid'), program.uuid),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const { results: paginatedEnrollments, goTo, currentPage } = usePagination(enrollments ?? [], programsToShowCount);
|
|
49
58
|
|
|
50
59
|
const launchProgramsForm = React.useCallback(() => launchPatientWorkspace('programs-form-workspace'), []);
|
|
51
60
|
|
|
@@ -54,34 +63,59 @@ const ProgramsOverview: React.FC<ProgramsOverviewProps> = ({ patientUuid, basePa
|
|
|
54
63
|
key: 'display',
|
|
55
64
|
header: t('activePrograms', 'Active programs'),
|
|
56
65
|
},
|
|
66
|
+
{
|
|
67
|
+
key: 'location',
|
|
68
|
+
header: t('location', 'Location'),
|
|
69
|
+
},
|
|
57
70
|
{
|
|
58
71
|
key: 'dateEnrolled',
|
|
59
72
|
header: t('dateEnrolled', 'Date enrolled'),
|
|
60
73
|
},
|
|
74
|
+
{
|
|
75
|
+
key: 'status',
|
|
76
|
+
header: t('status', 'Status'),
|
|
77
|
+
},
|
|
61
78
|
];
|
|
62
79
|
|
|
63
80
|
const tableRows = React.useMemo(() => {
|
|
64
81
|
return paginatedEnrollments?.map((enrollment) => ({
|
|
65
|
-
...enrollment,
|
|
66
82
|
id: enrollment.uuid,
|
|
67
83
|
display: enrollment.display,
|
|
68
|
-
|
|
84
|
+
location: enrollment.location?.display,
|
|
85
|
+
dateEnrolled: formatDatetime(new Date(enrollment.dateEnrolled)),
|
|
86
|
+
status: enrollment.dateCompleted
|
|
87
|
+
? `${t('completedOn', 'Completed On')} ${formatDate(new Date(enrollment.dateCompleted))}`
|
|
88
|
+
: t('active', 'Active'),
|
|
69
89
|
}));
|
|
70
|
-
}, [paginatedEnrollments]);
|
|
90
|
+
}, [paginatedEnrollments, t]);
|
|
71
91
|
|
|
72
92
|
if (isLoading) return <DataTableSkeleton role="progressbar" />;
|
|
73
93
|
if (isError) return <ErrorState error={isError} headerTitle={headerTitle} />;
|
|
74
94
|
if (activeEnrollments?.length) {
|
|
75
95
|
return (
|
|
76
96
|
<div className={styles.widgetCard}>
|
|
77
|
-
<
|
|
78
|
-
<h4 className={`${styles.productiveHeading03} ${styles.text02}`}>{headerTitle}</h4>
|
|
97
|
+
<CardHeader title={headerTitle}>
|
|
79
98
|
<span>{isValidating ? <InlineLoading /> : null}</span>
|
|
80
|
-
<Button
|
|
99
|
+
<Button
|
|
100
|
+
kind="ghost"
|
|
101
|
+
renderIcon={Add16}
|
|
102
|
+
iconDescription="Add programs"
|
|
103
|
+
onClick={launchProgramsForm}
|
|
104
|
+
disabled={availablePrograms?.length && eligiblePrograms?.length === 0}
|
|
105
|
+
>
|
|
81
106
|
{t('add', 'Add')}
|
|
82
107
|
</Button>
|
|
83
|
-
</
|
|
108
|
+
</CardHeader>
|
|
84
109
|
<TableContainer>
|
|
110
|
+
{availablePrograms?.length && eligiblePrograms?.length === 0 && (
|
|
111
|
+
<InlineNotification
|
|
112
|
+
style={{ minWidth: '100%', margin: '0rem', padding: '0rem' }}
|
|
113
|
+
kind={'info'}
|
|
114
|
+
lowContrast={true}
|
|
115
|
+
subtitle={t('noEligibleEnrollments', 'There are no more programs left to enroll this patient in')}
|
|
116
|
+
title={t('fullyEnrolled', 'Enrolled in all programs')}
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
85
119
|
<DataTable rows={tableRows} headers={tableHeaders} isSortable={true} size="short">
|
|
86
120
|
{({ rows, headers, getHeaderProps, getTableProps }) => (
|
|
87
121
|
<Table {...getTableProps()} useZebraStyles>
|
|
@@ -119,7 +153,7 @@ const ProgramsOverview: React.FC<ProgramsOverviewProps> = ({ patientUuid, basePa
|
|
|
119
153
|
pageNumber={currentPage}
|
|
120
154
|
pageSize={programsToShowCount}
|
|
121
155
|
pageUrl={pageUrl}
|
|
122
|
-
totalItems={
|
|
156
|
+
totalItems={enrollments.length}
|
|
123
157
|
urlLabel={urlLabel}
|
|
124
158
|
/>
|
|
125
159
|
</div>
|
|
@@ -5,19 +5,3 @@
|
|
|
5
5
|
.widgetCard {
|
|
6
6
|
border: 1px solid $ui-03;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
.programsHeader {
|
|
10
|
-
display: flex;
|
|
11
|
-
justify-content: space-between;
|
|
12
|
-
align-items: center;
|
|
13
|
-
padding: $spacing-04 0 $spacing-04 $spacing-05;
|
|
14
|
-
background-color: $ui-background;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.programsHeader > h4:after {
|
|
18
|
-
content: "";
|
|
19
|
-
display: block;
|
|
20
|
-
width: 2rem;
|
|
21
|
-
padding-top: 0.188rem;
|
|
22
|
-
border-bottom: 0.375rem solid $brand-teal-01;
|
|
23
|
-
}
|