@openmrs/esm-service-queues-app 5.0.1-pre.2087

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 (254) hide show
  1. package/.turbo/turbo-build.log +40 -0
  2. package/LICENSE.md +401 -0
  3. package/README.md +23 -0
  4. package/__mocks__/active-visits.mock.ts +97 -0
  5. package/__mocks__/appointments-data.mock.ts +283 -0
  6. package/__mocks__/search.mock.ts +35 -0
  7. package/__mocks__/visits.mock.ts +42 -0
  8. package/dist/121.js +1 -0
  9. package/dist/121.js.map +1 -0
  10. package/dist/130.js +2 -0
  11. package/dist/130.js.LICENSE.txt +5 -0
  12. package/dist/130.js.map +1 -0
  13. package/dist/176.js +1 -0
  14. package/dist/176.js.map +1 -0
  15. package/dist/227.js +1 -0
  16. package/dist/227.js.map +1 -0
  17. package/dist/242.js +1 -0
  18. package/dist/242.js.map +1 -0
  19. package/dist/252.js +1 -0
  20. package/dist/252.js.map +1 -0
  21. package/dist/286.js +1 -0
  22. package/dist/286.js.map +1 -0
  23. package/dist/308.js +1 -0
  24. package/dist/308.js.map +1 -0
  25. package/dist/319.js +1 -0
  26. package/dist/328.js +1 -0
  27. package/dist/328.js.map +1 -0
  28. package/dist/330.js +1 -0
  29. package/dist/330.js.map +1 -0
  30. package/dist/364.js +1 -0
  31. package/dist/364.js.map +1 -0
  32. package/dist/425.js +1 -0
  33. package/dist/425.js.map +1 -0
  34. package/dist/457.js +1 -0
  35. package/dist/457.js.map +1 -0
  36. package/dist/469.js +1 -0
  37. package/dist/469.js.map +1 -0
  38. package/dist/521.js +1 -0
  39. package/dist/521.js.map +1 -0
  40. package/dist/53.js +1 -0
  41. package/dist/53.js.map +1 -0
  42. package/dist/574.js +1 -0
  43. package/dist/591.js +2 -0
  44. package/dist/591.js.LICENSE.txt +32 -0
  45. package/dist/591.js.map +1 -0
  46. package/dist/610.js +1 -0
  47. package/dist/610.js.map +1 -0
  48. package/dist/611.js +1 -0
  49. package/dist/611.js.map +1 -0
  50. package/dist/616.js +1 -0
  51. package/dist/616.js.map +1 -0
  52. package/dist/644.js +1 -0
  53. package/dist/644.js.map +1 -0
  54. package/dist/670.js +2 -0
  55. package/dist/670.js.LICENSE.txt +57 -0
  56. package/dist/670.js.map +1 -0
  57. package/dist/733.js +1 -0
  58. package/dist/733.js.map +1 -0
  59. package/dist/757.js +1 -0
  60. package/dist/764.js +1 -0
  61. package/dist/764.js.map +1 -0
  62. package/dist/766.js +1 -0
  63. package/dist/766.js.map +1 -0
  64. package/dist/784.js +2 -0
  65. package/dist/784.js.LICENSE.txt +9 -0
  66. package/dist/784.js.map +1 -0
  67. package/dist/788.js +1 -0
  68. package/dist/807.js +1 -0
  69. package/dist/833.js +1 -0
  70. package/dist/86.js +1 -0
  71. package/dist/86.js.map +1 -0
  72. package/dist/981.js +1 -0
  73. package/dist/981.js.map +1 -0
  74. package/dist/main.js +1 -0
  75. package/dist/main.js.map +1 -0
  76. package/dist/openmrs-esm-service-queues-app.js +1 -0
  77. package/dist/openmrs-esm-service-queues-app.js.buildmanifest.json +930 -0
  78. package/dist/openmrs-esm-service-queues-app.js.map +1 -0
  79. package/dist/routes.json +1 -0
  80. package/jest.config.js +3 -0
  81. package/package.json +53 -0
  82. package/src/active-visits/active-visits-tab.component.tsx +71 -0
  83. package/src/active-visits/active-visits-tab.test.tsx +19 -0
  84. package/src/active-visits/active-visits-table.component.tsx +543 -0
  85. package/src/active-visits/active-visits-table.resource.ts +443 -0
  86. package/src/active-visits/active-visits-table.scss +271 -0
  87. package/src/active-visits/active-visits-table.test.tsx +131 -0
  88. package/src/active-visits/change-status-dialog.component.tsx +233 -0
  89. package/src/active-visits/change-status-dialog.scss +32 -0
  90. package/src/active-visits/change-status-dialog.test.tsx +107 -0
  91. package/src/add-patient-toqueue/add-patient-toqueue-dialog.component.tsx +218 -0
  92. package/src/add-patient-toqueue/add-patient-toqueue-dialog.scss +32 -0
  93. package/src/add-provider-queue-room/add-provider-queue-room.component.tsx +236 -0
  94. package/src/add-provider-queue-room/add-provider-queue-room.resource.tsx +76 -0
  95. package/src/add-provider-queue-room/add-provider-queue-room.scss +17 -0
  96. package/src/add-provider-queue-room/add-provider-queue-room.test.tsx +98 -0
  97. package/src/clear-queue-entries-dialog/clear-queue-entries-dialog.component.tsx +74 -0
  98. package/src/clear-queue-entries-dialog/clear-queue-entries-dialog.resource.tsx +46 -0
  99. package/src/clear-queue-entries-dialog/clear-queue-entries-dialog.scss +15 -0
  100. package/src/clear-queue-entries-dialog/clear-queue-entries-dialog.test.tsx +42 -0
  101. package/src/clear-queue-entries-dialog/clear-queue-entries.component.tsx +36 -0
  102. package/src/config-schema.ts +177 -0
  103. package/src/constants.ts +7 -0
  104. package/src/createDashboardLink.tsx +35 -0
  105. package/src/current-visit/current-visit-summary.component.tsx +38 -0
  106. package/src/current-visit/current-visit-summary.test.tsx +47 -0
  107. package/src/current-visit/current-visit.resource.ts +84 -0
  108. package/src/current-visit/current-visit.scss +34 -0
  109. package/src/current-visit/hooks/useVitalsConceptMetadata.tsx +101 -0
  110. package/src/current-visit/visit-details/biometrics-config-schema.ts +14 -0
  111. package/src/current-visit/visit-details/current-visit-details.component.tsx +96 -0
  112. package/src/current-visit/visit-details/triage-note.component.tsx +53 -0
  113. package/src/current-visit/visit-details/triage-note.scss +89 -0
  114. package/src/current-visit/visit-details/vitals-config-schema.ts +17 -0
  115. package/src/current-visit/visit-details/vitals.component.tsx +164 -0
  116. package/src/dashboard.meta.ts +5 -0
  117. package/src/declarations.d.ts +2 -0
  118. package/src/helpers/functions.ts +67 -0
  119. package/src/helpers/helpers.ts +170 -0
  120. package/src/helpers/time-helpers.ts +15 -0
  121. package/src/home.component.tsx +29 -0
  122. package/src/home.test.tsx +58 -0
  123. package/src/index.ts +100 -0
  124. package/src/overlay.component.tsx +46 -0
  125. package/src/overlay.scss +96 -0
  126. package/src/overlay.test.tsx +49 -0
  127. package/src/past-visit/past-visit-details/encounter-list.component.tsx +51 -0
  128. package/src/past-visit/past-visit-details/medications-list.component.tsx +95 -0
  129. package/src/past-visit/past-visit-details/notes-list.component.tsx +40 -0
  130. package/src/past-visit/past-visit-details/past-visit-summary.component.tsx +172 -0
  131. package/src/past-visit/past-visit-details/past-visit-summary.scss +58 -0
  132. package/src/past-visit/past-visit.component.tsx +37 -0
  133. package/src/past-visit/past-visit.resource.ts +83 -0
  134. package/src/past-visit/past-visit.scss +126 -0
  135. package/src/past-visit/past-visit.test.tsx +41 -0
  136. package/src/patient-info/appointment-details.component.tsx +98 -0
  137. package/src/patient-info/appointment-details.scss +34 -0
  138. package/src/patient-info/appointment-details.test.tsx +36 -0
  139. package/src/patient-info/appointments.resource.tsx +43 -0
  140. package/src/patient-info/contact-details.component.tsx +71 -0
  141. package/src/patient-info/contact-details.scss +31 -0
  142. package/src/patient-info/contact-details.test.tsx +94 -0
  143. package/src/patient-info/hooks/usePatientAttributes.tsx +42 -0
  144. package/src/patient-info/patient-info.component.tsx +78 -0
  145. package/src/patient-info/patient-info.scss +60 -0
  146. package/src/patient-info/patient-info.test.tsx +45 -0
  147. package/src/patient-queue-header/patient-queue-header.component.tsx +64 -0
  148. package/src/patient-queue-header/patient-queue-header.scss +88 -0
  149. package/src/patient-queue-header/patient-queue-illustration.component.tsx +22 -0
  150. package/src/patient-queue-metrics/clinic-metrics.component.tsx +93 -0
  151. package/src/patient-queue-metrics/clinic-metrics.resource.tsx +51 -0
  152. package/src/patient-queue-metrics/clinic-metrics.scss +11 -0
  153. package/src/patient-queue-metrics/clinic-metrics.test.tsx +87 -0
  154. package/src/patient-queue-metrics/metrics-card.component.tsx +64 -0
  155. package/src/patient-queue-metrics/metrics-card.scss +80 -0
  156. package/src/patient-queue-metrics/metrics-header.component.tsx +74 -0
  157. package/src/patient-queue-metrics/metrics-header.scss +22 -0
  158. package/src/patient-queue-metrics/queue-metrics.resource.ts +54 -0
  159. package/src/patient-search/advanced-search.component.tsx +190 -0
  160. package/src/patient-search/advanced-search.scss +154 -0
  161. package/src/patient-search/advanced-search.test.tsx +29 -0
  162. package/src/patient-search/basic-search.component.tsx +112 -0
  163. package/src/patient-search/basic-search.scss +139 -0
  164. package/src/patient-search/basic-search.test.tsx +23 -0
  165. package/src/patient-search/empty-data-illustration.component.tsx +41 -0
  166. package/src/patient-search/hooks/useActivePatientEnrollment.tsx +29 -0
  167. package/src/patient-search/hooks/useDefaultLocation.tsx +14 -0
  168. package/src/patient-search/hooks/usePatients.tsx +25 -0
  169. package/src/patient-search/hooks/useQueueLocations.tsx +20 -0
  170. package/src/patient-search/hooks/useRecommendedVisitTypes.tsx +36 -0
  171. package/src/patient-search/hooks/useScheduledVisits.tsx +50 -0
  172. package/src/patient-search/patient-scheduled-visits.component.tsx +350 -0
  173. package/src/patient-search/patient-scheduled-visits.scss +131 -0
  174. package/src/patient-search/patient-scheduled-visits.test.tsx +55 -0
  175. package/src/patient-search/patient-search.component.tsx +64 -0
  176. package/src/patient-search/search-illustration.component.tsx +27 -0
  177. package/src/patient-search/search-results.component.tsx +75 -0
  178. package/src/patient-search/search-results.scss +80 -0
  179. package/src/patient-search/search-results.test.tsx +77 -0
  180. package/src/patient-search/search.resource.ts +10 -0
  181. package/src/patient-search/visit-form/base-visit-type.component.tsx +82 -0
  182. package/src/patient-search/visit-form/base-visit-type.scss +100 -0
  183. package/src/patient-search/visit-form/base-visit-type.test.tsx +41 -0
  184. package/src/patient-search/visit-form/queue.resource.ts +73 -0
  185. package/src/patient-search/visit-form/recommended-visit-type.component.tsx +38 -0
  186. package/src/patient-search/visit-form/visit-form.component.tsx +366 -0
  187. package/src/patient-search/visit-form/visit-form.scss +96 -0
  188. package/src/patient-search/visit-form-queue-fields/visit-form-queue-fields.component.tsx +160 -0
  189. package/src/patient-search/visit-form-queue-fields/visit-form-queue-fields.scss +21 -0
  190. package/src/patient-search/visit-form-queue-fields/visit-form-queue-fields.test.tsx +67 -0
  191. package/src/queue-entry-table-components/actions-menu.component.tsx +51 -0
  192. package/src/queue-entry-table-components/actions-menu.scss +3 -0
  193. package/src/queue-entry-table-components/edit-entry.component.tsx +28 -0
  194. package/src/queue-entry-table-components/edit-entry.scss +14 -0
  195. package/src/queue-entry-table-components/open-chart.component.tsx +33 -0
  196. package/src/queue-entry-table-components/open-chart.resource.tsx +14 -0
  197. package/src/queue-entry-table-components/open-chart.scss +14 -0
  198. package/src/queue-entry-table-components/status-icon.component.tsx +17 -0
  199. package/src/queue-entry-table-components/transition-entry.component.tsx +53 -0
  200. package/src/queue-entry-table-components/transition-entry.scss +22 -0
  201. package/src/queue-patient-linelists/queue-linelist-base-table.component.tsx +230 -0
  202. package/src/queue-patient-linelists/queue-linelist-base-table.scss +110 -0
  203. package/src/queue-patient-linelists/queue-linelist-base-table.test.tsx +118 -0
  204. package/src/queue-patient-linelists/queue-linelist-filter.component.tsx +172 -0
  205. package/src/queue-patient-linelists/queue-linelist-filter.scss +63 -0
  206. package/src/queue-patient-linelists/queue-linelist-filter.test.tsx +76 -0
  207. package/src/queue-patient-linelists/queue-linelist.component.tsx +29 -0
  208. package/src/queue-patient-linelists/queue-linelist.resource.tsx +84 -0
  209. package/src/queue-patient-linelists/queue-linelist.test.tsx +22 -0
  210. package/src/queue-patient-linelists/queue-services-table.component.tsx +63 -0
  211. package/src/queue-patient-linelists/scheduled-appointments-table.component.tsx +300 -0
  212. package/src/queue-patient-linelists/scheduled-appointments-table.test.tsx +42 -0
  213. package/src/queue-rooms/queue-room-form.component.tsx +168 -0
  214. package/src/queue-rooms/queue-room-form.test.tsx +65 -0
  215. package/src/queue-rooms/queue-room.resource.tsx +20 -0
  216. package/src/queue-rooms/queue-room.scss +58 -0
  217. package/src/queue-screen/queue-screen.component.tsx +47 -0
  218. package/src/queue-screen/queue-screen.scss +39 -0
  219. package/src/queue-screen/queue-screen.test.tsx +47 -0
  220. package/src/queue-screen/useActiveTickets.tsx +13 -0
  221. package/src/queue-services/queue-service-form.component.tsx +183 -0
  222. package/src/queue-services/queue-service-form.test.tsx +56 -0
  223. package/src/queue-services/queue-service.resource.tsx +38 -0
  224. package/src/queue-services/queue-service.scss +54 -0
  225. package/src/remove-queue-entry-dialog/remove-queue-entry.component.tsx +95 -0
  226. package/src/remove-queue-entry-dialog/remove-queue-entry.resource.tsx +93 -0
  227. package/src/remove-queue-entry-dialog/remove-queue-entry.scss +8 -0
  228. package/src/remove-queue-entry-dialog/remove-queue-entry.test.tsx +44 -0
  229. package/src/root.component.tsx +28 -0
  230. package/src/root.scss +15 -0
  231. package/src/root.test.tsx +28 -0
  232. package/src/routes.json +91 -0
  233. package/src/side-menu/nav-group/createNavGroup.tsx +17 -0
  234. package/src/side-menu/nav-group/nav-group.component.tsx +24 -0
  235. package/src/side-menu/nav-group/nav-group.test.tsx +32 -0
  236. package/src/side-menu/nav-group/nav-group.ts +10 -0
  237. package/src/side-menu/side-menu.component.tsx +6 -0
  238. package/src/side-menu/side-menu.test.tsx +17 -0
  239. package/src/transition-queue-entry/transition-queue-entry-dialog.component.tsx +174 -0
  240. package/src/transition-queue-entry/transition-queue-entry-dialog.scss +12 -0
  241. package/src/transition-queue-entry/transition-queue-entry-dialog.test.tsx +95 -0
  242. package/src/transition-queue-entry/transition-queue-entry.resource.ts +16 -0
  243. package/src/types/index.ts +434 -0
  244. package/src/visits-missing-inqueue/visits-missing-inqueue.component.tsx +298 -0
  245. package/src/visits-missing-inqueue/visits-missing-inqueue.resource.ts +93 -0
  246. package/src/visits-missing-inqueue/visits-missing-inqueue.scss +108 -0
  247. package/translations/am.json +258 -0
  248. package/translations/en.json +258 -0
  249. package/translations/es.json +258 -0
  250. package/translations/fr.json +258 -0
  251. package/translations/he.json +262 -0
  252. package/translations/km.json +258 -0
  253. package/tsconfig.json +5 -0
  254. package/webpack.config.js +1 -0
@@ -0,0 +1,76 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import QueueLinelistFilter from './queue-linelist-filter.component';
4
+ import { mockVisitTypes } from '../../__mocks__/visits.mock';
5
+
6
+ jest.mock('@openmrs/esm-framework', () => ({
7
+ ...jest.requireActual('@openmrs/esm-framework'),
8
+ useLayoutType: jest.fn(() => 'tablet'),
9
+ useVisitTypes: jest.fn(() => mockVisitTypes),
10
+ toOpemrsIsoString: jest.fn(),
11
+ }));
12
+ // Additional mock functions if needed
13
+
14
+ describe('QueueLinelistFilter', () => {
15
+ it('renders the form with filter elements', () => {
16
+ render(<QueueLinelistFilter closePanel={jest.fn()} />);
17
+
18
+ expect(screen.getByText('Gender')).toBeInTheDocument();
19
+ expect(screen.getByLabelText('Age')).toBeInTheDocument();
20
+ expect(screen.getByLabelText('Between')).toBeInTheDocument();
21
+ expect(screen.getByLabelText('And')).toBeInTheDocument();
22
+ expect(screen.getByLabelText('Date')).toBeInTheDocument();
23
+ expect(screen.getByText("Use today's date")).toBeInTheDocument();
24
+ expect(screen.getByRole('button', { name: /Select visit type/i })).toBeInTheDocument();
25
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
26
+ expect(screen.getByText('Apply filters')).toBeInTheDocument();
27
+ });
28
+
29
+ it('calls closePanel function when cancel button is clicked', () => {
30
+ const closePanelMock = jest.fn();
31
+ render(<QueueLinelistFilter closePanel={closePanelMock} />);
32
+
33
+ const cancelButton = screen.getByText('Cancel');
34
+ fireEvent.click(cancelButton);
35
+
36
+ expect(closePanelMock).toHaveBeenCalledTimes(1);
37
+ });
38
+
39
+ it('updates gender state when a radio button is selected', () => {
40
+ render(<QueueLinelistFilter closePanel={jest.fn()} />);
41
+
42
+ const maleRadioButton = screen.getByLabelText('Male');
43
+ fireEvent.click(maleRadioButton);
44
+
45
+ expect(maleRadioButton).toBeChecked();
46
+ });
47
+
48
+ it('updates startAge state when a number is entered', () => {
49
+ render(<QueueLinelistFilter closePanel={jest.fn()} />);
50
+
51
+ const startAgeInput = screen.getByLabelText('Between');
52
+ fireEvent.change(startAgeInput, { target: { value: '10' } });
53
+
54
+ expect(startAgeInput).toHaveValue(10);
55
+ });
56
+
57
+ it('updates returnDate state when date input changes', () => {
58
+ render(<QueueLinelistFilter closePanel={jest.fn()} />);
59
+
60
+ const returnDateInput = screen.getByLabelText('Date');
61
+ fireEvent.change(returnDateInput, { target: { value: '2023-08-20' } });
62
+
63
+ expect(returnDateInput).toHaveValue('2023-08-20');
64
+ });
65
+
66
+ it('should open the visit type dropdown and close after selection', () => {
67
+ render(<QueueLinelistFilter closePanel={jest.fn()} />);
68
+
69
+ const visitTypeDropdown = screen.getByRole('button', { name: /Select visit type/i });
70
+ fireEvent.click(visitTypeDropdown);
71
+ const type1Option = screen.getByText('Outpatient Visit');
72
+ fireEvent.click(type1Option);
73
+
74
+ expect(visitTypeDropdown).toHaveTextContent('Open menu');
75
+ });
76
+ });
@@ -0,0 +1,29 @@
1
+ import React, { useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import Overlay from '../overlay.component';
4
+ import QueueLinelistFilter from './queue-linelist-filter.component';
5
+ import { FilterTypes } from '../types/index';
6
+
7
+ interface QueueLinelistProps {
8
+ closePanel: () => void;
9
+ }
10
+
11
+ const QueueLinelist: React.FC<QueueLinelistProps> = ({ closePanel }) => {
12
+ const { t } = useTranslation();
13
+ const [showFilter, setShowFilter] = useState<FilterTypes>(FilterTypes.SHOW);
14
+ const toggleFilter = (filterType: FilterTypes) => {
15
+ setShowFilter(filterType);
16
+ };
17
+
18
+ return (
19
+ <>
20
+ <Overlay header={t('filters', 'Filters')} closePanel={closePanel}>
21
+ <div className="omrs-main-content">
22
+ {showFilter === FilterTypes.SHOW ? <QueueLinelistFilter closePanel={closePanel} /> : null}
23
+ </div>
24
+ </Overlay>
25
+ </>
26
+ );
27
+ };
28
+
29
+ export default QueueLinelist;
@@ -0,0 +1,84 @@
1
+ import useSWR from 'swr';
2
+ import { openmrsFetch } from '@openmrs/esm-framework';
3
+ import { Appointment, AppointmentsFetchResponse } from '../types/index';
4
+ import { Provider } from '../types';
5
+ import { startOfDay } from '../constants';
6
+ import dayjs from 'dayjs';
7
+
8
+ export function useAppointments() {
9
+ const apiUrl = `/ws/rest/v1/appointment/all?forDate=${startOfDay}`;
10
+ const { data, error, isLoading, isValidating } = useSWR<{ data: Array<Appointment> }, Error>(apiUrl, openmrsFetch);
11
+
12
+ return {
13
+ appointmentQueueEntries: data ? data?.data : [],
14
+ isLoading,
15
+ isError: error,
16
+ isValidating,
17
+ };
18
+ }
19
+
20
+ export function useCheckedInAppointments() {
21
+ const apiUrl = `/ws/rest/v1/appointment/appointmentStatus?forDate=${startOfDay}&status=CheckedIn`;
22
+
23
+ const { data, error, isLoading, isValidating } = useSWR<{ data: Array<Appointment> }, Error>(apiUrl, openmrsFetch);
24
+
25
+ return {
26
+ checkedInAppointments: data ? data?.data : [],
27
+ isLoading,
28
+ isError: error,
29
+ isValidating,
30
+ };
31
+ }
32
+
33
+ export function useProviders() {
34
+ const customRepresentation = 'custom:(uuid,display,person:(age,display,gender,uuid))';
35
+ const apiUrl = `/ws/rest/v1/provider?q=&v=${customRepresentation}`;
36
+ const { data, error, isLoading, isValidating } = useSWR<{ data: { results: Array<Provider> } }, Error>(
37
+ apiUrl,
38
+ openmrsFetch,
39
+ );
40
+
41
+ return {
42
+ providers: data ? data.data?.results : [],
43
+ isLoading,
44
+ isError: error,
45
+ isValidating,
46
+ };
47
+ }
48
+
49
+ export function usePatientAppointments(patientUuid: string, startDate) {
50
+ const abortController = new AbortController();
51
+
52
+ const appointmentsSearchUrl = `/ws/rest/v1/appointments/search`;
53
+ const fetcher = () =>
54
+ openmrsFetch(appointmentsSearchUrl, {
55
+ method: 'POST',
56
+ signal: abortController.signal,
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ },
60
+ body: {
61
+ patientUuid: patientUuid,
62
+ startDate: startDate,
63
+ },
64
+ });
65
+
66
+ const { data, error, isLoading, isValidating } = useSWR<AppointmentsFetchResponse, Error>(
67
+ appointmentsSearchUrl,
68
+ fetcher,
69
+ );
70
+
71
+ const appointments = data?.data?.length ? data.data : [];
72
+
73
+ const upcomingAppointments = appointments
74
+ ?.sort((a, b) => (a.startDateTime > b.startDateTime ? 1 : -1))
75
+ ?.filter(({ status }) => status !== 'Cancelled')
76
+ ?.filter(({ startDateTime }) => dayjs(new Date(startDateTime).toISOString()).isAfter(new Date()));
77
+
78
+ return {
79
+ upcomingAppointment: upcomingAppointments ? upcomingAppointments?.[0] : null,
80
+ isError: error,
81
+ isLoading,
82
+ isValidating,
83
+ };
84
+ }
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import QueueLinelist from './queue-linelist.component';
4
+
5
+ describe('QueueLinelist', () => {
6
+ it('renders with filter content initially', () => {
7
+ render(<QueueLinelist closePanel={jest.fn()} />);
8
+
9
+ const filterContent = screen.getByText('Filters');
10
+ expect(filterContent).toBeInTheDocument();
11
+ });
12
+
13
+ it('toggles between filter content and null', () => {
14
+ render(<QueueLinelist closePanel={jest.fn()} />);
15
+
16
+ const closeButton = screen.getByText('Close overlay');
17
+ fireEvent.click(closeButton);
18
+
19
+ const filterContent = screen.queryByTestId('filter-content');
20
+ expect(filterContent).not.toBeInTheDocument();
21
+ });
22
+ });
@@ -0,0 +1,63 @@
1
+ import React, { useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import QueuePatientBaseTable from './queue-linelist-base-table.component';
4
+ import { useServiceQueueEntries } from '../active-visits/active-visits-table.resource';
5
+
6
+ const ServicesTable: React.FC = () => {
7
+ const { t } = useTranslation();
8
+
9
+ const currentPathName: string = window.location.pathname.replace('%20', ' ');
10
+ let service = currentPathName.split('/')[4];
11
+ let locationUuid = currentPathName.split('/')[6];
12
+ const { serviceQueueEntries, isLoading } = useServiceQueueEntries(service, locationUuid);
13
+
14
+ const tableHeaders = useMemo(
15
+ () => [
16
+ {
17
+ id: 0,
18
+ header: t('name', 'Name'),
19
+ key: 'name',
20
+ },
21
+ {
22
+ id: 1,
23
+ header: t('returnDate', 'Return Date'),
24
+ key: 'returnDate',
25
+ },
26
+ {
27
+ id: 2,
28
+ header: t('gender', 'Gender'),
29
+ key: 'gender',
30
+ },
31
+ {
32
+ id: 3,
33
+ header: t('age', 'Age'),
34
+ key: 'age',
35
+ },
36
+ {
37
+ id: 4,
38
+ header: t('visitType', 'Visit Type'),
39
+ key: 'visitType',
40
+ },
41
+ {
42
+ id: 5,
43
+ header: t('phoneNumber', 'Phone Number'),
44
+ key: 'phoneNumber',
45
+ },
46
+ ],
47
+ [t],
48
+ );
49
+
50
+ return (
51
+ <div>
52
+ <QueuePatientBaseTable
53
+ title={t('alistOfClients', 'A list of clients waiting for ')}
54
+ headers={tableHeaders}
55
+ patientData={serviceQueueEntries}
56
+ serviceType={service}
57
+ isLoading={isLoading}
58
+ />
59
+ </div>
60
+ );
61
+ };
62
+
63
+ export default ServicesTable;
@@ -0,0 +1,300 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import {
4
+ Button,
5
+ DataTable,
6
+ DataTableHeader,
7
+ Layer,
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableContainer,
12
+ TableHead,
13
+ TableHeader,
14
+ TableRow,
15
+ TableToolbar,
16
+ TableToolbarContent,
17
+ TableToolbarSearch,
18
+ Tag,
19
+ Tile,
20
+ DataTableSkeleton,
21
+ Dropdown,
22
+ Pagination,
23
+ } from '@carbon/react';
24
+ import { Filter, OverflowMenuVertical } from '@carbon/react/icons';
25
+ import {
26
+ ConfigObject,
27
+ ExtensionSlot,
28
+ formatDatetime,
29
+ useConfig,
30
+ usePagination,
31
+ ConfigurableLink,
32
+ formatDate,
33
+ } from '@openmrs/esm-framework';
34
+ import styles from './queue-linelist-base-table.scss';
35
+ import QueueLinelist from './queue-linelist.component';
36
+ import { updateSelectedAppointmentStatus, useSelectedAppointmentStatus } from '../helpers/helpers';
37
+ import { useAppointments } from './queue-linelist.resource';
38
+ import { getGender } from '../helpers/functions';
39
+
40
+ type FilterProps = {
41
+ rowIds: Array<string>;
42
+ headers: Array<DataTableHeader>;
43
+ cellsById: any;
44
+ inputValue: string;
45
+ getCellId: (row, key) => string;
46
+ };
47
+
48
+ const AppointmentsTable: React.FC = () => {
49
+ const { t } = useTranslation();
50
+ const [showOverlay, setShowOverlay] = useState(false);
51
+ const { appointmentStatuses } = useConfig() as ConfigObject;
52
+ const currentAppointmentStatus = useSelectedAppointmentStatus();
53
+ const { appointmentQueueEntries, isLoading } = useAppointments();
54
+ const [filteredRows, setFilteredRows] = useState(appointmentQueueEntries);
55
+ const { results, currentPage, goTo } = usePagination(filteredRows ?? [], 20);
56
+
57
+ const handleFilter = ({ rowIds, headers, cellsById, inputValue, getCellId }: FilterProps): Array<string> => {
58
+ return rowIds.filter((rowId) =>
59
+ headers.some(({ key }) => {
60
+ const cellId = getCellId(rowId, key);
61
+ const filterableValue = cellsById[cellId].value;
62
+ const filterTerm = inputValue.toLowerCase();
63
+
64
+ if (typeof filterableValue === 'boolean') {
65
+ return false;
66
+ }
67
+ if (filterableValue.hasOwnProperty('content')) {
68
+ if (Array.isArray(filterableValue.content.props.children)) {
69
+ return ('' + filterableValue.content.props.children[1].props.children).toLowerCase().includes(filterTerm);
70
+ }
71
+ if (typeof filterableValue.content.props.children === 'object') {
72
+ return ('' + filterableValue.content.props.children.props.children.props.children)
73
+ .toLowerCase()
74
+ .includes(filterTerm);
75
+ }
76
+ return ('' + filterableValue.content.props.children).toLowerCase().includes(filterTerm);
77
+ }
78
+ return ('' + filterableValue).toLowerCase().includes(filterTerm);
79
+ }),
80
+ );
81
+ };
82
+
83
+ useEffect(() => {
84
+ if (currentAppointmentStatus != t('all', 'All') && currentAppointmentStatus !== '') {
85
+ setFilteredRows(
86
+ appointmentQueueEntries?.filter((appointment) => appointment.status === currentAppointmentStatus),
87
+ );
88
+ } else {
89
+ setFilteredRows(appointmentQueueEntries);
90
+ }
91
+ }, [t, currentAppointmentStatus, results, appointmentQueueEntries]);
92
+
93
+ const tableHeaders = useMemo(
94
+ () => [
95
+ {
96
+ id: 0,
97
+ header: t('name', 'Name'),
98
+ key: 'name',
99
+ },
100
+ {
101
+ id: 1,
102
+ header: t('returnDate', 'Return Date'),
103
+ key: 'returnDate',
104
+ },
105
+ {
106
+ id: 2,
107
+ header: t('gender', 'Gender'),
108
+ key: 'gender',
109
+ },
110
+ {
111
+ id: 3,
112
+ header: t('age', 'Age'),
113
+ key: 'age',
114
+ },
115
+ {
116
+ id: 4,
117
+ header: t('visitType', 'Visit Type'),
118
+ key: 'visitType',
119
+ },
120
+ {
121
+ id: 5,
122
+ header: t('status', 'Status'),
123
+ key: 'status',
124
+ },
125
+ {
126
+ id: 6,
127
+ header: t('phoneNumber', 'Phone Number'),
128
+ key: 'phoneNumber',
129
+ },
130
+ ],
131
+ [t],
132
+ );
133
+
134
+ const tableRows = useMemo(() => {
135
+ return results?.map((appointment) => ({
136
+ id: appointment.uuid,
137
+ name: {
138
+ content: (
139
+ <ConfigurableLink to={`\${openmrsSpaBase}/patient/${appointment.patient.uuid}/chart`}>
140
+ {appointment.patient.name}
141
+ </ConfigurableLink>
142
+ ),
143
+ },
144
+ returnDate: formatDate(new Date(appointment.startDateTime), {
145
+ mode: 'wide',
146
+ }),
147
+ gender: getGender(appointment.patient?.gender, t),
148
+ age: appointment.patient.age,
149
+ visitType: appointment.appointmentKind,
150
+ status: appointment.status,
151
+ phoneNumber: appointment.patient?.phoneNumber,
152
+ }));
153
+ }, [results, t]);
154
+
155
+ const handleStatusChange = ({ selectedItem }) => {
156
+ updateSelectedAppointmentStatus(selectedItem);
157
+ };
158
+
159
+ const pageSizes = useMemo(() => {
160
+ const numberOfPages = Math.ceil(appointmentQueueEntries?.length / 100);
161
+ return [...Array(numberOfPages).keys()].map((x) => {
162
+ return (x + 1) * 100;
163
+ });
164
+ }, [appointmentQueueEntries]);
165
+
166
+ if (isLoading) {
167
+ return <DataTableSkeleton role="progressbar" />;
168
+ }
169
+
170
+ return (
171
+ <div className={styles.container}>
172
+ <ExtensionSlot name="breadcrumbs-slot" />
173
+
174
+ <div className={styles.headerContainer}>
175
+ <div>
176
+ <p className={styles.title}>{t('scheduledAppointmentsList', 'Scheduled appointments patient list')}</p>
177
+ <p className={styles.subTitle}>
178
+ {appointmentQueueEntries?.length} · Last Updated: {formatDatetime(new Date(), { mode: 'standard' })}
179
+ </p>
180
+ </div>
181
+
182
+ <Button kind="ghost" size="sm" renderIcon={(props) => <OverflowMenuVertical size={16} {...props} />}>
183
+ {t('actions', 'Actions')}
184
+ </Button>
185
+ </div>
186
+
187
+ <Layer>
188
+ <Tile className={styles.filterTile}>
189
+ <Tag size="md" title="Clear Filter" type="blue">
190
+ {t('today', 'Today')}
191
+ </Tag>
192
+
193
+ <div className={styles.actionsBtn}>
194
+ <Button
195
+ kind="ghost"
196
+ renderIcon={(props) => <Filter size={16} {...props} />}
197
+ iconDescription={t('filter', 'Filter (1)')}
198
+ onClick={() => setShowOverlay(true)}
199
+ size="sm">
200
+ {t('filter', 'Filter (1)')}
201
+ </Button>
202
+ </div>
203
+ </Tile>
204
+ </Layer>
205
+
206
+ <DataTable
207
+ data-floating-menu-container
208
+ filterRows={handleFilter}
209
+ headers={tableHeaders}
210
+ overflowMenuOnHover={false}
211
+ rows={tableRows}
212
+ size="md"
213
+ useZebraStyles>
214
+ {({ rows, headers, getHeaderProps, getTableProps, getRowProps, onInputChange }) => (
215
+ <TableContainer className={styles.tableContainer}>
216
+ <TableToolbar
217
+ style={{
218
+ position: 'static',
219
+ height: '3rem',
220
+ overflow: 'visible',
221
+ backgroundColor: 'color',
222
+ }}>
223
+ <TableToolbarContent className={styles.toolbarContent}>
224
+ <div className={styles.filterContainer}>
225
+ <Dropdown
226
+ id="serviceFilter"
227
+ initialSelectedItem={'All'}
228
+ label={currentAppointmentStatus}
229
+ titleText={t('status', 'Status') + ':'}
230
+ type="inline"
231
+ items={['All', ...appointmentStatuses]}
232
+ onChange={handleStatusChange}
233
+ size="sm"
234
+ />
235
+ </div>
236
+
237
+ <TableToolbarSearch
238
+ className={styles.search}
239
+ expanded
240
+ onChange={onInputChange}
241
+ placeholder={t('searchThisList', 'Search this list')}
242
+ size="sm"
243
+ />
244
+ </TableToolbarContent>
245
+ </TableToolbar>
246
+ <Table {...getTableProps()} className={styles.queueTable}>
247
+ <TableHead>
248
+ <TableRow>
249
+ {headers.map((header) => (
250
+ <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
251
+ ))}
252
+ </TableRow>
253
+ </TableHead>
254
+ <TableBody>
255
+ {rows.map((row, index) => {
256
+ return (
257
+ <React.Fragment key={row.id}>
258
+ <TableRow {...getRowProps({ row })}>
259
+ {row.cells.map((cell) => (
260
+ <TableCell key={cell.id}>{cell.value?.content ?? cell.value}</TableCell>
261
+ ))}
262
+ </TableRow>
263
+ </React.Fragment>
264
+ );
265
+ })}
266
+ </TableBody>
267
+ </Table>
268
+ {rows.length === 0 ? (
269
+ <div className={styles.tileContainer}>
270
+ <Layer>
271
+ <Tile className={styles.tile}>
272
+ <div className={styles.tileContent}>
273
+ <p className={styles.content}>{t('noPatientsToDisplay', 'No patients to display')}</p>
274
+ <p className={styles.helper}>{t('checkFilters', 'Check the filters above')}</p>
275
+ </div>
276
+ </Tile>
277
+ </Layer>
278
+ </div>
279
+ ) : null}
280
+ </TableContainer>
281
+ )}
282
+ </DataTable>
283
+
284
+ <Pagination
285
+ backwardText="Previous page"
286
+ forwardText="Next page"
287
+ page={currentPage}
288
+ pageNumberText="Page Number"
289
+ pageSize={100}
290
+ onChange={({ page }) => goTo(page)}
291
+ pageSizes={pageSizes?.length > 0 ? pageSizes : [100]}
292
+ totalItems={appointmentQueueEntries?.length ?? 0}
293
+ />
294
+
295
+ {showOverlay && <QueueLinelist closePanel={() => setShowOverlay(false)} />}
296
+ </div>
297
+ );
298
+ };
299
+
300
+ export default AppointmentsTable;
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import AppointmentsTable from './scheduled-appointments-table.component';
4
+ import { mockAppointmentsData } from '../../__mocks__/appointments-data.mock';
5
+
6
+ jest.mock('@openmrs/esm-framework', () => ({
7
+ ...jest.requireActual('@openmrs/esm-framework'),
8
+ useConfig: () => ({
9
+ appointmentStatuses: ['All', 'Scheduled', 'Completed'],
10
+ }),
11
+ usePagination: () => ({
12
+ goTo: jest.fn(),
13
+ results: mockAppointmentsData.data,
14
+ currentPage: 1,
15
+ }),
16
+ }));
17
+
18
+ jest.mock('./queue-linelist.resource', () => ({
19
+ useAppointments: () => ({
20
+ appointmentQueueEntries: mockAppointmentsData.data,
21
+ isLoading: false,
22
+ }),
23
+ }));
24
+
25
+ describe('AppointmentsTable', () => {
26
+ it('renders appointments when loading is complete', () => {
27
+ render(<AppointmentsTable />);
28
+
29
+ const appointmentName = screen.getByText('Hungai Kevin');
30
+ expect(appointmentName).toBeInTheDocument();
31
+ });
32
+
33
+ it('filters appointments based on status selection', () => {
34
+ render(<AppointmentsTable />);
35
+
36
+ const statusDropdown = screen.getAllByLabelText('Status:');
37
+ fireEvent.change(statusDropdown[0], { target: { value: 'Completed' } });
38
+
39
+ const filteredAppointmentName = screen.getByText('Hungai Kevin');
40
+ expect(filteredAppointmentName).toBeInTheDocument();
41
+ });
42
+ });