@kenyaemr/esm-service-queues-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 (289) hide show
  1. package/.turbo/turbo-build.log +40 -0
  2. package/README.md +20 -0
  3. package/dist/130.js +2 -0
  4. package/dist/130.js.LICENSE.txt +3 -0
  5. package/dist/130.js.map +1 -0
  6. package/dist/152.js +1 -0
  7. package/dist/152.js.map +1 -0
  8. package/dist/169.js +1 -0
  9. package/dist/169.js.map +1 -0
  10. package/dist/185.js +1 -0
  11. package/dist/185.js.map +1 -0
  12. package/dist/233.js +1 -0
  13. package/dist/233.js.map +1 -0
  14. package/dist/237.js +1 -0
  15. package/dist/237.js.map +1 -0
  16. package/dist/255.js +2 -0
  17. package/dist/255.js.LICENSE.txt +9 -0
  18. package/dist/255.js.map +1 -0
  19. package/dist/271.js +1 -0
  20. package/dist/276.js +1 -0
  21. package/dist/276.js.map +1 -0
  22. package/dist/303.js +1 -0
  23. package/dist/303.js.map +1 -0
  24. package/dist/319.js +1 -0
  25. package/dist/401.js +1 -0
  26. package/dist/401.js.map +1 -0
  27. package/dist/430.js +2 -0
  28. package/dist/430.js.LICENSE.txt +50 -0
  29. package/dist/430.js.map +1 -0
  30. package/dist/460.js +1 -0
  31. package/dist/501.js +1 -0
  32. package/dist/501.js.map +1 -0
  33. package/dist/574.js +1 -0
  34. package/dist/591.js +2 -0
  35. package/dist/591.js.LICENSE.txt +32 -0
  36. package/dist/591.js.map +1 -0
  37. package/dist/644.js +1 -0
  38. package/dist/647.js +1 -0
  39. package/dist/647.js.map +1 -0
  40. package/dist/650.js +1 -0
  41. package/dist/650.js.map +1 -0
  42. package/dist/669.js +1 -0
  43. package/dist/669.js.map +1 -0
  44. package/dist/696.js +1 -0
  45. package/dist/696.js.map +1 -0
  46. package/dist/703.js +1 -0
  47. package/dist/703.js.map +1 -0
  48. package/dist/729.js +1 -0
  49. package/dist/729.js.map +1 -0
  50. package/dist/738.js +1 -0
  51. package/dist/738.js.map +1 -0
  52. package/dist/757.js +1 -0
  53. package/dist/764.js +1 -0
  54. package/dist/764.js.map +1 -0
  55. package/dist/784.js +2 -0
  56. package/dist/784.js.LICENSE.txt +9 -0
  57. package/dist/784.js.map +1 -0
  58. package/dist/788.js +1 -0
  59. package/dist/806.js +1 -0
  60. package/dist/806.js.map +1 -0
  61. package/dist/807.js +1 -0
  62. package/dist/833.js +1 -0
  63. package/dist/877.js +1 -0
  64. package/dist/877.js.map +1 -0
  65. package/dist/917.js +1 -0
  66. package/dist/917.js.map +1 -0
  67. package/dist/940.js +1 -0
  68. package/dist/940.js.map +1 -0
  69. package/dist/981.js +1 -0
  70. package/dist/981.js.map +1 -0
  71. package/dist/kenyaemr-esm-service-queues-app.js +1 -0
  72. package/dist/kenyaemr-esm-service-queues-app.js.buildmanifest.json +965 -0
  73. package/dist/kenyaemr-esm-service-queues-app.js.map +1 -0
  74. package/dist/main.js +2 -0
  75. package/dist/main.js.LICENSE.txt +60 -0
  76. package/dist/main.js.map +1 -0
  77. package/dist/routes.json +1 -0
  78. package/jest.config.js +3 -0
  79. package/package.json +54 -0
  80. package/src/active-visits/active-visits-row-actions.component.tsx +25 -0
  81. package/src/active-visits/active-visits-row-actions.scss +4 -0
  82. package/src/active-visits/active-visits-table.resource.ts +273 -0
  83. package/src/active-visits/change-status-dialog.component.tsx +272 -0
  84. package/src/active-visits/change-status-dialog.scss +47 -0
  85. package/src/active-visits/change-status-dialog.test.tsx +154 -0
  86. package/src/add-patient-toqueue/add-patient-toqueue-dialog.component.tsx +228 -0
  87. package/src/add-patient-toqueue/add-patient-toqueue-dialog.scss +32 -0
  88. package/src/add-provider-queue-room/add-provider-queue-room.component.tsx +238 -0
  89. package/src/add-provider-queue-room/add-provider-queue-room.resource.ts +76 -0
  90. package/src/add-provider-queue-room/add-provider-queue-room.scss +17 -0
  91. package/src/add-provider-queue-room/add-provider-queue-room.test.tsx +105 -0
  92. package/src/clear-queue-entries-dialog/clear-queue-entries-dialog.component.tsx +76 -0
  93. package/src/clear-queue-entries-dialog/clear-queue-entries-dialog.resource.ts +7 -0
  94. package/src/clear-queue-entries-dialog/clear-queue-entries-dialog.scss +8 -0
  95. package/src/clear-queue-entries-dialog/clear-queue-entries-dialog.test.tsx +43 -0
  96. package/src/clear-queue-entries-dialog/clear-queue-entries.component.tsx +43 -0
  97. package/src/config-schema.ts +465 -0
  98. package/src/constants.ts +10 -0
  99. package/src/createDashboardLink.component.tsx +39 -0
  100. package/src/current-visit/current-visit-summary.component.tsx +38 -0
  101. package/src/current-visit/current-visit-summary.test.tsx +43 -0
  102. package/src/current-visit/current-visit.resource.ts +84 -0
  103. package/src/current-visit/current-visit.scss +34 -0
  104. package/src/current-visit/hooks/useVitalsConceptMetadata.tsx +101 -0
  105. package/src/current-visit/visit-details/biometrics-config-schema.ts +14 -0
  106. package/src/current-visit/visit-details/current-visit-details.component.tsx +96 -0
  107. package/src/current-visit/visit-details/triage-note.component.tsx +53 -0
  108. package/src/current-visit/visit-details/triage-note.scss +89 -0
  109. package/src/current-visit/visit-details/vitals-config-schema.ts +17 -0
  110. package/src/current-visit/visit-details/vitals.component.tsx +165 -0
  111. package/src/dashboard.meta.ts +5 -0
  112. package/src/declarations.d.ts +4 -0
  113. package/src/helpers/functions.ts +28 -0
  114. package/src/helpers/helpers.ts +177 -0
  115. package/src/helpers/time-helpers.ts +15 -0
  116. package/src/home.component.tsx +16 -0
  117. package/src/home.test.tsx +48 -0
  118. package/src/hooks/useConcept.ts +13 -0
  119. package/src/hooks/useQueue.ts +10 -0
  120. package/src/hooks/useQueueEntries.ts +187 -0
  121. package/src/hooks/useQueues.ts +22 -0
  122. package/src/hooks/useSystemSetting.ts +18 -0
  123. package/src/index.ts +172 -0
  124. package/src/past-visit/past-visit-details/encounter-list.component.tsx +54 -0
  125. package/src/past-visit/past-visit-details/medications-list.component.tsx +98 -0
  126. package/src/past-visit/past-visit-details/notes-list.component.tsx +41 -0
  127. package/src/past-visit/past-visit-details/past-visit-summary.component.tsx +181 -0
  128. package/src/past-visit/past-visit-details/past-visit-summary.scss +58 -0
  129. package/src/past-visit/past-visit.component.tsx +37 -0
  130. package/src/past-visit/past-visit.resource.ts +83 -0
  131. package/src/past-visit/past-visit.scss +126 -0
  132. package/src/past-visit/past-visit.test.tsx +41 -0
  133. package/src/patient-info/appointment-details.component.tsx +98 -0
  134. package/src/patient-info/appointment-details.scss +34 -0
  135. package/src/patient-info/appointment-details.test.tsx +36 -0
  136. package/src/patient-info/appointments.resource.ts +43 -0
  137. package/src/patient-info/hooks/usePatientAttributes.tsx +42 -0
  138. package/src/patient-info/patient-info.component.tsx +82 -0
  139. package/src/patient-info/patient-info.scss +60 -0
  140. package/src/patient-info/patient-info.test.tsx +43 -0
  141. package/src/patient-queue-header/patient-queue-header.component.tsx +99 -0
  142. package/src/patient-queue-header/patient-queue-header.scss +90 -0
  143. package/src/patient-queue-header/patient-queue-illustration.component.tsx +22 -0
  144. package/src/patient-queue-metrics/clinic-metrics.component.tsx +98 -0
  145. package/src/patient-queue-metrics/clinic-metrics.resource.ts +58 -0
  146. package/src/patient-queue-metrics/clinic-metrics.scss +11 -0
  147. package/src/patient-queue-metrics/clinic-metrics.test.tsx +76 -0
  148. package/src/patient-queue-metrics/metrics-card.component.tsx +68 -0
  149. package/src/patient-queue-metrics/metrics-card.scss +80 -0
  150. package/src/patient-queue-metrics/metrics-header.component.tsx +61 -0
  151. package/src/patient-queue-metrics/metrics-header.scss +26 -0
  152. package/src/patient-queue-metrics/queue-metrics.resource.ts +42 -0
  153. package/src/patient-search/advanced-search.component.tsx +191 -0
  154. package/src/patient-search/advanced-search.scss +154 -0
  155. package/src/patient-search/advanced-search.test.tsx +29 -0
  156. package/src/patient-search/basic-search.component.tsx +112 -0
  157. package/src/patient-search/basic-search.scss +139 -0
  158. package/src/patient-search/basic-search.test.tsx +23 -0
  159. package/src/patient-search/empty-data-illustration.component.tsx +41 -0
  160. package/src/patient-search/hooks/useActivePatientEnrollment.tsx +29 -0
  161. package/src/patient-search/hooks/useDefaultLocation.ts +14 -0
  162. package/src/patient-search/hooks/usePatients.tsx +25 -0
  163. package/src/patient-search/hooks/useQueueLocations.tsx +23 -0
  164. package/src/patient-search/hooks/useRecommendedVisitTypes.tsx +35 -0
  165. package/src/patient-search/hooks/useScheduledVisits.ts +52 -0
  166. package/src/patient-search/patient-scheduled-visits.component.tsx +324 -0
  167. package/src/patient-search/patient-scheduled-visits.scss +131 -0
  168. package/src/patient-search/patient-scheduled-visits.test.tsx +44 -0
  169. package/src/patient-search/patient-search.scss +43 -0
  170. package/src/patient-search/patient-search.workspace.tsx +135 -0
  171. package/src/patient-search/search-illustration.component.tsx +27 -0
  172. package/src/patient-search/search-results.component.tsx +75 -0
  173. package/src/patient-search/search-results.scss +80 -0
  174. package/src/patient-search/search-results.test.tsx +77 -0
  175. package/src/patient-search/search.resource.ts +10 -0
  176. package/src/patient-search/visit-form/existing-visit-form.component.tsx +112 -0
  177. package/src/patient-search/visit-form/queue.resource.ts +64 -0
  178. package/src/patient-search/visit-form/visit-form.component.tsx +344 -0
  179. package/src/patient-search/visit-form/visit-form.scss +73 -0
  180. package/src/patient-search/visit-form/visit-type-selector.component.tsx +155 -0
  181. package/src/patient-search/visit-form/visit-type-selector.scss +100 -0
  182. package/src/patient-search/visit-form/visit-type-selector.test.tsx +83 -0
  183. package/src/patient-search/visit-form-queue-fields/visit-form-queue-fields.component.tsx +178 -0
  184. package/src/patient-search/visit-form-queue-fields/visit-form-queue-fields.scss +19 -0
  185. package/src/patient-search/visit-form-queue-fields/visit-form-queue-fields.test.tsx +63 -0
  186. package/src/queue-entry-table-components/edit-entry.scss +14 -0
  187. package/src/queue-entry-table-components/queue-duration.component.tsx +41 -0
  188. package/src/queue-entry-table-components/queue-priority.component.tsx +38 -0
  189. package/src/queue-entry-table-components/queue-priority.scss +12 -0
  190. package/src/queue-entry-table-components/queue-status.component.tsx +39 -0
  191. package/src/queue-entry-table-components/transition-entry.component.tsx +55 -0
  192. package/src/queue-entry-table-components/transition-entry.scss +22 -0
  193. package/src/queue-patient-linelists/queue-linelist-base-table.component.tsx +241 -0
  194. package/src/queue-patient-linelists/queue-linelist-base-table.scss +110 -0
  195. package/src/queue-patient-linelists/queue-linelist-base-table.test.tsx +93 -0
  196. package/src/queue-patient-linelists/queue-linelist-filter.scss +63 -0
  197. package/src/queue-patient-linelists/queue-linelist-filter.test.tsx +94 -0
  198. package/src/queue-patient-linelists/queue-linelist-filter.workspace.tsx +185 -0
  199. package/src/queue-patient-linelists/queue-linelist.resource.ts +84 -0
  200. package/src/queue-patient-linelists/queue-services-table.component.tsx +63 -0
  201. package/src/queue-patient-linelists/scheduled-appointments-table.component.tsx +305 -0
  202. package/src/queue-patient-linelists/scheduled-appointments-table.test.tsx +41 -0
  203. package/src/queue-rooms/queue-room-form.scss +56 -0
  204. package/src/queue-rooms/queue-room-form.test.tsx +80 -0
  205. package/src/queue-rooms/queue-room-form.workspace.tsx +169 -0
  206. package/src/queue-rooms/queue-room.resource.ts +20 -0
  207. package/src/queue-screen/queue-screen.component.tsx +47 -0
  208. package/src/queue-screen/queue-screen.scss +39 -0
  209. package/src/queue-screen/queue-screen.test.tsx +51 -0
  210. package/src/queue-screen/useActiveTickets.tsx +13 -0
  211. package/src/queue-services/queue-service-form.scss +61 -0
  212. package/src/queue-services/queue-service-form.test.tsx +60 -0
  213. package/src/queue-services/queue-service-form.workspace.tsx +179 -0
  214. package/src/queue-services/queue-service.resource.ts +33 -0
  215. package/src/queue-table/cells/columns.resource.ts +135 -0
  216. package/src/queue-table/cells/queue-table-action-cell.component.tsx +88 -0
  217. package/src/queue-table/cells/queue-table-action-cell.scss +7 -0
  218. package/src/queue-table/cells/queue-table-coming-from-cell.component.tsx +13 -0
  219. package/src/queue-table/cells/queue-table-extension-cell.component.tsx +16 -0
  220. package/src/queue-table/cells/queue-table-name-cell.component.tsx +20 -0
  221. package/src/queue-table/cells/queue-table-patient-age-cell.component.tsx +18 -0
  222. package/src/queue-table/cells/queue-table-patient-identifier-cell.component.tsx +25 -0
  223. package/src/queue-table/cells/queue-table-priority-cell.component.tsx +23 -0
  224. package/src/queue-table/cells/queue-table-queue-name-cell.component.tsx +14 -0
  225. package/src/queue-table/cells/queue-table-status-cell.component.tsx +18 -0
  226. package/src/queue-table/cells/queue-table-visit-attribute-queue-number-cell.component.tsx +37 -0
  227. package/src/queue-table/cells/queue-table-visit-start-time-cell.component.tsx +20 -0
  228. package/src/queue-table/cells/queue-table-wait-time-cell.component.tsx +17 -0
  229. package/src/queue-table/default-queue-table.component.tsx +174 -0
  230. package/src/queue-table/default-queue-table.test.tsx +131 -0
  231. package/src/queue-table/queue-entry-actions/edit-queue-entry-modal.component.tsx +52 -0
  232. package/src/queue-table/queue-entry-actions/end-queue-entry-modal.component.tsx +39 -0
  233. package/src/queue-table/queue-entry-actions/queue-entry-actions-modal.component.tsx +362 -0
  234. package/src/queue-table/queue-entry-actions/queue-entry-actions-modal.test.tsx +152 -0
  235. package/src/queue-table/queue-entry-actions/queue-entry-actions.resource.ts +83 -0
  236. package/src/queue-table/queue-entry-actions/queue-entry-actons-modal.scss +36 -0
  237. package/src/queue-table/queue-entry-actions/queue-entry-confirm-action-modal.component.tsx +97 -0
  238. package/src/queue-table/queue-entry-actions/queue-entry-confirm-action-modal.test.tsx +106 -0
  239. package/src/queue-table/queue-entry-actions/queue-entry-undo-actions-modal.test.tsx +76 -0
  240. package/src/queue-table/queue-entry-actions/transition-queue-entry-modal.component.tsx +51 -0
  241. package/src/queue-table/queue-entry-actions/undo-transition-queue-entry-modal.component.tsx +58 -0
  242. package/src/queue-table/queue-entry-actions/void-queue-entry-modal.component.tsx +34 -0
  243. package/src/queue-table/queue-table-by-status-menu.component.tsx +42 -0
  244. package/src/queue-table/queue-table-by-status-menu.scss +11 -0
  245. package/src/queue-table/queue-table-by-status-skeleton.component.tsx +32 -0
  246. package/src/queue-table/queue-table-by-status.component.tsx +96 -0
  247. package/src/queue-table/queue-table-expanded-row.component.tsx +29 -0
  248. package/src/queue-table/queue-table-metrics-card.component.tsx +50 -0
  249. package/src/queue-table/queue-table-metrics-card.scss +48 -0
  250. package/src/queue-table/queue-table-metrics.component.tsx +30 -0
  251. package/src/queue-table/queue-table-metrics.scss +11 -0
  252. package/src/queue-table/queue-table.component.tsx +179 -0
  253. package/src/queue-table/queue-table.scss +192 -0
  254. package/src/queue-table/queue-table.test.tsx +210 -0
  255. package/src/remove-queue-entry-dialog/remove-queue-entry.component.tsx +87 -0
  256. package/src/remove-queue-entry-dialog/remove-queue-entry.resource.ts +93 -0
  257. package/src/remove-queue-entry-dialog/remove-queue-entry.scss +7 -0
  258. package/src/remove-queue-entry-dialog/remove-queue-entry.test.tsx +45 -0
  259. package/src/root.component.tsx +28 -0
  260. package/src/root.scss +15 -0
  261. package/src/root.test.tsx +24 -0
  262. package/src/routes.json +133 -0
  263. package/src/side-menu/nav-group/createNavGroup.tsx +17 -0
  264. package/src/side-menu/nav-group/nav-group.component.tsx +24 -0
  265. package/src/side-menu/nav-group/nav-group.test.tsx +32 -0
  266. package/src/side-menu/nav-group/nav-group.ts +10 -0
  267. package/src/side-menu/side-menu.component.tsx +6 -0
  268. package/src/side-menu/side-menu.test.tsx +17 -0
  269. package/src/transition-queue-entry/transition-queue-entry-dialog.component.tsx +134 -0
  270. package/src/transition-queue-entry/transition-queue-entry-dialog.scss +12 -0
  271. package/src/transition-queue-entry/transition-queue-entry-dialog.test.tsx +102 -0
  272. package/src/transition-queue-entry/transition-queue-entry.resource.ts +16 -0
  273. package/src/types/index.ts +494 -0
  274. package/src/views/queue-table-by-status-view.component.tsx +25 -0
  275. package/src/views/queue-tables-for-all-statuses.component.tsx +150 -0
  276. package/src/visits-missing-inqueue/visits-missing-inqueue.component.tsx +277 -0
  277. package/src/visits-missing-inqueue/visits-missing-inqueue.resource.ts +93 -0
  278. package/src/visits-missing-inqueue/visits-missing-inqueue.scss +108 -0
  279. package/translations/am.json +295 -0
  280. package/translations/ar.json +295 -0
  281. package/translations/en.json +305 -0
  282. package/translations/es.json +295 -0
  283. package/translations/fr.json +295 -0
  284. package/translations/he.json +295 -0
  285. package/translations/km.json +295 -0
  286. package/translations/zh.json +295 -0
  287. package/translations/zh_CN.json +295 -0
  288. package/tsconfig.json +5 -0
  289. package/webpack.config.js +1 -0
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { screen } from '@testing-library/react';
3
+ import PastVisitSummary from './past-visit-details/past-visit-summary.component';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { mockPastVisit } from '__mocks__';
6
+ import { mockPatient, renderWithSwr } from 'tools';
7
+ import { usePastVisits } from './past-visit.resource';
8
+
9
+ const mockUsePastVisits = usePastVisits as jest.Mock;
10
+
11
+ jest.mock('./past-visit.resource', () => ({
12
+ usePastVisits: jest.fn(),
13
+ }));
14
+
15
+ describe('PastVisit: ', () => {
16
+ it('renders an empty state when notes, encounters, medications, and vitals data is not available', async () => {
17
+ const user = userEvent.setup();
18
+
19
+ mockUsePastVisits.mockReturnValueOnce({
20
+ data: mockPastVisit.data.results,
21
+ });
22
+
23
+ renderPastVisitTabs();
24
+
25
+ expect(screen.queryAllByText(/vitals/i));
26
+ const vitalsTab = screen.getByRole('tab', { name: /vitals/i });
27
+
28
+ expect(vitalsTab).toBeInTheDocument();
29
+
30
+ expect(screen.getByRole('tab', { name: /notes/i })).toBeInTheDocument();
31
+ expect(screen.getByRole('tab', { name: /medications/i })).toBeInTheDocument();
32
+
33
+ await user.click(vitalsTab);
34
+
35
+ expect(vitalsTab).toHaveAttribute('aria-selected', 'true');
36
+ });
37
+ });
38
+
39
+ function renderPastVisitTabs() {
40
+ renderWithSwr(<PastVisitSummary patientUuid={mockPatient.id} encounters={[]} />);
41
+ }
@@ -0,0 +1,98 @@
1
+ import React from 'react';
2
+ import dayjs from 'dayjs';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { InlineLoading } from '@carbon/react';
5
+ import { formatDatetime, parseDate, type Visit } from '@openmrs/esm-framework';
6
+ import { useAppointments } from './appointments.resource';
7
+ import styles from './appointment-details.scss';
8
+ import { usePastVisits } from '../past-visit/past-visit.resource';
9
+ import { type Appointment } from '../types';
10
+
11
+ interface AppointmentDetailsProps {
12
+ patientUuid: string;
13
+ }
14
+
15
+ interface PastAppointmentDetailsProps {
16
+ pastVisit: Visit;
17
+ isLoading: boolean;
18
+ }
19
+
20
+ interface UpcomingAppointmentDetailsProps {
21
+ upcomingAppointment: Appointment;
22
+ isLoading: boolean;
23
+ }
24
+
25
+ const PastAppointmentDetails: React.FC<PastAppointmentDetailsProps> = ({ pastVisit, isLoading }) => {
26
+ const { t } = useTranslation();
27
+
28
+ if (isLoading) {
29
+ return (
30
+ <div className={styles.widgetCard}>
31
+ <InlineLoading description={t('loading', 'Loading...')} role="progressbar" />
32
+ </div>
33
+ );
34
+ }
35
+
36
+ if (pastVisit) {
37
+ return (
38
+ <div className={styles.widgetCard}>
39
+ <React.Fragment>
40
+ <p className={styles.title}>{t('lastEncounter', 'Last encounter')}</p>
41
+ <p className={styles.subtitle}>
42
+ {formatDatetime(parseDate(pastVisit?.startDatetime))} ·{' '}
43
+ {pastVisit?.visitType ? pastVisit?.visitType?.display : '--'} ·{' '}
44
+ {pastVisit?.location ? pastVisit.location?.display : '--'}
45
+ </p>
46
+ </React.Fragment>
47
+ </div>
48
+ );
49
+ }
50
+ return (
51
+ <p className={styles.content}>{t('noLastEncounter', 'There is no last encounter to display for this patient')}</p>
52
+ );
53
+ };
54
+
55
+ const UpcomingAppointmentDetails: React.FC<UpcomingAppointmentDetailsProps> = ({ upcomingAppointment, isLoading }) => {
56
+ const { t } = useTranslation();
57
+
58
+ if (isLoading) {
59
+ return (
60
+ <div className={styles.widgetCard}>
61
+ <InlineLoading description={t('loading', 'Loading...')} role="progressbar" />
62
+ </div>
63
+ );
64
+ }
65
+
66
+ if (upcomingAppointment) {
67
+ return (
68
+ <div className={styles.widgetCard}>
69
+ <React.Fragment>
70
+ <p className={styles.title}>{t('returnDate', 'Return Date')}</p>
71
+ <p className={styles.subtitle}>
72
+ {formatDatetime(parseDate(upcomingAppointment?.startDateTime))} ·{' '}
73
+ {upcomingAppointment?.service ? upcomingAppointment?.service?.name : '--'} ·{' '}
74
+ {upcomingAppointment?.location ? upcomingAppointment?.location?.name : '--'}{' '}
75
+ </p>
76
+ </React.Fragment>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ return <p className={styles.content}>{t('noReturnDate', 'There is no return date to display for this patient')}</p>;
82
+ };
83
+
84
+ const AppointmentDetails: React.FC<AppointmentDetailsProps> = ({ patientUuid }) => {
85
+ const { t } = useTranslation();
86
+ const startDate = dayjs(new Date().toISOString()).subtract(6, 'month').toISOString();
87
+ const { upcomingAppointment, isLoading } = useAppointments(patientUuid, startDate);
88
+ const { visits, isLoading: loading } = usePastVisits(patientUuid);
89
+
90
+ return (
91
+ <>
92
+ <PastAppointmentDetails pastVisit={visits} isLoading={loading} />
93
+ <UpcomingAppointmentDetails upcomingAppointment={upcomingAppointment} isLoading={isLoading} />
94
+ </>
95
+ );
96
+ };
97
+
98
+ export default AppointmentDetails;
@@ -0,0 +1,34 @@
1
+ @use '@carbon/styles/scss/colors';
2
+ @use '@carbon/styles/scss/type';
3
+ @use '@carbon/styles/scss/spacing';
4
+ @import '~@openmrs/esm-styleguide/src/vars';
5
+
6
+ .widgetCard {
7
+ border: 1px solid colors.$blue-20;
8
+ border-left: none;
9
+ border-right: none;
10
+ background-color: colors.$blue-10;
11
+ padding: spacing.$spacing-05;
12
+ }
13
+
14
+ .title {
15
+ @include type.type-style('heading-compact-01');
16
+ color: $text-02;
17
+ margin-bottom: spacing.$spacing-02;
18
+ }
19
+
20
+ .subtitle {
21
+ color: $text-02;
22
+ @include type.type-style('body-compact-01');
23
+ }
24
+
25
+ .content {
26
+ @include type.type-style('heading-compact-01');
27
+ background-color: colors.$blue-10;
28
+ padding: spacing.$spacing-05;
29
+ border: 1px solid colors.$blue-20;
30
+ }
31
+
32
+ .tile {
33
+ text-align: center;
34
+ }
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import { screen } from '@testing-library/react';
3
+ import { mockPatient, renderWithSwr } from 'tools';
4
+ import AppointmentDetails from './appointment-details.component';
5
+ import { usePastVisits } from './../past-visit/past-visit.resource';
6
+ import { useAppointments } from './appointments.resource';
7
+
8
+ const testProps = {
9
+ patientUuid: mockPatient.id,
10
+ };
11
+
12
+ const mockUseAppointments = useAppointments as jest.Mock;
13
+ const mockUsePastVisits = usePastVisits as jest.Mock;
14
+
15
+ jest.mock('./../past-visit/past-visit.resource', () => ({
16
+ usePastVisits: jest.fn(),
17
+ }));
18
+
19
+ jest.mock('./appointments.resource', () => ({
20
+ useAppointments: jest.fn(),
21
+ }));
22
+
23
+ describe('RecentandUpcomingAppointments', () => {
24
+ it('renders no data if past and upcoming visit is empty', async () => {
25
+ mockUseAppointments.mockReturnValueOnce({ data: [] });
26
+ mockUsePastVisits.mockReturnValueOnce({ data: [] });
27
+ renderAppointments();
28
+
29
+ expect(screen.getByText(/there is no last encounter to display for this patient/i)).toBeInTheDocument();
30
+ expect(screen.getByText(/there is no return date to display for this patient/i)).toBeInTheDocument();
31
+ });
32
+ });
33
+
34
+ const renderAppointments = () => {
35
+ renderWithSwr(<AppointmentDetails {...testProps} />);
36
+ };
@@ -0,0 +1,43 @@
1
+ import dayjs from 'dayjs';
2
+ import useSWR from 'swr';
3
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
4
+ import { type AppointmentsFetchResponse } from '../types';
5
+
6
+ export const appointmentsSearchUrl = `${restBaseUrl}/appointments/search`;
7
+
8
+ export function useAppointments(patientUuid: string, startDate: string) {
9
+ const abortController = new AbortController();
10
+
11
+ const fetcher = () =>
12
+ openmrsFetch(appointmentsSearchUrl, {
13
+ method: 'POST',
14
+ signal: abortController.signal,
15
+ headers: {
16
+ 'Content-Type': 'application/json',
17
+ },
18
+ body: {
19
+ patientUuid: patientUuid,
20
+ startDate: startDate,
21
+ },
22
+ });
23
+
24
+ const { data, error, isLoading, isValidating } = useSWR<AppointmentsFetchResponse, Error>(
25
+ appointmentsSearchUrl,
26
+ fetcher,
27
+ );
28
+
29
+ const appointments = data?.data?.length
30
+ ? data.data.sort((a, b) => (b.startDateTime > a.startDateTime ? 1 : -1))
31
+ : null;
32
+
33
+ const upcomingAppointment = appointments?.find((appointment) =>
34
+ dayjs((appointment.startDateTime / 1000) * 1000).isAfter(dayjs()),
35
+ );
36
+
37
+ return {
38
+ upcomingAppointment: upcomingAppointment ? upcomingAppointment : null,
39
+ isError: error,
40
+ isLoading,
41
+ isValidating,
42
+ };
43
+ }
@@ -0,0 +1,42 @@
1
+ import { openmrsFetch, restBaseUrl, useConfig, type Patient } from '@openmrs/esm-framework';
2
+ import useSWRImmutable from 'swr/immutable';
3
+ import { type ConfigObject } from '../../config-schema';
4
+ import { type Attribute } from '../../types';
5
+
6
+ const customRepresentation =
7
+ 'custom:(uuid,display,identifiers:(identifier,uuid,preferred,location:(uuid,name),identifierType:(uuid,name,format,formatDescription,validator)),person:(uuid,display,gender,birthdate,dead,age,deathDate,birthdateEstimated,causeOfDeath,preferredName:(uuid,preferred,givenName,middleName,familyName),attributes,preferredAddress:(uuid,preferred,address1,address2,cityVillage,longitude,stateProvince,latitude,country,postalCode,countyDistrict,address3,address4,address5,address6,address7)))';
8
+
9
+ /**
10
+ * React hook that takes a patientUuid and returns patient attributes {@link Attribute}
11
+ * @param patientUuid Unique patient identifier
12
+ * @returns An object containing patient attributes, an `isLoading` boolean and an `error` object
13
+ */
14
+ export const usePatientAttributes = (patientUuid: string) => {
15
+ const { data, error, isLoading } = useSWRImmutable<{ data: Patient }>(
16
+ `${restBaseUrl}/patient/${patientUuid}?v=${customRepresentation}`,
17
+ openmrsFetch,
18
+ );
19
+
20
+ return {
21
+ isLoading,
22
+ attributes: data?.data.person.attributes ?? [],
23
+ error: error,
24
+ };
25
+ };
26
+ /**
27
+ * React hook that takes patientUuid {@link string} and return contact details
28
+ * derived from patient-attributes using configured attributeTypes
29
+ * @param patientUuid Unique patient identifier {@type string}
30
+ * @returns Object containing `contactAttribute` {@link Attribute} loading status
31
+ */
32
+ export const usePatientContactAttributes = (patientUuid: string) => {
33
+ const { contactAttributeType } = useConfig<ConfigObject>();
34
+ const { attributes, isLoading } = usePatientAttributes(patientUuid);
35
+ const contactAttributes = attributes.filter(
36
+ ({ attributeType }) => contactAttributeType?.some((uuid) => attributeType.uuid === uuid),
37
+ );
38
+ return {
39
+ contactAttributes: contactAttributes ?? [],
40
+ isLoading,
41
+ };
42
+ };
@@ -0,0 +1,82 @@
1
+ import React, { useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { ClickableTile } from '@carbon/react';
5
+ import { Edit } from '@carbon/react/icons';
6
+ import {
7
+ age,
8
+ displayName,
9
+ formatDate,
10
+ parseDate,
11
+ ConfigurableLink,
12
+ PatientPhoto,
13
+ PatientBannerToggleContactDetailsButton,
14
+ PatientBannerContactDetails,
15
+ } from '@openmrs/esm-framework';
16
+ import AppointmentDetails from './appointment-details.component';
17
+ import styles from './patient-info.scss';
18
+
19
+ interface PatientInfoProps {
20
+ patient: fhir.Patient;
21
+ handlePatientInfoClick: () => void;
22
+ }
23
+
24
+ const PatientInfo: React.FC<PatientInfoProps> = ({ patient, handlePatientInfoClick }) => {
25
+ const { t } = useTranslation();
26
+ const [showContactDetails, setShowContactDetails] = useState<boolean>(false);
27
+ const patientName = displayName(patient);
28
+
29
+ const toggleShowMore = (e: React.MouseEvent) => {
30
+ e.stopPropagation();
31
+ setShowContactDetails((prevState) => !prevState);
32
+ };
33
+
34
+ return (
35
+ <ClickableTile className={styles.container} onClick={handlePatientInfoClick}>
36
+ <div
37
+ className={classNames({
38
+ [styles.activePatientInfoContainer]: showContactDetails,
39
+ [styles.patientInfoContainer]: !showContactDetails,
40
+ })}>
41
+ <PatientPhoto patientUuid={patient.id} patientName={patientName} size="small" />
42
+ <div className={styles.patientInfoContent}>
43
+ <div className={styles.patientInfoRow}>
44
+ <span className={styles.patientName}>{patientName}</span>
45
+ <ConfigurableLink
46
+ className={styles.patientEditBtn}
47
+ to={`\${openmrsSpaBase}/patient/${patient.id}/edit`}
48
+ title={t('editPatientDetails', 'Edit patient details')}>
49
+ <Edit size={16} />
50
+ </ConfigurableLink>
51
+ </div>
52
+ <div className={styles.patientInfoRow}>
53
+ <div className={styles.demographics}>
54
+ <span>
55
+ {(patient.gender ?? t('unknown', 'Unknown')).replace(/^\w/, (c) => c.toUpperCase())} &middot;{' '}
56
+ </span>
57
+ <span>{age(patient.birthDate)} &middot; </span>
58
+ <span>{formatDate(parseDate(patient.birthDate), { mode: 'wide', time: false })}</span>
59
+ </div>
60
+ </div>
61
+ <div className={styles.patientInfoRow}>
62
+ <span className={styles.identifier}>
63
+ {patient.identifier.length ? patient.identifier.map((identifier) => identifier.value).join(', ') : '--'}
64
+ </span>
65
+ <PatientBannerToggleContactDetailsButton
66
+ showContactDetails={showContactDetails}
67
+ toggleContactDetails={toggleShowMore}
68
+ />
69
+ </div>
70
+ </div>
71
+ </div>
72
+ {showContactDetails && (
73
+ <>
74
+ <PatientBannerContactDetails patientId={patient.id} deceased={patient.deceasedBoolean} />
75
+ <AppointmentDetails patientUuid={patient.id} />
76
+ </>
77
+ )}
78
+ </ClickableTile>
79
+ );
80
+ };
81
+
82
+ export default PatientInfo;
@@ -0,0 +1,60 @@
1
+ @use '@carbon/styles/scss/type';
2
+ @use '@carbon/styles/scss/spacing';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .container {
6
+ border-bottom: 0.0125rem solid $color-gray-30;
7
+ background-color: $ui-02;
8
+ padding: 0;
9
+ }
10
+
11
+ .patientInfoContainer {
12
+ background-color: $ui-02;
13
+ padding: 0 0 0 spacing.$spacing-05;
14
+ display: flex;
15
+ align-items: center;
16
+ min-height: 7rem;
17
+ }
18
+
19
+ .activePatientInfoContainer {
20
+ background-color: $color-blue-10;
21
+ padding: 0 0 0 spacing.$spacing-05;
22
+ display: flex;
23
+ align-items: center;
24
+ min-height: 7rem;
25
+ }
26
+
27
+ .patientName {
28
+ @include type.type-style('heading-03');
29
+ }
30
+
31
+ .patientInfoContent {
32
+ margin: spacing.$spacing-05 0 0 spacing.$spacing-05;
33
+ width: 100%;
34
+ }
35
+
36
+ .demographics {
37
+ @include type.type-style('body-compact-02');
38
+ color: $text-02;
39
+ margin-top: spacing.$spacing-02;
40
+ }
41
+
42
+ .identifier {
43
+ @include type.type-style('body-compact-02');
44
+ color: $ui-04;
45
+ }
46
+
47
+ .patientInfoRow {
48
+ display: flex;
49
+ justify-content: space-between;
50
+ align-items: center;
51
+
52
+ & > button {
53
+ min-height: 2rem;
54
+ }
55
+ }
56
+
57
+ .patientEditBtn {
58
+ color: $ui-05;
59
+ margin: spacing.$spacing-03;
60
+ }
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { screen, render } from '@testing-library/react';
4
+ import { age } from '@openmrs/esm-framework';
5
+ import { mockPatient, mockPatientWithLongName, mockPatientWithoutFormattedName } from 'tools';
6
+ import PatientInfo from './patient-info.component';
7
+
8
+ const mockAge = age as jest.Mock;
9
+
10
+ jest.mock('@openmrs/esm-framework', () => {
11
+ const originalModule = jest.requireActual('@openmrs/esm-framework');
12
+ return {
13
+ ...originalModule,
14
+ age: jest.fn(),
15
+ };
16
+ });
17
+
18
+ describe('Patient Info', () => {
19
+ it.each([
20
+ [mockPatient, 'Wilson, John'],
21
+ [mockPatientWithLongName, 'family name, Some very long given name'],
22
+ [mockPatientWithoutFormattedName, 'given middle family name'],
23
+ ])(`should render patient info correctly`, async (patient, displayName) => {
24
+ const user = userEvent.setup();
25
+
26
+ mockAge.mockReturnValue(35);
27
+
28
+ renderPatientInfo(patient);
29
+
30
+ expect(screen.getByText(new RegExp(displayName))).toBeInTheDocument();
31
+ expect(screen.getByText(/35/)).toBeInTheDocument();
32
+ expect(screen.getByText(/Male/i)).toBeInTheDocument();
33
+ expect(screen.getByText(/04 — Apr — 1972/i)).toBeInTheDocument();
34
+ expect(screen.getByText(/100732HE, 100GEJ/i)).toBeInTheDocument();
35
+
36
+ const showDetailsButton = screen.getByText('Patient Banner Toggle Contact Details Button');
37
+ expect(showDetailsButton).toBeInTheDocument();
38
+ });
39
+ });
40
+
41
+ const renderPatientInfo = (patient) => {
42
+ render(<PatientInfo handlePatientInfoClick={() => {}} patient={patient} />);
43
+ };
@@ -0,0 +1,99 @@
1
+ import React, { useCallback, useEffect } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Calendar, Location } from '@carbon/react/icons';
4
+ import { Dropdown } from '@carbon/react';
5
+ import { formatDate, useSession } from '@openmrs/esm-framework';
6
+ import PatientQueueIllustration from './patient-queue-illustration.component';
7
+ import { useQueueLocations } from '../patient-search/hooks/useQueueLocations';
8
+
9
+ import {
10
+ updateSelectedQueueLocationUuid,
11
+ updateSelectedQueueLocationName,
12
+ updateSelectedServiceName,
13
+ useSelectedQueueLocationName,
14
+ useSelectedQueueLocationUuid,
15
+ } from '../helpers/helpers';
16
+ import styles from './patient-queue-header.scss';
17
+
18
+ const PatientQueueHeader: React.FC<{ title?: string }> = ({ title }) => {
19
+ const { t } = useTranslation();
20
+ const { queueLocations, isLoading, error } = useQueueLocations();
21
+ const userSession = useSession();
22
+ const userLocation = userSession?.sessionLocation?.display;
23
+ const currentQueueLocationName = useSelectedQueueLocationName();
24
+ const currentQueueLocationUuid = useSelectedQueueLocationUuid();
25
+
26
+ const handleQueueLocationChange = useCallback(({ selectedItem }) => {
27
+ if (selectedItem.id === 'all') {
28
+ updateSelectedQueueLocationUuid(null);
29
+ updateSelectedQueueLocationName(null);
30
+ } else {
31
+ updateSelectedQueueLocationUuid(selectedItem.id);
32
+ updateSelectedQueueLocationName(selectedItem.name);
33
+ updateSelectedServiceName('All');
34
+ }
35
+ }, []);
36
+
37
+ useEffect(() => {
38
+ if (!isLoading && !error && !currentQueueLocationUuid) {
39
+ if (queueLocations.length === 1) {
40
+ handleQueueLocationChange({ selectedItem: queueLocations[0] });
41
+ }
42
+ if (queueLocations.some((location) => location.id === userSession?.sessionLocation?.uuid)) {
43
+ handleQueueLocationChange({
44
+ selectedItem: {
45
+ id: userSession?.sessionLocation?.uuid,
46
+ name: userSession?.sessionLocation?.display,
47
+ },
48
+ });
49
+ }
50
+ }
51
+ }, [
52
+ queueLocations,
53
+ currentQueueLocationName,
54
+ currentQueueLocationUuid,
55
+ isLoading,
56
+ error,
57
+ userSession?.sessionLocation?.uuid,
58
+ ]);
59
+
60
+ return (
61
+ <>
62
+ <div className={styles.header} data-testid="patient-queue-header">
63
+ <div className={styles['left-justified-items']}>
64
+ <PatientQueueIllustration />
65
+ <div className={styles['page-labels']}>
66
+ <p>{t('serviceQueues', 'Service queues')}</p>
67
+ <p className={styles['page-name']}>{title ?? t('home', 'Home')}</p>
68
+ </div>
69
+ </div>
70
+ <div className={styles['right-justified-items']}>
71
+ <div className={styles['date-and-location']}>
72
+ <Location size={16} />
73
+ <span className={styles.value}>{userLocation}</span>
74
+ <span className={styles.middot}>&middot;</span>
75
+ <Calendar size={16} />
76
+ <span className={styles.value}>{formatDate(new Date(), { mode: 'standard' })}</span>
77
+ </div>
78
+ <div className={styles.dropdownContainer}>
79
+ <Dropdown
80
+ aria-label="Select queue location"
81
+ className={styles.dropdown}
82
+ id="queueLocationDropdown"
83
+ label={currentQueueLocationName ?? t('all', 'All')}
84
+ items={
85
+ queueLocations.length !== 1 ? [{ id: 'all', name: t('all', 'All') }, ...queueLocations] : queueLocations
86
+ }
87
+ itemToString={(item) => (item ? item.name : '')}
88
+ titleText={t('location', 'Location')}
89
+ type="inline"
90
+ onChange={handleQueueLocationChange}
91
+ />
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </>
96
+ );
97
+ };
98
+
99
+ export default PatientQueueHeader;
@@ -0,0 +1,90 @@
1
+ @use '@carbon/styles/scss/type';
2
+ @use '@carbon/styles/scss/spacing';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .header {
6
+ @include type.type-style('body-compact-02');
7
+ color: $text-02;
8
+ height: spacing.$spacing-12;
9
+ background-color: $ui-02;
10
+ border: 1px solid $ui-03;
11
+ border-left: 0px;
12
+ display: flex;
13
+ justify-content: space-between;
14
+ }
15
+
16
+ .left-justified-items {
17
+ display: flex;
18
+ flex-direction: row;
19
+ align-items: center;
20
+ }
21
+
22
+ .right-justified-items {
23
+ @include type.type-style('body-compact-02');
24
+ color: $text-02;
25
+ padding-top: 1rem;
26
+ }
27
+
28
+ .page-name {
29
+ @include type.type-style('heading-04');
30
+ }
31
+
32
+ .page-labels {
33
+ p:first-of-type {
34
+ margin-bottom: 0.25rem;
35
+ }
36
+ }
37
+
38
+ .date-and-location {
39
+ display: flex;
40
+ justify-content: flex-end;
41
+ align-items: center;
42
+ margin-right: 1rem;
43
+ }
44
+
45
+ .value {
46
+ margin-left: 0.25rem;
47
+ }
48
+
49
+ .middot {
50
+ margin: 0 0.5rem;
51
+ }
52
+
53
+ .view {
54
+ @include type.type-style('label-01');
55
+ }
56
+
57
+ .dropdownContainer {
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: flex-end;
61
+ margin-top: 0.5rem;
62
+ }
63
+
64
+ .dropdown {
65
+ :global(.cds--list-box__field) {
66
+ width: 14rem;
67
+ overflow: hidden;
68
+ }
69
+
70
+ :global(.cds--dropdown--inline .cds--list-box__menu) {
71
+ left: -4rem;
72
+ }
73
+ }
74
+
75
+ // Overriding styles for RTL support
76
+ html[dir='rtl'] {
77
+ .date-and-location {
78
+ margin-right: unset;
79
+ margin-left: spacing.$spacing-05;
80
+ & > svg:first-child {
81
+ order: -1;
82
+ }
83
+ & > svg:nth-child(4) {
84
+ order: 1;
85
+ }
86
+ & > span:nth-child(2) {
87
+ order: -2;
88
+ }
89
+ }
90
+ }