@kenyaemr/esm-appointments-app 7.0.2-pre.65

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 (181) hide show
  1. package/.turbo/turbo-build.log +42 -0
  2. package/dist/130.js +2 -0
  3. package/dist/130.js.LICENSE.txt +3 -0
  4. package/dist/130.js.map +1 -0
  5. package/dist/152.js +1 -0
  6. package/dist/152.js.map +1 -0
  7. package/dist/224.js +1 -0
  8. package/dist/224.js.map +1 -0
  9. package/dist/255.js +2 -0
  10. package/dist/255.js.LICENSE.txt +9 -0
  11. package/dist/255.js.map +1 -0
  12. package/dist/271.js +1 -0
  13. package/dist/303.js +1 -0
  14. package/dist/303.js.map +1 -0
  15. package/dist/309.js +1 -0
  16. package/dist/309.js.map +1 -0
  17. package/dist/319.js +1 -0
  18. package/dist/4.js +1 -0
  19. package/dist/4.js.map +1 -0
  20. package/dist/445.js +2 -0
  21. package/dist/445.js.LICENSE.txt +54 -0
  22. package/dist/445.js.map +1 -0
  23. package/dist/460.js +1 -0
  24. package/dist/501.js +1 -0
  25. package/dist/501.js.map +1 -0
  26. package/dist/574.js +1 -0
  27. package/dist/591.js +2 -0
  28. package/dist/591.js.LICENSE.txt +32 -0
  29. package/dist/591.js.map +1 -0
  30. package/dist/644.js +1 -0
  31. package/dist/729.js +1 -0
  32. package/dist/729.js.map +1 -0
  33. package/dist/757.js +1 -0
  34. package/dist/784.js +2 -0
  35. package/dist/784.js.LICENSE.txt +9 -0
  36. package/dist/784.js.map +1 -0
  37. package/dist/788.js +1 -0
  38. package/dist/807.js +1 -0
  39. package/dist/833.js +1 -0
  40. package/dist/857.js +2 -0
  41. package/dist/857.js.LICENSE.txt +5 -0
  42. package/dist/857.js.map +1 -0
  43. package/dist/904.js +1 -0
  44. package/dist/904.js.map +1 -0
  45. package/dist/kenyaemr-esm-appointments-app.js +1 -0
  46. package/dist/kenyaemr-esm-appointments-app.js.buildmanifest.json +699 -0
  47. package/dist/kenyaemr-esm-appointments-app.js.map +1 -0
  48. package/dist/main.js +2 -0
  49. package/dist/main.js.LICENSE.txt +64 -0
  50. package/dist/main.js.map +1 -0
  51. package/dist/routes.json +1 -0
  52. package/jest.config.js +3 -0
  53. package/package.json +57 -0
  54. package/src/admin/appointment-services/appointment-services-hook.ts +31 -0
  55. package/src/admin/appointment-services/appointment-services-validation.ts +17 -0
  56. package/src/admin/appointment-services/appointment-services.component.tsx +182 -0
  57. package/src/admin/appointment-services/appointment-services.scss +25 -0
  58. package/src/appointments/appointment-tabs.component.tsx +48 -0
  59. package/src/appointments/appointment-tabs.scss +53 -0
  60. package/src/appointments/appointment-tabs.test.tsx +55 -0
  61. package/src/appointments/common-components/appointments-actions.component.tsx +86 -0
  62. package/src/appointments/common-components/appointments-actions.scss +4 -0
  63. package/src/appointments/common-components/appointments-actions.test.tsx +201 -0
  64. package/src/appointments/common-components/appointments-table.component.tsx +277 -0
  65. package/src/appointments/common-components/appointments-table.scss +133 -0
  66. package/src/appointments/common-components/appointments-table.test.tsx +134 -0
  67. package/src/appointments/common-components/checkin-button.component.tsx +43 -0
  68. package/src/appointments/common-components/end-appointment-modal.component.tsx +104 -0
  69. package/src/appointments/common-components/end-appointment-modal.test.tsx +80 -0
  70. package/src/appointments/common-components/location-select-option.component.tsx +48 -0
  71. package/src/appointments/details/appointment-details.component.tsx +91 -0
  72. package/src/appointments/details/appointment-details.scss +81 -0
  73. package/src/appointments/details/appointment-details.test.tsx +103 -0
  74. package/src/appointments/scheduled/appointments-list.component.tsx +33 -0
  75. package/src/appointments/scheduled/early-appointments.component.tsx +32 -0
  76. package/src/appointments/scheduled/scheduled-appointments.component.tsx +215 -0
  77. package/src/appointments/scheduled/scheduled-appointments.scss +4 -0
  78. package/src/appointments/unscheduled/unscheduled-appointments.component.tsx +146 -0
  79. package/src/appointments/unscheduled/unscheduled-appointments.test.tsx +131 -0
  80. package/src/appointments/utils.tsx +80 -0
  81. package/src/appointments.component.tsx +44 -0
  82. package/src/appointments.test.tsx +15 -0
  83. package/src/calendar/appointments-calendar-view-view.scss +24 -0
  84. package/src/calendar/appointments-calendar-view.component.tsx +36 -0
  85. package/src/calendar/appointments-calendar-view.test.tsx +22 -0
  86. package/src/calendar/header/calendar-header.component.tsx +34 -0
  87. package/src/calendar/header/calendar-header.scss +32 -0
  88. package/src/calendar/monthly/days-of-week.component.tsx +16 -0
  89. package/src/calendar/monthly/days-of-week.scss +33 -0
  90. package/src/calendar/monthly/monthly-calendar-view.component.tsx +34 -0
  91. package/src/calendar/monthly/monthly-header.module.scss +14 -0
  92. package/src/calendar/monthly/monthly-header.module.tsx +40 -0
  93. package/src/calendar/monthly/monthly-view-workload.scss +188 -0
  94. package/src/calendar/monthly/monthly-workload-view-expanded.component.tsx +42 -0
  95. package/src/calendar/monthly/monthly-workload-view.component.tsx +109 -0
  96. package/src/config-schema.ts +151 -0
  97. package/src/constants.ts +55 -0
  98. package/src/createDashboardLink.component.tsx +39 -0
  99. package/src/dashboard.meta.ts +21 -0
  100. package/src/declarations.d.ts +4 -0
  101. package/src/empty-state/empty-data-illustration.component.tsx +39 -0
  102. package/src/empty-state/empty-state.component.tsx +32 -0
  103. package/src/empty-state/empty-state.scss +69 -0
  104. package/src/form/appointments-form.component.tsx +891 -0
  105. package/src/form/appointments-form.resource.ts +165 -0
  106. package/src/form/appointments-form.scss +113 -0
  107. package/src/form/appointments-form.test.tsx +212 -0
  108. package/src/header/appointments-header.component.tsx +79 -0
  109. package/src/header/appointments-header.scss +95 -0
  110. package/src/header/appointments-illustration.component.tsx +22 -0
  111. package/src/helpers/excel.ts +61 -0
  112. package/src/helpers/functions.ts +82 -0
  113. package/src/helpers/index.ts +2 -0
  114. package/src/helpers/time.tsx +15 -0
  115. package/src/home/home-appointments.component.tsx +22 -0
  116. package/src/home/home-appointments.scss +10 -0
  117. package/src/hooks/patientAppointmentContext.ts +15 -0
  118. package/src/hooks/selectedDateContext.ts +10 -0
  119. package/src/hooks/useAppointmentList.ts +48 -0
  120. package/src/hooks/useAppointmentService.ts +11 -0
  121. package/src/hooks/useAppointmentsCalendar.ts +68 -0
  122. package/src/hooks/useClinicalMetrics.ts +79 -0
  123. package/src/hooks/useDefaultLocation.ts +14 -0
  124. package/src/hooks/useOverlay.tsx +45 -0
  125. package/src/hooks/usePatientAppointmentHistory.ts +49 -0
  126. package/src/hooks/useProviders.ts +18 -0
  127. package/src/hooks/useTodaysVisits.ts +19 -0
  128. package/src/hooks/useUnscheduledAppointments.ts +45 -0
  129. package/src/index.ts +111 -0
  130. package/src/metrics/appointments-metrics.component.tsx +71 -0
  131. package/src/metrics/appointments-metrics.scss +15 -0
  132. package/src/metrics/appointments-metrics.test.tsx +49 -0
  133. package/src/metrics/metrics-card.component.tsx +76 -0
  134. package/src/metrics/metrics-card.scss +77 -0
  135. package/src/metrics/metrics-header.component.tsx +62 -0
  136. package/src/metrics/metrics-header.scss +33 -0
  137. package/src/past-visit/encounter-list.component.tsx +54 -0
  138. package/src/past-visit/past-visit.component.tsx +106 -0
  139. package/src/past-visit/past-visit.resource.ts +25 -0
  140. package/src/past-visit/past-visit.scss +106 -0
  141. package/src/patient-appointments/patient-appointments-action-menu.component.tsx +65 -0
  142. package/src/patient-appointments/patient-appointments-action-menu.scss +7 -0
  143. package/src/patient-appointments/patient-appointments-base.component.tsx +165 -0
  144. package/src/patient-appointments/patient-appointments-base.scss +85 -0
  145. package/src/patient-appointments/patient-appointments-base.test.tsx +91 -0
  146. package/src/patient-appointments/patient-appointments-cancel-modal.component.tsx +66 -0
  147. package/src/patient-appointments/patient-appointments-detailed-summary.component.tsx +15 -0
  148. package/src/patient-appointments/patient-appointments-header.scss +27 -0
  149. package/src/patient-appointments/patient-appointments-header.tsx +42 -0
  150. package/src/patient-appointments/patient-appointments-overview.component.tsx +35 -0
  151. package/src/patient-appointments/patient-appointments-overview.scss +7 -0
  152. package/src/patient-appointments/patient-appointments-table.scss +0 -0
  153. package/src/patient-appointments/patient-appointments-table.tsx +128 -0
  154. package/src/patient-appointments/patient-appointments.resource.ts +72 -0
  155. package/src/patient-appointments/patient-upcoming-appointments-card.component.tsx +122 -0
  156. package/src/patient-appointments/patient-upcoming-appointments-card.scss +46 -0
  157. package/src/patient-search/patient-search.component.tsx +34 -0
  158. package/src/patient-search/patient-search.scss +23 -0
  159. package/src/root.component.tsx +26 -0
  160. package/src/root.scss +50 -0
  161. package/src/routes.json +153 -0
  162. package/src/scheduled-appointments-config-schema.ts +169 -0
  163. package/src/types/index.ts +189 -0
  164. package/src/workload/monthly-view-workload/monthly-view.component.tsx +69 -0
  165. package/src/workload/monthly-view-workload/monthly-workload.scss +223 -0
  166. package/src/workload/monthly-view-workload/monthlyWorkCard.tsx +45 -0
  167. package/src/workload/workload-card.component.tsx +31 -0
  168. package/src/workload/workload.component.tsx +47 -0
  169. package/src/workload/workload.resource.ts +78 -0
  170. package/src/workload/workload.scss +92 -0
  171. package/translations/am.json +148 -0
  172. package/translations/ar.json +148 -0
  173. package/translations/en.json +159 -0
  174. package/translations/es.json +148 -0
  175. package/translations/fr.json +148 -0
  176. package/translations/he.json +148 -0
  177. package/translations/km.json +148 -0
  178. package/translations/zh.json +148 -0
  179. package/translations/zh_CN.json +148 -0
  180. package/tsconfig.json +5 -0
  181. package/webpack.config.js +1 -0
@@ -0,0 +1,201 @@
1
+ import { render } from '@testing-library/react';
2
+ import React from 'react';
3
+ import AppointmentActions from './appointments-actions.component';
4
+ import { useTodaysVisits } from '../../hooks/useTodaysVisits';
5
+ import { type Appointment, AppointmentKind, AppointmentStatus } from '../../types';
6
+ import { useConfig } from '@openmrs/esm-framework';
7
+
8
+ const appointment: Appointment = {
9
+ uuid: '7cd38a6d-377e-491b-8284-b04cf8b8c6d8',
10
+ appointmentNumber: '0000',
11
+ patient: {
12
+ identifier: '100GEJ',
13
+ identifiers: [],
14
+ name: 'John Wilson',
15
+ uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e',
16
+ gender: 'M',
17
+ age: '35',
18
+ },
19
+ service: {
20
+ appointmentServiceId: 1,
21
+ name: 'Outpatient',
22
+ description: null,
23
+ startTime: '',
24
+ endTime: '',
25
+ maxAppointmentsLimit: null,
26
+ durationMins: null,
27
+ location: {
28
+ uuid: '8d6c993e-c2cc-11de-8d13-0010c6dffd0f',
29
+ },
30
+ uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90',
31
+ initialAppointmentStatus: 'Scheduled',
32
+ creatorName: null,
33
+ },
34
+ provider: {
35
+ uuid: 'f9badd80-ab76-11e2-9e96-0800200c9a66',
36
+ person: { uuid: '24252571-dd5a-11e6-9d9c-0242ac150002', display: 'Dr James Cook' },
37
+ },
38
+ location: { name: 'HIV Clinic', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' },
39
+ startDateTime: new Date().toISOString(),
40
+ appointmentKind: AppointmentKind.WALKIN,
41
+ status: null,
42
+ comments: 'Some comments',
43
+ additionalInfo: null,
44
+ providers: [{ uuid: '24252571-dd5a-11e6-9d9c-0242ac150002', display: 'Dr James Cook' }],
45
+ recurring: false,
46
+ voided: false,
47
+ teleconsultationLink: null,
48
+ extensions: [],
49
+ };
50
+
51
+ jest.mock('../../hooks/useTodaysVisits', () => {
52
+ const originalModule = jest.requireActual('../../hooks/useTodaysVisits');
53
+
54
+ return {
55
+ ...originalModule,
56
+ useTodaysVisits: jest.fn(),
57
+ };
58
+ });
59
+
60
+ jest.mock('@openmrs/esm-framework', () => {
61
+ const originalModule = jest.requireActual('@openmrs/esm-framework');
62
+ return {
63
+ ...originalModule,
64
+ useConfig: jest.fn(),
65
+ };
66
+ });
67
+
68
+ describe('AppointmentActions', () => {
69
+ const defaultProps = {
70
+ visits: [],
71
+ appointment: appointment,
72
+ scheduleType: 'Pending',
73
+ mutate: () => {},
74
+ };
75
+
76
+ afterAll(() => {
77
+ jest.useRealTimers();
78
+ });
79
+
80
+ it('renders the check in button when appointment is today and the patient has not checked in and check in button enabled', () => {
81
+ appointment.status = AppointmentStatus.SCHEDULED;
82
+ useConfig.mockImplementation(() => ({
83
+ checkInButton: { enabled: true },
84
+ checkOutButton: { enabled: true },
85
+ }));
86
+ useTodaysVisits.mockImplementation(() => ({
87
+ visits: [],
88
+ }));
89
+ const props = { ...defaultProps };
90
+ const { getByText } = render(<AppointmentActions {...props} />);
91
+ const button = getByText(/check in/i);
92
+ expect(button).toBeInTheDocument();
93
+ });
94
+
95
+ it('does not renders the check in button when appointment is today and the patient has not checked in but the check-in button is disabled', () => {
96
+ appointment.status = AppointmentStatus.SCHEDULED;
97
+ useConfig.mockImplementation(() => ({
98
+ checkInButton: { enabled: false },
99
+ checkOutButton: { enabled: true },
100
+ }));
101
+ useTodaysVisits.mockImplementation(() => ({
102
+ visits: [],
103
+ }));
104
+ const props = { ...defaultProps };
105
+ const { queryByText } = render(<AppointmentActions {...props} />);
106
+ const button = queryByText('Check In');
107
+ expect(button).not.toBeInTheDocument();
108
+ });
109
+
110
+ it('renders the checked out button when the patient has checked out', () => {
111
+ appointment.status = AppointmentStatus.COMPLETED;
112
+ useConfig.mockImplementation(() => ({
113
+ checkInButton: { enabled: true },
114
+ checkOutButton: { enabled: true },
115
+ }));
116
+ useTodaysVisits.mockImplementation(() => ({
117
+ visits: [
118
+ {
119
+ patient: { uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' },
120
+ startDatetime: new Date().toISOString(),
121
+ stopDatetime: new Date().toISOString(),
122
+ },
123
+ ],
124
+ }));
125
+ const props = { ...defaultProps };
126
+ const { getByText } = render(<AppointmentActions {...props} />);
127
+ const button = getByText('Checked out');
128
+ expect(button).toBeInTheDocument();
129
+ });
130
+
131
+ it('renders the check out button when the patient has an active visit and today is the appointment date and the check out button enabled', () => {
132
+ appointment.status = AppointmentStatus.CHECKEDIN;
133
+ useConfig.mockImplementation(() => ({
134
+ checkInButton: { enabled: true },
135
+ checkOutButton: { enabled: true },
136
+ }));
137
+ useTodaysVisits.mockImplementation(() => ({
138
+ visits: [
139
+ {
140
+ patient: { uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' },
141
+ startDatetime: new Date().toISOString(),
142
+ stopDatetime: null,
143
+ },
144
+ ],
145
+ }));
146
+ const props = { ...defaultProps, scheduleType: 'Scheduled' };
147
+ const { getByText } = render(<AppointmentActions {...props} />);
148
+ const button = getByText('Check out');
149
+ expect(button).toBeInTheDocument();
150
+ });
151
+
152
+ it('does not render check out button when the patient has an active visit and today is the appointment date but the check out button is disabled', () => {
153
+ appointment.status = AppointmentStatus.CHECKEDIN;
154
+ useConfig.mockImplementation(() => ({
155
+ checkInButton: { enabled: true },
156
+ checkOutButton: { enabled: false },
157
+ }));
158
+ useTodaysVisits.mockImplementation(() => ({
159
+ visits: [
160
+ {
161
+ patient: { uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' },
162
+ startDatetime: new Date().toISOString(),
163
+ stopDatetime: null,
164
+ },
165
+ ],
166
+ }));
167
+ const props = { ...defaultProps, scheduleType: 'Scheduled' };
168
+ const { queryByText } = render(<AppointmentActions {...props} />);
169
+ const button = queryByText('Check out');
170
+ expect(button).not.toBeInTheDocument();
171
+ });
172
+
173
+ // commenting these tests out as this functionality is not implemented yet so not sure how they would have ever passed?
174
+ /*it('renders the correct button when today is the appointment date and the schedule type is pending', () => {
175
+ useConfig.mockImplementation(() => ({
176
+ checkInButton: { enabled: true },
177
+ checkOutButton: { enabled: true },
178
+ }));
179
+ useTodaysVisits.mockImplementation(() => ({
180
+ visits: [],
181
+ }));
182
+ const props = { ...defaultProps, scheduleType: 'Pending' };
183
+ render(<AppointmentActions {...props} />);
184
+ const button = screen.getByRole('button', { name: /Checked out/i });
185
+ expect(button).toBeInTheDocument();
186
+ });
187
+
188
+ it('renders the correct button when today is the appointment date and the schedule type is not pending', () => {
189
+ useConfig.mockImplementation(() => ({
190
+ checkInButton: { enabled: true },
191
+ checkOutButton: { enabled: true },
192
+ }));
193
+ useTodaysVisits.mockImplementation(() => ({
194
+ visits: [],
195
+ }));
196
+ const props = { ...defaultProps, scheduleType: 'Confirmed' };
197
+ render(<AppointmentActions {...props} />);
198
+ const button = screen.getByRole('button', { name: /Checked out/i });
199
+ expect(button).toBeInTheDocument();
200
+ });*/
201
+ });
@@ -0,0 +1,277 @@
1
+ import React, { useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import dayjs from 'dayjs';
4
+ import isToday from 'dayjs/plugin/isToday';
5
+ import utc from 'dayjs/plugin/utc';
6
+ import {
7
+ Button,
8
+ DataTable,
9
+ DataTableSkeleton,
10
+ Layer,
11
+ OverflowMenu,
12
+ OverflowMenuItem,
13
+ Pagination,
14
+ Search,
15
+ Table,
16
+ TableBody,
17
+ TableCell,
18
+ TableContainer,
19
+ TableExpandedRow,
20
+ TableExpandHeader,
21
+ TableExpandRow,
22
+ TableHead,
23
+ TableHeader,
24
+ TableRow,
25
+ Tile,
26
+ } from '@carbon/react';
27
+ import {
28
+ ConfigurableLink,
29
+ formatDate,
30
+ formatDatetime,
31
+ isDesktop,
32
+ parseDate,
33
+ useConfig,
34
+ useLayoutType,
35
+ launchWorkspace,
36
+ usePagination,
37
+ } from '@openmrs/esm-framework';
38
+ import { Download } from '@carbon/react/icons';
39
+ import { EmptyState } from '../../empty-state/empty-state.component';
40
+ import { downloadAppointmentsAsExcel } from '../../helpers/excel';
41
+ import { useTodaysVisits } from '../../hooks/useTodaysVisits';
42
+ import { type Appointment } from '../../types';
43
+ import { type ConfigObject } from '../../config-schema';
44
+ import { getPageSizes, useAppointmentSearchResults } from '../utils';
45
+ import AppointmentActions from './appointments-actions.component';
46
+ import AppointmentDetails from '../details/appointment-details.component';
47
+ import PatientSearch from '../../patient-search/patient-search.component';
48
+ import styles from './appointments-table.scss';
49
+
50
+ dayjs.extend(utc);
51
+ dayjs.extend(isToday);
52
+
53
+ interface AppointmentsTableProps {
54
+ appointments: Array<Appointment>;
55
+ isLoading: boolean;
56
+ tableHeading: string;
57
+ }
58
+
59
+ const AppointmentsTable: React.FC<AppointmentsTableProps> = ({ appointments, isLoading, tableHeading }) => {
60
+ const { t } = useTranslation();
61
+ const [pageSize, setPageSize] = useState(25);
62
+ const [searchString, setSearchString] = useState('');
63
+ const searchResults = useAppointmentSearchResults(appointments, searchString);
64
+ const { results, goTo, currentPage } = usePagination(searchResults, pageSize);
65
+ const { customPatientChartUrl, patientIdentifierType } = useConfig<ConfigObject>();
66
+ const { visits } = useTodaysVisits();
67
+ const layout = useLayoutType();
68
+ const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
69
+ const headerData = [
70
+ {
71
+ header: t('patientName', 'Patient name'),
72
+ key: 'patientName',
73
+ },
74
+ {
75
+ header: t('identifier', 'Identifier'),
76
+ key: 'identifier',
77
+ },
78
+ {
79
+ header: t('location', 'Location'),
80
+ key: 'location',
81
+ },
82
+ {
83
+ header: t('serviceType', 'Service type'),
84
+ key: 'serviceType',
85
+ },
86
+ {
87
+ header: t('status', 'Status'),
88
+ key: 'status',
89
+ },
90
+ ];
91
+
92
+ const rowData = results?.map((appointment) => ({
93
+ id: appointment.uuid,
94
+ patientName: (
95
+ <ConfigurableLink
96
+ className={styles.link}
97
+ to={customPatientChartUrl}
98
+ templateParams={{ patientUuid: appointment.patient.uuid }}>
99
+ {appointment.patient.name}
100
+ </ConfigurableLink>
101
+ ),
102
+ nextAppointmentDate: '--',
103
+ identifier: patientIdentifierType
104
+ ? appointment.patient[patientIdentifierType.replaceAll(' ', '')] ?? appointment.patient.identifier
105
+ : appointment.patient.identifier,
106
+ dateTime: formatDatetime(new Date(appointment.startDateTime)),
107
+ serviceType: appointment.service.name,
108
+ location: appointment.location?.name,
109
+ provider: appointment.provider,
110
+ status: <AppointmentActions appointment={appointment} />,
111
+ }));
112
+
113
+ if (isLoading) {
114
+ return <DataTableSkeleton role="progressbar" row={5} />;
115
+ }
116
+
117
+ if (!appointments?.length) {
118
+ return (
119
+ <EmptyState
120
+ headerTitle={`${t(tableHeading)} ${t('appointments_lower', 'appointments')}`}
121
+ displayText={`${
122
+ tableHeading?.match(/today/i)
123
+ ? t('appointmentsScheduledForToday', 'appointments scheduled for today')
124
+ : `${t(tableHeading)} ${t('appointments_lower', 'appointments')}`
125
+ }`}
126
+ launchForm={() => launchWorkspace('search-patient')}
127
+ />
128
+ );
129
+ }
130
+
131
+ return (
132
+ <Layer className={styles.container}>
133
+ <Tile className={styles.headerContainer}>
134
+ <div className={isDesktop(layout) ? styles.desktopHeading : styles.tabletHeading}>
135
+ <h4>{`${t(tableHeading)} ${t('appointments', 'Appointments')}`}</h4>
136
+ </div>
137
+ </Tile>
138
+ <div className={styles.toolbar}>
139
+ <Search
140
+ className={styles.searchbar}
141
+ labelText=""
142
+ placeholder={t('filterTable', 'Filter table')}
143
+ onChange={(event) => setSearchString(event.target.value)}
144
+ size={responsiveSize}
145
+ />
146
+ <Button
147
+ size={responsiveSize}
148
+ kind="tertiary"
149
+ renderIcon={Download}
150
+ onClick={() => {
151
+ const date = appointments[0]?.startDateTime
152
+ ? formatDate(parseDate(appointments[0]?.startDateTime), {
153
+ time: false,
154
+ noToday: true,
155
+ })
156
+ : null;
157
+ downloadAppointmentsAsExcel(appointments, `${tableHeading}_appointments_${date}`);
158
+ }}>
159
+ {t('download', 'Download')}
160
+ </Button>
161
+ </div>
162
+ <DataTable
163
+ aria-label={t('appointmentsTable', 'Appointments table')}
164
+ data-floating-menu-container
165
+ rows={rowData}
166
+ headers={headerData}
167
+ isSortable
168
+ size={responsiveSize}
169
+ useZebraStyles>
170
+ {({
171
+ rows,
172
+ headers,
173
+ getExpandHeaderProps,
174
+ getHeaderProps,
175
+ getRowProps,
176
+ getTableProps,
177
+ getTableContainerProps,
178
+ }) => (
179
+ <>
180
+ <TableContainer {...getTableContainerProps()}>
181
+ <Table {...getTableProps()}>
182
+ <TableHead>
183
+ <TableRow>
184
+ <TableExpandHeader enableToggle {...getExpandHeaderProps()} />
185
+ {headers.map((header) => (
186
+ <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
187
+ ))}
188
+ <TableHeader />
189
+ </TableRow>
190
+ </TableHead>
191
+ <TableBody>
192
+ {rows.map((row) => {
193
+ const matchingAppointment = appointments.find((appointment) => appointment.uuid === row.id);
194
+ const patientUuid = matchingAppointment.patient?.uuid;
195
+ const visitDate = dayjs(matchingAppointment.startDateTime);
196
+ const isFutureAppointment = visitDate.isAfter(dayjs());
197
+ const isTodayAppointment = visitDate.isToday();
198
+ const hasActiveVisitToday = visits?.some(
199
+ (visit) => visit?.patient?.uuid === patientUuid && visit?.startDatetime,
200
+ );
201
+
202
+ return (
203
+ <React.Fragment key={row.id}>
204
+ <TableExpandRow {...getRowProps({ row })}>
205
+ {row.cells.map((cell) => (
206
+ <TableCell key={cell.id}>{cell.value?.content ?? cell.value}</TableCell>
207
+ ))}
208
+ <TableCell className="cds--table-column-menu">
209
+ {isFutureAppointment || (isTodayAppointment && !hasActiveVisitToday) ? (
210
+ <OverflowMenu
211
+ align="left"
212
+ aria-label={t('actions', 'Actions')}
213
+ flipped
214
+ size={responsiveSize}>
215
+ <OverflowMenuItem
216
+ className={styles.menuItem}
217
+ itemText={t('editAppointments', 'Edit appointment')}
218
+ size={responsiveSize}
219
+ onClick={() =>
220
+ launchWorkspace('edit-appointments-form', {
221
+ patientUuid: matchingAppointment.patient.uuid,
222
+ appointment: matchingAppointment,
223
+ context: 'editing',
224
+ })
225
+ }
226
+ />
227
+ </OverflowMenu>
228
+ ) : null}
229
+ </TableCell>
230
+ </TableExpandRow>
231
+ {row.isExpanded ? (
232
+ <TableExpandedRow className={styles.expandedRow} colSpan={headers.length + 2}>
233
+ <AppointmentDetails appointment={matchingAppointment} />
234
+ </TableExpandedRow>
235
+ ) : (
236
+ <TableExpandedRow className={styles.hiddenRow} colSpan={headers.length + 2} />
237
+ )}
238
+ </React.Fragment>
239
+ );
240
+ })}
241
+ </TableBody>
242
+ </Table>
243
+ </TableContainer>
244
+ {rows.length === 0 ? (
245
+ <div className={styles.tileContainer}>
246
+ <Layer>
247
+ <Tile className={styles.tile}>
248
+ <div className={styles.tileContent}>
249
+ <p className={styles.content}>{t('noAppointmentsToDisplay', 'No appointments to display')}</p>
250
+ <p className={styles.helper}>{t('checkFilters', 'Check the filters above')}</p>
251
+ </div>
252
+ </Tile>
253
+ </Layer>
254
+ </div>
255
+ ) : null}
256
+ </>
257
+ )}
258
+ </DataTable>
259
+ <Pagination
260
+ backwardText={t('previousPage', 'Previous page')}
261
+ forwardText={t('nextPage', 'Next page')}
262
+ itemsPerPageText={t('itemsPerPage', 'Items per page') + ':'}
263
+ page={currentPage}
264
+ pageNumberText={t('pageNumber', 'Page number')}
265
+ pageSize={pageSize}
266
+ pageSizes={getPageSizes(appointments, pageSize) ?? []}
267
+ onChange={({ page, pageSize }) => {
268
+ goTo(page);
269
+ setPageSize(pageSize);
270
+ }}
271
+ totalItems={appointments.length ?? 0}
272
+ />
273
+ </Layer>
274
+ );
275
+ };
276
+
277
+ export default AppointmentsTable;
@@ -0,0 +1,133 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+ @import '~@openmrs/esm-styleguide/src/vars';
5
+
6
+ .expandedRow > td > div {
7
+ max-height: max-content !important;
8
+ }
9
+
10
+ .expandedRow td {
11
+ padding: 0 2rem;
12
+ }
13
+
14
+ .expandedRow th[colspan] td[colspan] > div:first-child {
15
+ padding: 0 1rem;
16
+ }
17
+
18
+ .hiddenRow {
19
+ display: none;
20
+ }
21
+
22
+ .container {
23
+ border: 1px solid colors.$gray-20;
24
+ }
25
+
26
+ .headerContainer {
27
+ display: flex;
28
+ justify-content: space-between;
29
+ align-items: center;
30
+ padding: layout.$spacing-04 0 layout.$spacing-04 layout.$spacing-05;
31
+ background-color: colors.$white-0;
32
+ }
33
+
34
+ .tabletHeading {
35
+ h4 {
36
+ @include type.type-style('heading-03');
37
+ color: colors.$gray-70;
38
+ }
39
+ }
40
+
41
+ .desktopHeading,
42
+ .tabletHeading {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ text-align: left;
46
+ text-transform: capitalize;
47
+
48
+ h4 {
49
+ @include type.type-style('heading-compact-02');
50
+ color: colors.$gray-70;
51
+
52
+ &:after {
53
+ content: '';
54
+ display: block;
55
+ width: 2rem;
56
+ padding-top: 3px;
57
+ border-bottom: 0.375rem solid var(--brand-03);
58
+ }
59
+ }
60
+ }
61
+
62
+ .emptyStateContent {
63
+ @include type.type-style('heading-compact-01');
64
+ color: $text-02;
65
+ margin-top: layout.$spacing-05;
66
+ margin-bottom: layout.$spacing-03;
67
+ }
68
+
69
+ .searchbar {
70
+ input {
71
+ background-color: colors.$gray-10;
72
+ }
73
+ }
74
+
75
+ .link {
76
+ text-decoration: none;
77
+ max-width: 50%;
78
+ }
79
+
80
+ .toolbar {
81
+ display: flex;
82
+ justify-content: space-between;
83
+ align-items: center;
84
+ }
85
+
86
+ .menuItem {
87
+ max-width: none;
88
+ }
89
+
90
+ .content {
91
+ @include type.type-style('heading-compact-02');
92
+ color: $text-02;
93
+ margin-bottom: 0.5rem;
94
+ }
95
+
96
+ .tileContainer {
97
+ background-color: $ui-02;
98
+ border-top: 1px solid $ui-03;
99
+ padding: 5rem 0;
100
+ }
101
+
102
+ .tile {
103
+ margin: auto;
104
+ width: fit-content;
105
+ }
106
+
107
+ .tileContent {
108
+ display: flex;
109
+ flex-direction: column;
110
+ align-items: center;
111
+ }
112
+
113
+ .layer {
114
+ height: 100%;
115
+
116
+ :global(.cds--btn--primary) {
117
+ background-color: unset;
118
+ }
119
+ }
120
+
121
+ // Overriding styles for RTL support
122
+ html[dir='rtl'] {
123
+ .headerContainer {
124
+ padding: layout.$spacing-04 layout.$spacing-05 layout.$spacing-04 0;
125
+ svg {
126
+ margin-left: 0;
127
+ margin-right: layout.$spacing-03;
128
+ }
129
+ h4 {
130
+ text-align: right;
131
+ }
132
+ }
133
+ }