@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.
Files changed (83) hide show
  1. package/dist/169.js +2 -0
  2. package/dist/169.js.map +1 -0
  3. package/dist/206.js +3 -0
  4. package/dist/{314.openmrs-esm-patient-programs-app.js.LICENSE.txt → 206.js.LICENSE.txt} +0 -0
  5. package/dist/206.js.map +1 -0
  6. package/dist/313.js +2 -0
  7. package/dist/313.js.map +1 -0
  8. package/dist/339.js +3 -0
  9. package/dist/339.js.LICENSE.txt +1 -0
  10. package/dist/339.js.map +1 -0
  11. package/dist/364.js +3 -0
  12. package/dist/{545.openmrs-esm-patient-programs-app.js.LICENSE.txt → 364.js.LICENSE.txt} +0 -0
  13. package/dist/364.js.map +1 -0
  14. package/dist/373.js +2 -0
  15. package/dist/373.js.map +1 -0
  16. package/dist/402.js +2 -0
  17. package/dist/402.js.map +1 -0
  18. package/dist/482.js +2 -0
  19. package/dist/482.js.map +1 -0
  20. package/dist/539.js +2 -0
  21. package/dist/539.js.map +1 -0
  22. package/dist/574.js +1 -0
  23. package/dist/600.js +2 -0
  24. package/dist/600.js.map +1 -0
  25. package/dist/628.js +3 -0
  26. package/dist/628.js.LICENSE.txt +20 -0
  27. package/dist/628.js.map +1 -0
  28. package/dist/635.js +3 -0
  29. package/dist/635.js.LICENSE.txt +16 -0
  30. package/dist/635.js.map +1 -0
  31. package/dist/754.js +2 -0
  32. package/dist/754.js.map +1 -0
  33. package/dist/{757.openmrs-esm-patient-programs-app.js → 757.js} +0 -0
  34. package/dist/78.js +3 -0
  35. package/dist/{946.openmrs-esm-patient-programs-app.js.LICENSE.txt → 78.js.LICENSE.txt} +0 -0
  36. package/dist/78.js.map +1 -0
  37. package/dist/784.js +3 -0
  38. package/dist/784.js.LICENSE.txt +14 -0
  39. package/dist/784.js.map +1 -0
  40. package/dist/879.js +2 -0
  41. package/dist/879.js.map +1 -0
  42. package/dist/9.js +2 -0
  43. package/dist/9.js.map +1 -0
  44. package/dist/932.js +2 -0
  45. package/dist/932.js.map +1 -0
  46. package/dist/943.js +2 -0
  47. package/dist/943.js.map +1 -0
  48. package/dist/968.js +2 -0
  49. package/dist/968.js.map +1 -0
  50. package/dist/openmrs-esm-patient-programs-app.js +1 -2
  51. package/dist/openmrs-esm-patient-programs-app.js.buildmanifest.json +462 -54
  52. package/dist/openmrs-esm-patient-programs-app.js.map +1 -1
  53. package/package.json +4 -3
  54. package/src/index.ts +3 -5
  55. package/src/programs/programs-detailed-summary.component.tsx +37 -8
  56. package/src/programs/programs-detailed-summary.scss +0 -16
  57. package/src/programs/programs-detailed-summary.test.tsx +28 -1
  58. package/src/programs/programs-form.component.tsx +77 -74
  59. package/src/programs/programs-form.scss +42 -6
  60. package/src/programs/programs-form.test.tsx +2 -2
  61. package/src/programs/programs-overview.component.tsx +49 -15
  62. package/src/programs/programs-overview.scss +0 -16
  63. package/src/programs/programs-overview.test.tsx +2 -0
  64. package/src/programs/programs.component.tsx +157 -17
  65. package/src/programs/programs.resource.tsx +4 -2
  66. package/translations/en.json +4 -6
  67. package/dist/314.openmrs-esm-patient-programs-app.js +0 -3
  68. package/dist/314.openmrs-esm-patient-programs-app.js.map +0 -1
  69. package/dist/545.openmrs-esm-patient-programs-app.js +0 -3
  70. package/dist/545.openmrs-esm-patient-programs-app.js.map +0 -1
  71. package/dist/574.openmrs-esm-patient-programs-app.js +0 -1
  72. package/dist/639.openmrs-esm-patient-programs-app.js +0 -2
  73. package/dist/639.openmrs-esm-patient-programs-app.js.map +0 -1
  74. package/dist/839.openmrs-esm-patient-programs-app.js +0 -2
  75. package/dist/839.openmrs-esm-patient-programs-app.js.map +0 -1
  76. package/dist/943.openmrs-esm-patient-programs-app.js +0 -2
  77. package/dist/943.openmrs-esm-patient-programs-app.js.map +0 -1
  78. package/dist/946.openmrs-esm-patient-programs-app.js +0 -3
  79. package/dist/946.openmrs-esm-patient-programs-app.js.map +0 -1
  80. package/dist/openmrs-esm-patient-programs-app.js.LICENSE.txt +0 -5
  81. package/src/programs/program-record.component.tsx +0 -93
  82. package/src/programs/program-record.css +0 -42
  83. 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",
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.3",
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": "0613587d69c851115180b05633ff41c5db771a87"
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, frontendDependencies, importTranslation, setupOpenMRS };
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
- dateEnrolled: dayjs(program.dateEnrolled).format('MMM-YYYY'),
72
+ location: program.location?.display,
73
+ dateEnrolled: formatDatetime(new Date(program.dateEnrolled)),
59
74
  status: program.dateCompleted
60
- ? `${t('completedOn', 'Completed On')} ${dayjs(program.dateCompleted).format('MMM-YYYY')}`
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
- <div className={styles.programsHeader}>
74
- <h4 className={`${styles.productiveHeading03} ${styles.text02}`}>{headerTitle}</h4>
88
+ <CardHeader title={headerTitle}>
75
89
  <span>{isValidating ? <InlineLoading /> : null}</span>
76
- <Button kind="ghost" renderIcon={Add16} iconDescription="Add programs" onClick={launchProgramsForm}>
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
- </div>
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 { Button, DatePicker, DatePickerInput, Select, SelectItem, Form, FormGroup } from 'carbon-components-react';
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 style={{ margin: '2rem' }} onSubmit={handleSubmit}>
99
- <FormGroup style={{ width: '50%' }} legendText={t('program', 'Program')}>
100
- <div className={styles.selectContainer}>
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="program"
103
- invalidText={t('required', 'Required')}
159
+ id="location"
160
+ invalidText="Required"
104
161
  labelText=""
105
162
  light={isTablet}
106
- onChange={(event) => setSelectedProgram(event.target.value)}
163
+ onChange={(event) => setUserLocation(event.target.value)}
164
+ value={userLocation}
107
165
  >
108
- {!selectedProgram ? <SelectItem text={t('chooseProgram', 'Choose a program')} value="" /> : null}
109
- {eligiblePrograms?.length > 0 &&
110
- eligiblePrograms.map((program) => (
111
- <SelectItem key={program.uuid} text={program.display} value={program.uuid}>
112
- {program.display}
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
- </div>
117
- {availablePrograms?.length &&
118
- eligiblePrograms?.length === 0 &&
119
- showNotification({
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 style={{ width: '50%' }} kind="primary" type="submit" disabled={!selectedProgram}>
178
- {t('enroll', 'Enroll')}
180
+ <Button className={styles.button} kind="primary" disabled={!selectedProgram} type="submit">
181
+ {t('saveAndClose', 'Save and close')}
179
182
  </Button>
180
- </div>
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: /enroll/i });
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: /enroll/i });
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
- results: paginatedEnrollments,
46
- goTo,
47
- currentPage,
48
- } = usePagination(activeEnrollments ?? [], programsToShowCount);
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
- dateEnrolled: dayjs(enrollment.dateEnrolled).format('MMM-YYYY'),
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
- <div className={styles.programsHeader}>
78
- <h4 className={`${styles.productiveHeading03} ${styles.text02}`}>{headerTitle}</h4>
97
+ <CardHeader title={headerTitle}>
79
98
  <span>{isValidating ? <InlineLoading /> : null}</span>
80
- <Button kind="ghost" renderIcon={Add16} iconDescription="Add programs" onClick={launchProgramsForm}>
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
- </div>
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={activeEnrollments.length}
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
- }
@@ -16,6 +16,8 @@ jest.mock('@openmrs/esm-framework', () => {
16
16
 
17
17
  return {
18
18
  ...originalModule,
19
+ formatDate: jest.fn(),
20
+ formatDatetime: jest.fn(),
19
21
  openmrsFetch: jest.fn(),
20
22
  usePagination: jest.fn().mockImplementation(() => ({
21
23
  currentPage: 1,