@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,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, 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 { 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/test-helpers';
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 } from '@openmrs/esm-framework';
4
+ import { AppointmentsFetchResponse } from '../types';
5
+
6
+ export const appointmentsSearchUrl = `/ws/rest/v1/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,71 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Grid, Row, Column, InlineLoading } from '@carbon/react';
4
+ import { usePatientContactAttributes } from './hooks/usePatientAttributes';
5
+ import styles from './contact-details.scss';
6
+
7
+ interface ContactDetailsProps {
8
+ patientId: string;
9
+ address: Array<fhir.Address>;
10
+ contact: Array<fhir.ContactPoint>;
11
+ }
12
+
13
+ const Address: React.FC<{ address?: fhir.Address }> = ({ address }) => {
14
+ const { t } = useTranslation();
15
+
16
+ return (
17
+ <div>
18
+ <p className={styles.heading}>{t('placeOfResidence', 'Place of Residence')}</p>
19
+ <ul>
20
+ {address ? (
21
+ <>
22
+ <li>{address.postalCode}</li>
23
+ <li>{address.city}</li>
24
+ <li>{address.state}</li>
25
+ <li>{address.country}</li>
26
+ </>
27
+ ) : (
28
+ '--'
29
+ )}
30
+ </ul>
31
+ </div>
32
+ );
33
+ };
34
+
35
+ const Contact: React.FC<{ contact: Array<fhir.ContactPoint>; patientUuid: string }> = ({ contact, patientUuid }) => {
36
+ const { t } = useTranslation();
37
+ const value = contact?.length ? contact[0].value : '--';
38
+
39
+ const { isLoading, contactAttributes } = usePatientContactAttributes(patientUuid);
40
+
41
+ return (
42
+ <div>
43
+ <p className={styles.heading}>{t('contactDetails', 'Contact Details')}</p>
44
+ <ul>
45
+ <li>{value}</li>
46
+ {isLoading ? (
47
+ <InlineLoading description={t('loading', 'Loading...')} />
48
+ ) : (
49
+ contactAttributes?.map(({ attributeType, value, uuid }) => (
50
+ <li key={uuid}>
51
+ {attributeType.display} : {value}
52
+ </li>
53
+ ))
54
+ )}
55
+ </ul>
56
+ </div>
57
+ );
58
+ };
59
+
60
+ const ContactDetails: React.FC<ContactDetailsProps> = ({ patientId, address, contact }) => {
61
+ const currentAddress = address ? address.find((a) => a.use === 'home') : undefined;
62
+
63
+ return (
64
+ <div className={styles.container}>
65
+ <Address address={currentAddress} />
66
+ <Contact contact={contact} patientUuid={patientId} />
67
+ </div>
68
+ );
69
+ };
70
+
71
+ export default ContactDetails;
@@ -0,0 +1,31 @@
1
+ @use '@carbon/styles/scss/colors';
2
+ @use '@carbon/styles/scss/spacing';
3
+ @use '@carbon/styles/scss/type';
4
+ @import '~@openmrs/esm-styleguide/src/vars';
5
+
6
+ .heading {
7
+ @include type.type-style('heading-compact-01');
8
+ color: $text-02;
9
+ }
10
+
11
+ .container {
12
+ background-color: $color-blue-10;
13
+ display: grid;
14
+ grid-template-columns: 1fr 1fr;
15
+ border-top: 1px solid colors.$blue-20;
16
+ margin-top: spacing.$spacing-01;
17
+
18
+ & > div {
19
+ padding: spacing.$spacing-05;
20
+ }
21
+
22
+ & > div:first-child {
23
+ border-right: 1px solid colors.$blue-20;
24
+ }
25
+
26
+ & ul > li {
27
+ padding: spacing.$spacing-02 0;
28
+ color: $text-02;
29
+ @include type.type-style('body-compact-01');
30
+ }
31
+ }
@@ -0,0 +1,94 @@
1
+ import React from 'react';
2
+ import { screen } from '@testing-library/react';
3
+ import { renderWithSwr } from '../../../../tools/test-helpers';
4
+ import { usePatientAttributes, usePatientContactAttributes } from './hooks/usePatientAttributes';
5
+ import ContactDetails from './contact-details.component';
6
+
7
+ const mockUsePatientAttributes = usePatientAttributes as jest.Mock;
8
+ const mockUsePatientContactAttributes = usePatientContactAttributes as jest.Mock;
9
+
10
+ jest.mock('./hooks/usePatientAttributes', () => ({
11
+ usePatientContactAttributes: jest.fn(),
12
+ usePatientAttributes: jest.fn(),
13
+ }));
14
+
15
+ let testProps = {
16
+ address: [
17
+ {
18
+ city: 'City9564',
19
+ country: 'Country9564',
20
+ id: '0000',
21
+ postalCode: '18156',
22
+ state: 'State9564',
23
+ use: 'home',
24
+ },
25
+ ],
26
+ contact: [{ value: '+0123456789' }],
27
+ patientId: '1111',
28
+ };
29
+
30
+ const personAttributeMock = [
31
+ {
32
+ display: 'Next of Kin Contact Phone Number = 0000000000',
33
+ uuid: '1de1ac71-68e8-4a08-a7e2-5042093d4e46',
34
+ value: '0700-000-000',
35
+ attributeType: {
36
+ uuid: 'a657a4f1-9c0f-444b-a1fd-445bb91dd12d',
37
+ display: 'Next of Kin Contact Phone Number',
38
+ },
39
+ },
40
+ ];
41
+
42
+ describe('ContactDetails: ', () => {
43
+ it("renders the patient's address and contact details when available", async () => {
44
+ mockUsePatientAttributes.mockReturnValueOnce({
45
+ isLoading: false,
46
+ attributes: personAttributeMock,
47
+ error: null,
48
+ });
49
+
50
+ mockUsePatientContactAttributes.mockReturnValueOnce({
51
+ isLoading: false,
52
+ contactAttributes: personAttributeMock,
53
+ });
54
+
55
+ renderContactDetails();
56
+
57
+ expect(screen.getByText(/Place of Residence/i)).toBeInTheDocument();
58
+ expect(screen.getByText(/City9564/)).toBeInTheDocument();
59
+ expect(screen.getByText(/Country9564/)).toBeInTheDocument();
60
+ expect(screen.getByText(/18156/)).toBeInTheDocument();
61
+ expect(screen.getByText(/State9564/)).toBeInTheDocument();
62
+ expect(screen.getByText(/Contact Details/i)).toBeInTheDocument();
63
+ expect(screen.getByText(/0123456789/)).toBeInTheDocument();
64
+ });
65
+
66
+ it('renders an empty state view when address and contact information are not available', () => {
67
+ testProps = {
68
+ address: null,
69
+ contact: null,
70
+ patientId: 'some-uuid',
71
+ };
72
+
73
+ mockUsePatientAttributes.mockReturnValueOnce({
74
+ isLoading: false,
75
+ attributes: [],
76
+ error: null,
77
+ });
78
+
79
+ mockUsePatientContactAttributes.mockReturnValueOnce({
80
+ isLoading: false,
81
+ contactAttributes: [],
82
+ });
83
+
84
+ renderContactDetails();
85
+
86
+ expect(screen.getByText(/Place of Residence/i)).toBeInTheDocument();
87
+ expect(screen.getByText(/Contact Details/i)).toBeInTheDocument();
88
+ expect(screen.getAllByText('--').length).toBe(2);
89
+ });
90
+ });
91
+
92
+ function renderContactDetails() {
93
+ renderWithSwr(<ContactDetails {...testProps} />);
94
+ }
@@ -0,0 +1,42 @@
1
+ import { openmrsFetch, useConfig } from '@openmrs/esm-framework';
2
+ import useSWRImmutable from 'swr/immutable';
3
+ import { ConfigObject } from '../../config-schema';
4
+ import { Patient, 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
+ `/ws/rest/v1/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() as ConfigObject;
34
+ const { attributes, isLoading } = usePatientAttributes(patientUuid);
35
+ const contactAttributes = attributes.filter(({ attributeType }) =>
36
+ contactAttributeType?.some((uuid) => attributeType.uuid === uuid),
37
+ );
38
+ return {
39
+ contactAttributes: contactAttributes ?? [],
40
+ isLoading,
41
+ };
42
+ };
@@ -0,0 +1,78 @@
1
+ import React, { useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button, ClickableTile } from '@carbon/react';
4
+ import { ChevronDown, ChevronUp, Edit } from '@carbon/react/icons';
5
+ import { age, ExtensionSlot, formatDate, parseDate, ConfigurableLink } from '@openmrs/esm-framework';
6
+ import AppointmentDetails from './appointment-details.component';
7
+ import ContactDetails from './contact-details.component';
8
+ import styles from './patient-info.scss';
9
+
10
+ interface PatientInfoProps {
11
+ patient: fhir.Patient;
12
+ handlePatientInfoClick: () => void;
13
+ }
14
+
15
+ const PatientInfo: React.FC<PatientInfoProps> = ({ patient, handlePatientInfoClick }) => {
16
+ const { t } = useTranslation();
17
+ const [showContactDetails, setShowContactDetails] = useState<boolean>(false);
18
+ const patientName = `${patient.name?.[0].given?.join(' ')} ${patient?.name?.[0].family}`;
19
+ const patientPhotoSlotState = React.useMemo(
20
+ () => ({ patientUuid: patient.id, patientName }),
21
+ [patient.id, patientName],
22
+ );
23
+
24
+ const toggleShowMore = (e: React.MouseEvent) => {
25
+ e.stopPropagation();
26
+ setShowContactDetails((prevState) => !prevState);
27
+ };
28
+
29
+ return (
30
+ <ClickableTile className={styles.container} onClick={handlePatientInfoClick}>
31
+ <div className={`${showContactDetails ? styles.activePatientInfoContainer : styles.patientInfoContainer}`}>
32
+ <ExtensionSlot name="patient-photo-slot" state={patientPhotoSlotState} />
33
+ <div className={styles.patientInfoContent}>
34
+ <div className={styles.patientInfoRow}>
35
+ <span className={styles.patientName}>{patientName}</span>
36
+ <ConfigurableLink
37
+ to={`\${openmrsSpaBase}/patient/${patient.id}/edit`}
38
+ className={styles.patientEditBtn}
39
+ title={t('editPatientDetails', 'Edit Patient Details')}>
40
+ <Edit size={16} />
41
+ </ConfigurableLink>
42
+ </div>
43
+ <div className={styles.patientInfoRow}>
44
+ <div className={styles.demographics}>
45
+ <span>
46
+ {(patient.gender ?? t('unknown', 'Unknown')).replace(/^\w/, (c) => c.toUpperCase())} &middot;{' '}
47
+ </span>
48
+ <span>{age(patient.birthDate)} &middot; </span>
49
+ <span>{formatDate(parseDate(patient.birthDate), { mode: 'wide', time: false })}</span>
50
+ </div>
51
+ </div>
52
+ <div className={styles.patientInfoRow}>
53
+ <span className={styles.identifier}>
54
+ {patient.identifier.length ? patient.identifier.map((identifier) => identifier.value).join(', ') : '--'}
55
+ </span>
56
+ <Button
57
+ kind="ghost"
58
+ renderIcon={(props) =>
59
+ showContactDetails ? <ChevronUp size={16} {...props} /> : <ChevronDown size={16} {...props} />
60
+ }
61
+ iconDescription="Toggle contact details"
62
+ onClick={(e) => toggleShowMore(e)}>
63
+ {showContactDetails ? t('showLess', 'Show less') : t('showAllDetails', 'Show all details')}
64
+ </Button>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ {showContactDetails && (
69
+ <>
70
+ <ContactDetails patientId={patient.id} address={patient.address ?? []} contact={patient.contact} />
71
+ <AppointmentDetails patientUuid={patient.id} />
72
+ </>
73
+ )}
74
+ </ClickableTile>
75
+ );
76
+ };
77
+
78
+ 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,45 @@
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 } from '../../../../tools/test-helpers';
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
+ test('should render patient info correctly', async () => {
20
+ const user = userEvent.setup();
21
+
22
+ mockAge.mockReturnValue(35);
23
+
24
+ renderPatientInfo();
25
+
26
+ expect(screen.getByText(/John Wilson/)).toBeInTheDocument();
27
+ expect(screen.getByText(/35/)).toBeInTheDocument();
28
+ expect(screen.getByText(/Male/i)).toBeInTheDocument();
29
+ expect(screen.getByText(/04 — Apr — 1972/i)).toBeInTheDocument();
30
+ expect(screen.getByText(/100732HE, 100GEJ/i)).toBeInTheDocument();
31
+
32
+ const showDetailsButton = screen.getByRole('button', { name: /Show all details/ });
33
+ expect(showDetailsButton).toBeInTheDocument();
34
+
35
+ await user.click(showDetailsButton);
36
+
37
+ expect(screen.queryByRole('button', { name: /Show all details/ })).not.toBeInTheDocument();
38
+ const showLessButton = screen.getByRole('button', { name: /Show less/i });
39
+ expect(showLessButton).toBeInTheDocument();
40
+ });
41
+ });
42
+
43
+ const renderPatientInfo = () => {
44
+ render(<PatientInfo handlePatientInfoClick={() => {}} patient={mockPatient} />);
45
+ };