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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/.turbo/turbo-build.log +42 -0
  2. package/dist/130.js +2 -0
  3. package/dist/130.js.LICENSE.txt +3 -0
  4. package/dist/130.js.map +1 -0
  5. package/dist/152.js +1 -0
  6. package/dist/152.js.map +1 -0
  7. package/dist/224.js +1 -0
  8. package/dist/224.js.map +1 -0
  9. package/dist/255.js +2 -0
  10. package/dist/255.js.LICENSE.txt +9 -0
  11. package/dist/255.js.map +1 -0
  12. package/dist/271.js +1 -0
  13. package/dist/303.js +1 -0
  14. package/dist/303.js.map +1 -0
  15. package/dist/309.js +1 -0
  16. package/dist/309.js.map +1 -0
  17. package/dist/319.js +1 -0
  18. package/dist/4.js +1 -0
  19. package/dist/4.js.map +1 -0
  20. package/dist/445.js +2 -0
  21. package/dist/445.js.LICENSE.txt +54 -0
  22. package/dist/445.js.map +1 -0
  23. package/dist/460.js +1 -0
  24. package/dist/501.js +1 -0
  25. package/dist/501.js.map +1 -0
  26. package/dist/574.js +1 -0
  27. package/dist/591.js +2 -0
  28. package/dist/591.js.LICENSE.txt +32 -0
  29. package/dist/591.js.map +1 -0
  30. package/dist/644.js +1 -0
  31. package/dist/729.js +1 -0
  32. package/dist/729.js.map +1 -0
  33. package/dist/757.js +1 -0
  34. package/dist/784.js +2 -0
  35. package/dist/784.js.LICENSE.txt +9 -0
  36. package/dist/784.js.map +1 -0
  37. package/dist/788.js +1 -0
  38. package/dist/807.js +1 -0
  39. package/dist/833.js +1 -0
  40. package/dist/857.js +2 -0
  41. package/dist/857.js.LICENSE.txt +5 -0
  42. package/dist/857.js.map +1 -0
  43. package/dist/904.js +1 -0
  44. package/dist/904.js.map +1 -0
  45. package/dist/kenyaemr-esm-appointments-app.js +1 -0
  46. package/dist/kenyaemr-esm-appointments-app.js.buildmanifest.json +699 -0
  47. package/dist/kenyaemr-esm-appointments-app.js.map +1 -0
  48. package/dist/main.js +2 -0
  49. package/dist/main.js.LICENSE.txt +64 -0
  50. package/dist/main.js.map +1 -0
  51. package/dist/routes.json +1 -0
  52. package/jest.config.js +3 -0
  53. package/package.json +57 -0
  54. package/src/admin/appointment-services/appointment-services-hook.ts +31 -0
  55. package/src/admin/appointment-services/appointment-services-validation.ts +17 -0
  56. package/src/admin/appointment-services/appointment-services.component.tsx +182 -0
  57. package/src/admin/appointment-services/appointment-services.scss +25 -0
  58. package/src/appointments/appointment-tabs.component.tsx +48 -0
  59. package/src/appointments/appointment-tabs.scss +53 -0
  60. package/src/appointments/appointment-tabs.test.tsx +55 -0
  61. package/src/appointments/common-components/appointments-actions.component.tsx +86 -0
  62. package/src/appointments/common-components/appointments-actions.scss +4 -0
  63. package/src/appointments/common-components/appointments-actions.test.tsx +201 -0
  64. package/src/appointments/common-components/appointments-table.component.tsx +277 -0
  65. package/src/appointments/common-components/appointments-table.scss +133 -0
  66. package/src/appointments/common-components/appointments-table.test.tsx +134 -0
  67. package/src/appointments/common-components/checkin-button.component.tsx +43 -0
  68. package/src/appointments/common-components/end-appointment-modal.component.tsx +104 -0
  69. package/src/appointments/common-components/end-appointment-modal.test.tsx +80 -0
  70. package/src/appointments/common-components/location-select-option.component.tsx +48 -0
  71. package/src/appointments/details/appointment-details.component.tsx +91 -0
  72. package/src/appointments/details/appointment-details.scss +81 -0
  73. package/src/appointments/details/appointment-details.test.tsx +103 -0
  74. package/src/appointments/scheduled/appointments-list.component.tsx +33 -0
  75. package/src/appointments/scheduled/early-appointments.component.tsx +32 -0
  76. package/src/appointments/scheduled/scheduled-appointments.component.tsx +215 -0
  77. package/src/appointments/scheduled/scheduled-appointments.scss +4 -0
  78. package/src/appointments/unscheduled/unscheduled-appointments.component.tsx +146 -0
  79. package/src/appointments/unscheduled/unscheduled-appointments.test.tsx +131 -0
  80. package/src/appointments/utils.tsx +80 -0
  81. package/src/appointments.component.tsx +44 -0
  82. package/src/appointments.test.tsx +15 -0
  83. package/src/calendar/appointments-calendar-view-view.scss +24 -0
  84. package/src/calendar/appointments-calendar-view.component.tsx +36 -0
  85. package/src/calendar/appointments-calendar-view.test.tsx +22 -0
  86. package/src/calendar/header/calendar-header.component.tsx +34 -0
  87. package/src/calendar/header/calendar-header.scss +32 -0
  88. package/src/calendar/monthly/days-of-week.component.tsx +16 -0
  89. package/src/calendar/monthly/days-of-week.scss +33 -0
  90. package/src/calendar/monthly/monthly-calendar-view.component.tsx +34 -0
  91. package/src/calendar/monthly/monthly-header.module.scss +14 -0
  92. package/src/calendar/monthly/monthly-header.module.tsx +40 -0
  93. package/src/calendar/monthly/monthly-view-workload.scss +188 -0
  94. package/src/calendar/monthly/monthly-workload-view-expanded.component.tsx +42 -0
  95. package/src/calendar/monthly/monthly-workload-view.component.tsx +109 -0
  96. package/src/config-schema.ts +151 -0
  97. package/src/constants.ts +55 -0
  98. package/src/createDashboardLink.component.tsx +39 -0
  99. package/src/dashboard.meta.ts +21 -0
  100. package/src/declarations.d.ts +4 -0
  101. package/src/empty-state/empty-data-illustration.component.tsx +39 -0
  102. package/src/empty-state/empty-state.component.tsx +32 -0
  103. package/src/empty-state/empty-state.scss +69 -0
  104. package/src/form/appointments-form.component.tsx +891 -0
  105. package/src/form/appointments-form.resource.ts +165 -0
  106. package/src/form/appointments-form.scss +113 -0
  107. package/src/form/appointments-form.test.tsx +212 -0
  108. package/src/header/appointments-header.component.tsx +79 -0
  109. package/src/header/appointments-header.scss +95 -0
  110. package/src/header/appointments-illustration.component.tsx +22 -0
  111. package/src/helpers/excel.ts +61 -0
  112. package/src/helpers/functions.ts +82 -0
  113. package/src/helpers/index.ts +2 -0
  114. package/src/helpers/time.tsx +15 -0
  115. package/src/home/home-appointments.component.tsx +22 -0
  116. package/src/home/home-appointments.scss +10 -0
  117. package/src/hooks/patientAppointmentContext.ts +15 -0
  118. package/src/hooks/selectedDateContext.ts +10 -0
  119. package/src/hooks/useAppointmentList.ts +48 -0
  120. package/src/hooks/useAppointmentService.ts +11 -0
  121. package/src/hooks/useAppointmentsCalendar.ts +68 -0
  122. package/src/hooks/useClinicalMetrics.ts +79 -0
  123. package/src/hooks/useDefaultLocation.ts +14 -0
  124. package/src/hooks/useOverlay.tsx +45 -0
  125. package/src/hooks/usePatientAppointmentHistory.ts +49 -0
  126. package/src/hooks/useProviders.ts +18 -0
  127. package/src/hooks/useTodaysVisits.ts +19 -0
  128. package/src/hooks/useUnscheduledAppointments.ts +45 -0
  129. package/src/index.ts +111 -0
  130. package/src/metrics/appointments-metrics.component.tsx +71 -0
  131. package/src/metrics/appointments-metrics.scss +15 -0
  132. package/src/metrics/appointments-metrics.test.tsx +49 -0
  133. package/src/metrics/metrics-card.component.tsx +76 -0
  134. package/src/metrics/metrics-card.scss +77 -0
  135. package/src/metrics/metrics-header.component.tsx +62 -0
  136. package/src/metrics/metrics-header.scss +33 -0
  137. package/src/past-visit/encounter-list.component.tsx +54 -0
  138. package/src/past-visit/past-visit.component.tsx +106 -0
  139. package/src/past-visit/past-visit.resource.ts +25 -0
  140. package/src/past-visit/past-visit.scss +106 -0
  141. package/src/patient-appointments/patient-appointments-action-menu.component.tsx +65 -0
  142. package/src/patient-appointments/patient-appointments-action-menu.scss +7 -0
  143. package/src/patient-appointments/patient-appointments-base.component.tsx +165 -0
  144. package/src/patient-appointments/patient-appointments-base.scss +85 -0
  145. package/src/patient-appointments/patient-appointments-base.test.tsx +91 -0
  146. package/src/patient-appointments/patient-appointments-cancel-modal.component.tsx +66 -0
  147. package/src/patient-appointments/patient-appointments-detailed-summary.component.tsx +15 -0
  148. package/src/patient-appointments/patient-appointments-header.scss +27 -0
  149. package/src/patient-appointments/patient-appointments-header.tsx +42 -0
  150. package/src/patient-appointments/patient-appointments-overview.component.tsx +35 -0
  151. package/src/patient-appointments/patient-appointments-overview.scss +7 -0
  152. package/src/patient-appointments/patient-appointments-table.scss +0 -0
  153. package/src/patient-appointments/patient-appointments-table.tsx +128 -0
  154. package/src/patient-appointments/patient-appointments.resource.ts +72 -0
  155. package/src/patient-appointments/patient-upcoming-appointments-card.component.tsx +122 -0
  156. package/src/patient-appointments/patient-upcoming-appointments-card.scss +46 -0
  157. package/src/patient-search/patient-search.component.tsx +34 -0
  158. package/src/patient-search/patient-search.scss +23 -0
  159. package/src/root.component.tsx +26 -0
  160. package/src/root.scss +50 -0
  161. package/src/routes.json +153 -0
  162. package/src/scheduled-appointments-config-schema.ts +169 -0
  163. package/src/types/index.ts +189 -0
  164. package/src/workload/monthly-view-workload/monthly-view.component.tsx +69 -0
  165. package/src/workload/monthly-view-workload/monthly-workload.scss +223 -0
  166. package/src/workload/monthly-view-workload/monthlyWorkCard.tsx +45 -0
  167. package/src/workload/workload-card.component.tsx +31 -0
  168. package/src/workload/workload.component.tsx +47 -0
  169. package/src/workload/workload.resource.ts +78 -0
  170. package/src/workload/workload.scss +92 -0
  171. package/translations/am.json +148 -0
  172. package/translations/ar.json +148 -0
  173. package/translations/en.json +159 -0
  174. package/translations/es.json +148 -0
  175. package/translations/fr.json +148 -0
  176. package/translations/he.json +148 -0
  177. package/translations/km.json +148 -0
  178. package/translations/zh.json +148 -0
  179. package/translations/zh_CN.json +148 -0
  180. package/tsconfig.json +5 -0
  181. package/webpack.config.js +1 -0
@@ -0,0 +1,134 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { defineConfigSchema, getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
5
+ import { type Appointment } from '../../types';
6
+ import { downloadAppointmentsAsExcel } from '../../helpers/excel';
7
+ import AppointmentsTable from './appointments-table.component';
8
+ import { configSchema } from '../../config-schema';
9
+ import { getByTextWithMarkup } from '../../../../../tools/test-utils';
10
+
11
+ defineConfigSchema('@openmrs/esm-appointments-app', configSchema);
12
+
13
+ const appointments: Array<Appointment> = [
14
+ {
15
+ uuid: '7cd38a6d-377e-491b-8284-b04cf8b8c6d8',
16
+ appointmentNumber: '00001',
17
+ patient: {
18
+ identifier: '100GEJ',
19
+ identifiers: [],
20
+ name: 'John Wilson',
21
+ uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e',
22
+ gender: 'M',
23
+ age: '35',
24
+ birthDate: '1986-04-03T00:00:00.000+0000',
25
+ phoneNumber: '0700000000',
26
+ },
27
+ service: {
28
+ appointmentServiceId: 1,
29
+ name: 'Outpatient',
30
+ description: null,
31
+ startTime: '',
32
+ endTime: '',
33
+ maxAppointmentsLimit: null,
34
+ durationMins: null,
35
+ location: {
36
+ uuid: '8d6c993e-c2cc-11de-8d13-0010c6dffd0f',
37
+ },
38
+ uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90',
39
+ initialAppointmentStatus: 'Scheduled',
40
+ creatorName: null,
41
+ },
42
+ provider: {
43
+ uuid: 'f9badd80-ab76-11e2-9e96-0800200c9a66',
44
+ person: { uuid: '24252571-dd5a-11e6-9d9c-0242ac150002', display: 'Dr James Cook' },
45
+ },
46
+ location: { name: 'HIV Clinic', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' },
47
+ startDateTime: new Date().toISOString(),
48
+ appointmentKind: 'WalkIn',
49
+ status: 'Scheduled',
50
+ comments: 'Some comments',
51
+ additionalInfo: null,
52
+ providers: [{ uuid: '24252571-dd5a-11e6-9d9c-0242ac150002', display: 'Dr James Cook' }],
53
+ recurring: false,
54
+ voided: false,
55
+ teleconsultationLink: null,
56
+ extensions: [],
57
+ },
58
+ ];
59
+
60
+ const mockedDownloadAppointmentsAsExcel = downloadAppointmentsAsExcel as jest.Mock;
61
+ const mockedUseConfig = useConfig as jest.Mock;
62
+
63
+ jest.mock('../../helpers/excel');
64
+ jest.mock('../../hooks/useOverlay');
65
+
66
+ describe('AppointmentsTable', () => {
67
+ const props = {
68
+ appointments: [],
69
+ isLoading: false,
70
+ tableHeading: 'scheduled',
71
+ visits: [],
72
+ scheduleType: 'Scheduled',
73
+ };
74
+
75
+ beforeEach(() => {
76
+ mockedUseConfig.mockReturnValue({
77
+ ...getDefaultsFromConfigSchema(configSchema),
78
+ customPatientChartUrl: 'url-to-patient-chart',
79
+ checkInButton: { enabled: false },
80
+ checkOutButton: { enabled: false },
81
+ });
82
+ });
83
+
84
+ it('renders an empty state if appointments data is unavailable', async () => {
85
+ render(<AppointmentsTable {...props} />);
86
+
87
+ await screen.findByRole('heading', { name: /scheduled appointment/i });
88
+
89
+ expect(getByTextWithMarkup('There are no scheduled appointments to display')).toBeInTheDocument();
90
+ });
91
+
92
+ it('renders a loading state when fetching data', () => {
93
+ render(<AppointmentsTable {...props} isLoading={true} />);
94
+
95
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
96
+ });
97
+
98
+ it('renders a tabular overview of the scheduled appointments', async () => {
99
+ render(<AppointmentsTable {...props} appointments={appointments} />);
100
+
101
+ await screen.findByRole('heading', { name: /scheduled appointment/i });
102
+ expect(screen.getByRole('search', { name: /filter table/i })).toBeInTheDocument();
103
+ expect(screen.getByRole('button', { name: /download/i })).toBeInTheDocument();
104
+ expect(screen.getByRole('row', { name: /john wilson 100gej hiv clinic outpatient/i })).toBeInTheDocument();
105
+ expect(screen.getByRole('link', { name: /john wilson/i })).toBeInTheDocument();
106
+ expect(screen.getByRole('link', { name: /john wilson/i })).toHaveAttribute('href', 'url-to-patient-chart');
107
+ });
108
+
109
+ it('updates the search string when the search input changes', async () => {
110
+ const user = userEvent.setup();
111
+
112
+ render(<AppointmentsTable {...props} appointments={appointments} />);
113
+
114
+ await screen.findByRole('heading', { name: /scheduled appointment/i });
115
+
116
+ const searchInput = screen.getByRole('searchbox');
117
+
118
+ await user.type(searchInput, 'John');
119
+ expect(searchInput).toHaveValue('John');
120
+ });
121
+
122
+ it('clicking the download button should download the scheduled appointments as an excel file', async () => {
123
+ const user = userEvent.setup();
124
+
125
+ render(<AppointmentsTable {...props} appointments={appointments} />);
126
+
127
+ await screen.findByRole('heading', { name: /scheduled appointment/i });
128
+ const downloadButton = screen.getByRole('button', { name: /download/i });
129
+
130
+ await user.click(downloadButton);
131
+ expect(downloadButton).toBeInTheDocument();
132
+ expect(mockedDownloadAppointmentsAsExcel).toHaveBeenCalledWith(appointments, expect.anything());
133
+ });
134
+ });
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import { Button } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { type Appointment } from '../../types';
5
+ import dayjs from 'dayjs';
6
+ import isToday from 'dayjs/plugin/isToday';
7
+ import utc from 'dayjs/plugin/utc';
8
+ import { navigate, useConfig, launchWorkspace } from '@openmrs/esm-framework';
9
+ import { type ConfigObject } from '../../config-schema';
10
+ dayjs.extend(utc);
11
+ dayjs.extend(isToday);
12
+
13
+ interface CheckInButtonProps {
14
+ patientUuid: string;
15
+ appointment: Appointment;
16
+ }
17
+
18
+ const CheckInButton: React.FC<CheckInButtonProps> = ({ appointment, patientUuid }) => {
19
+ const { checkInButton } = useConfig<ConfigObject>();
20
+ const { t } = useTranslation();
21
+ return (
22
+ <>
23
+ {checkInButton.enabled &&
24
+ (dayjs(appointment.startDateTime).isAfter(dayjs()) || dayjs(appointment.startDateTime).isToday()) && (
25
+ <Button
26
+ size="sm"
27
+ kind="tertiary"
28
+ onClick={() =>
29
+ checkInButton.customUrl
30
+ ? navigate({
31
+ to: checkInButton.customUrl,
32
+ templateParams: { patientUuid: appointment.patient.uuid, appointmentUuid: appointment.uuid },
33
+ })
34
+ : launchWorkspace('start-visit-workspace-form', { patientUuid: patientUuid, showPatientHeader: true })
35
+ }>
36
+ {t('checkIn', 'Check in')}
37
+ </Button>
38
+ )}
39
+ </>
40
+ );
41
+ };
42
+
43
+ export default CheckInButton;
@@ -0,0 +1,104 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useVisit, updateVisit, parseDate, showSnackbar } from '@openmrs/esm-framework';
4
+ import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
5
+ import { changeAppointmentStatus } from '../../patient-appointments/patient-appointments.resource';
6
+ import { useMutateAppointments } from '../../form/appointments-form.resource';
7
+
8
+ interface EndAppointmentModalProps {
9
+ patientUuid: string;
10
+ appointmentUuid: string;
11
+ closeModal: () => void;
12
+ }
13
+
14
+ const EndAppointmentModal: React.FC<EndAppointmentModalProps> = ({ patientUuid, appointmentUuid, closeModal }) => {
15
+ const { activeVisit, mutate } = useVisit(patientUuid);
16
+ const { t } = useTranslation();
17
+ const { mutateAppointments } = useMutateAppointments();
18
+
19
+ const endAppointment = () => {
20
+ return changeAppointmentStatus('Completed', appointmentUuid)
21
+ .then(() => {
22
+ mutateAppointments();
23
+ if (activeVisit) {
24
+ const abortController = new AbortController();
25
+ const endVisitPayload = {
26
+ location: activeVisit.location.uuid,
27
+ startDatetime: parseDate(activeVisit.startDatetime),
28
+ visitType: activeVisit.visitType.uuid,
29
+ stopDatetime: new Date(),
30
+ };
31
+ updateVisit(activeVisit.uuid, endVisitPayload, abortController)
32
+ .toPromise()
33
+ .then(() => {
34
+ mutate();
35
+ showSnackbar({
36
+ title: t('appointmentEnded', 'Appointment ended'),
37
+ subtitle: t(
38
+ 'appointmentEndedAndVisitClosedSuccessfully',
39
+ 'Appointment successfully ended and visit successfully closed.',
40
+ ),
41
+ isLowContrast: true,
42
+ kind: 'success',
43
+ });
44
+ closeModal();
45
+ })
46
+ .catch((err) => {
47
+ closeModal();
48
+ showSnackbar({
49
+ title: t('appointmentEndedButVisitNotClosedError', 'Appointment ended, but error closing visit'),
50
+ subtitle: err?.message,
51
+ kind: 'error',
52
+ isLowContrast: true,
53
+ });
54
+ });
55
+ } else {
56
+ closeModal();
57
+ showSnackbar({
58
+ title: t('appointmentEnded', 'Appointment ended'),
59
+ subtitle: t('appointmentEndedSuccessfully', 'Appointment successfully ended.'),
60
+ isLowContrast: true,
61
+ kind: 'success',
62
+ });
63
+ }
64
+ })
65
+ .catch((err) => {
66
+ closeModal();
67
+ showSnackbar({
68
+ title: t('appointmentEndError', 'Error ending appointment'),
69
+ subtitle: err?.message,
70
+ kind: 'error',
71
+ isLowContrast: true,
72
+ });
73
+ });
74
+ };
75
+
76
+ return (
77
+ <div>
78
+ <ModalHeader
79
+ closeModal={closeModal}
80
+ title={t('endAppointmentConfirmation', 'Are you sure you want to check the patient out for this appointment?')}
81
+ />
82
+ <ModalBody>
83
+ <p>
84
+ {activeVisit
85
+ ? t(
86
+ 'endAppointmentAndVisitConfirmationMessage',
87
+ 'Checking the patient out will mark the appointment as complete, and close out the active visit for this patient.',
88
+ )
89
+ : t('endAppointmentConfirmationMessage', 'Checking the patient out will mark the appointment as complete.')}
90
+ </p>
91
+ </ModalBody>
92
+ <ModalFooter>
93
+ <Button kind="secondary" onClick={closeModal}>
94
+ {t('cancel', 'Cancel')}
95
+ </Button>
96
+ <Button kind="danger" onClick={endAppointment}>
97
+ {t('checkOut', 'Check out')}
98
+ </Button>
99
+ </ModalFooter>
100
+ </div>
101
+ );
102
+ };
103
+
104
+ export default EndAppointmentModal;
@@ -0,0 +1,80 @@
1
+ import EndAppointmentModal from './end-appointment-modal.component';
2
+ import { render, screen } from '@testing-library/react';
3
+ import React from 'react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { updateVisit, showSnackbar, useVisit } from '@openmrs/esm-framework';
6
+ import { changeAppointmentStatus } from '../../patient-appointments/patient-appointments.resource';
7
+
8
+ const closeModal = jest.fn();
9
+ jest.mock('../../patient-appointments/patient-appointments.resource', () => ({
10
+ changeAppointmentStatus: jest.fn().mockResolvedValue({}),
11
+ }));
12
+
13
+ jest.mock('@openmrs/esm-framework', () => ({
14
+ useVisit: jest.fn(),
15
+ updateVisit: jest.fn().mockReturnValue({ toPromise: jest.fn().mockResolvedValue({}) }),
16
+ showSnackbar: jest.fn(),
17
+ parseDate: jest.fn(),
18
+ }));
19
+
20
+ describe('EndAppointmentModal', () => {
21
+ afterEach(() => {
22
+ jest.clearAllMocks();
23
+ });
24
+
25
+ it('has a cancel button that closes the model', async () => {
26
+ useVisit.mockReturnValue({});
27
+ const user = userEvent.setup();
28
+ render(<EndAppointmentModal appointmentUuid={'abc'} patientUuid={'123'} closeModal={closeModal} />);
29
+ const cancelButton = screen.getByText('Cancel');
30
+ await user.click(cancelButton);
31
+ expect(closeModal).toHaveBeenCalled();
32
+ });
33
+
34
+ it('should update appointment status but not visit on submit if no active visit', async () => {
35
+ useVisit.mockReturnValue({});
36
+ const user = userEvent.setup();
37
+
38
+ render(<EndAppointmentModal appointmentUuid={'abc'} patientUuid={'123'} closeModal={closeModal} />);
39
+
40
+ const submitButton = screen.getByRole('button', { name: /check out/i });
41
+ expect(submitButton).not.toBeDisabled();
42
+ await user.click(submitButton);
43
+
44
+ expect(changeAppointmentStatus).toHaveBeenCalledWith('Completed', 'abc');
45
+ expect(useVisit).toHaveBeenCalledWith('123');
46
+ expect(updateVisit).not.toHaveBeenCalled();
47
+ expect(closeModal).toHaveBeenCalled();
48
+ expect(showSnackbar).toHaveBeenCalledWith({
49
+ title: 'Appointment ended',
50
+ subtitle: 'Appointment successfully ended.',
51
+ isLowContrast: true,
52
+ kind: 'success',
53
+ });
54
+ });
55
+
56
+ it('should update appointment status and visit on submit if active visit', async () => {
57
+ useVisit.mockReturnValue({
58
+ mutate: jest.fn(),
59
+ activeVisit: { location: { uuid: 'def' }, visitType: { uuid: 'ghi' }, startDatetime: new Date() },
60
+ });
61
+ const user = userEvent.setup();
62
+
63
+ render(<EndAppointmentModal appointmentUuid={'abc'} patientUuid={'123'} closeModal={closeModal} />);
64
+
65
+ const submitButton = screen.getByRole('button', { name: /check out/i });
66
+ expect(submitButton).not.toBeDisabled();
67
+ await user.click(submitButton);
68
+
69
+ expect(changeAppointmentStatus).toHaveBeenCalledWith('Completed', 'abc');
70
+ expect(useVisit).toHaveBeenCalledWith('123');
71
+ expect(updateVisit).toHaveBeenCalled();
72
+ expect(closeModal).toHaveBeenCalled();
73
+ expect(showSnackbar).toHaveBeenCalledWith({
74
+ title: 'Appointment ended',
75
+ subtitle: 'Appointment successfully ended and visit successfully closed.',
76
+ isLowContrast: true,
77
+ kind: 'success',
78
+ });
79
+ });
80
+ });
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { SelectItem } from '@carbon/react';
4
+
5
+ interface SelectLocationProps {
6
+ selectedLocation: string;
7
+ defaultFacility: {
8
+ uuid: string;
9
+ display: string;
10
+ };
11
+ locations?: Array<LocationOptions>;
12
+ }
13
+
14
+ interface LocationOptions {
15
+ uuid?: string;
16
+ display?: string;
17
+ }
18
+
19
+ const LocationSelectOption: React.FC<SelectLocationProps> = ({ selectedLocation, defaultFacility, locations }) => {
20
+ const { t } = useTranslation();
21
+ if (!selectedLocation) {
22
+ return <SelectItem text={t('selectOption', 'Select an option')} value="" />;
23
+ }
24
+
25
+ if (defaultFacility && Object.keys(defaultFacility).length !== 0) {
26
+ return (
27
+ <SelectItem key={defaultFacility?.uuid} text={defaultFacility?.display} value={defaultFacility?.uuid}>
28
+ {defaultFacility?.display}
29
+ </SelectItem>
30
+ );
31
+ }
32
+
33
+ if (locations && locations.length > 0) {
34
+ return (
35
+ <>
36
+ {locations.map((location) => (
37
+ <SelectItem key={location.uuid} text={location.display} value={location.uuid}>
38
+ {location.display}
39
+ </SelectItem>
40
+ ))}
41
+ </>
42
+ );
43
+ }
44
+
45
+ return null;
46
+ };
47
+
48
+ export default LocationSelectOption;
@@ -0,0 +1,91 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import styles from './appointment-details.scss';
3
+ import { usePatientAppointmentHistory } from '../../hooks/usePatientAppointmentHistory';
4
+ import { formatDate, formatDatetime, usePatient } from '@openmrs/esm-framework';
5
+ import { getGender } from '../../helpers';
6
+ import { type Appointment } from '../../types';
7
+ import { useTranslation } from 'react-i18next';
8
+
9
+ interface AppointmentDetailsProps {
10
+ appointment: Appointment;
11
+ }
12
+
13
+ const AppointmentDetails: React.FC<AppointmentDetailsProps> = ({ appointment }) => {
14
+ const { t } = useTranslation();
15
+ const [isEnabledQuery, setIsEnabledQuery] = useState(false);
16
+ const { appointmentsCount, isLoading } = usePatientAppointmentHistory(appointment.patient.uuid);
17
+ const { patient } = usePatient(appointment.patient.uuid);
18
+
19
+ useEffect(() => {
20
+ if (!isLoading) {
21
+ setIsEnabledQuery(true);
22
+ }
23
+ }, [appointmentsCount, isLoading]);
24
+ return (
25
+ <div className={styles.appointmentDetailsContainer}>
26
+ <p className={styles.title}>{appointment.service.name}</p>
27
+ <p className={styles.subTitle}>{formatDatetime(new Date(appointment.startDateTime))}</p>
28
+
29
+ <div className={styles.patientInfoGrid}>
30
+ <div>
31
+ <p className={styles.gridTitle}>{t('patientDetails', 'Patient details')}</p>
32
+ <div className={styles.labelContainer}>
33
+ <p className={styles.labelBold}>{t('patientName', 'Patient name')}: </p>
34
+ <p className={styles.label}>{appointment.patient.name}</p>
35
+ </div>
36
+ <div className={styles.labelContainer}>
37
+ <p className={styles.labelBold}>{t('age', 'Age')}: </p>
38
+ <p className={styles.label}>{appointment.patient.age}</p>
39
+ </div>
40
+ <div className={styles.labelContainer}>
41
+ <p className={styles.labelBold}>{t('gender', 'Gender')}: </p>
42
+ <p className={styles.label}>{getGender(appointment.patient.gender, t)}</p>
43
+ </div>
44
+ {patient && patient?.birthDate ? (
45
+ <div className={styles.labelContainer}>
46
+ <p className={styles.labelBold}>{t('dateOfBirth', 'Date of birth')}: </p>
47
+ <p className={styles.label}>{formatDate(new Date(patient.birthDate))}</p>
48
+ </div>
49
+ ) : (
50
+ ''
51
+ )}
52
+ {patient && patient?.telecom
53
+ ? patient.telecom.map((contact, i) => (
54
+ <div className={styles.labelContainer}>
55
+ <p className={styles.labelBold}>{t('Contact', 'Contact {{index}}', { index: i + 1 })}: </p>
56
+ <p className={styles.label}>{contact.value}</p>
57
+ </div>
58
+ ))
59
+ : ''}
60
+ </div>
61
+ <div>
62
+ <p className={styles.gridTitle}>{t('appointmentNotes', 'Appointment Notes')}</p>
63
+ <p className={styles.label}>{appointment.comments}</p>
64
+ </div>
65
+ <div>
66
+ <p className={styles.gridTitle}>{t('appointmentHistory', 'Appointment History')}</p>
67
+ <div className={styles.historyGrid}>
68
+ <div>
69
+ <p className={styles.historyGridLabel}>{t('completed', 'Completed')}</p>
70
+ <span className={styles.historyGridCount}>{appointmentsCount.completedAppointments}</span>
71
+ </div>
72
+ <div>
73
+ <p className={styles.historyGridLabel}>{t('missed', 'Missed')}</p>
74
+ <span className={styles.historyGridCountRed}>{appointmentsCount.missedAppointments}</span>
75
+ </div>
76
+ <div>
77
+ <p className={styles.historyGridLabel}>{t('cancelled', 'Cancelled')}</p>
78
+ <span className={styles.historyGridCount}>{appointmentsCount.cancelledAppointments}</span>
79
+ </div>
80
+ <div>
81
+ <p className={styles.historyGridLabel}>{t('upcoming', 'Upcoming')}</p>
82
+ <span className={styles.historyGridCount}>{appointmentsCount.upcomingAppointments}</span>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ );
89
+ };
90
+
91
+ export default AppointmentDetails;
@@ -0,0 +1,81 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @use '@carbon/colors';
4
+
5
+ .appointmentDetailsContainer {
6
+ min-height: fit-content;
7
+ background-color: colors.$white;
8
+ padding: spacing.$spacing-05;
9
+ margin: spacing.$spacing-03;
10
+ }
11
+
12
+ .title {
13
+ @include type.type-style('heading-compact-01');
14
+ color: colors.$gray-90;
15
+ }
16
+
17
+ .subTitle {
18
+ @include type.type-style('label-01');
19
+ color: colors.$gray-70;
20
+ }
21
+
22
+ .tags {
23
+ margin-top: spacing.$spacing-05;
24
+ display: flex;
25
+ column-gap: spacing.$spacing-03;
26
+
27
+ & > div {
28
+ margin: 0;
29
+ }
30
+ }
31
+
32
+ .patientInfoGrid {
33
+ display: grid;
34
+ grid-template-columns: 1fr 1fr 1fr;
35
+ column-gap: spacing.$spacing-05;
36
+ margin-top: spacing.$spacing-05;
37
+ }
38
+
39
+ .gridTitle {
40
+ @include type.type-style('label-02');
41
+ margin-bottom: spacing.$spacing-04;
42
+ color: colors.$gray-70;
43
+ }
44
+
45
+ .label {
46
+ @include type.type-style('label-01');
47
+ color: colors.$gray-70;
48
+ }
49
+
50
+ .historyGrid {
51
+ display: grid;
52
+ grid-template-columns: 1fr 1fr;
53
+ row-gap: spacing.$spacing-05;
54
+ }
55
+
56
+ .historyGridLabel {
57
+ @include type.type-style('legal-01');
58
+ color: colors.$gray-70;
59
+ }
60
+ .historyGridCount {
61
+ @include type.type-style('heading-02');
62
+ margin-top: spacing.$spacing-01;
63
+ color: colors.$blue-60;
64
+ }
65
+
66
+ .historyGridCountRed {
67
+ @include type.type-style('heading-02');
68
+ margin-top: spacing.$spacing-01;
69
+ color: colors.$red-60;
70
+ }
71
+
72
+ .labelBold {
73
+ @include type.type-style('label-01');
74
+ color: colors.$gray-70;
75
+ font-weight: bold;
76
+ margin-right: spacing.$spacing-02;
77
+ }
78
+
79
+ .labelContainer {
80
+ display: flex;
81
+ }
@@ -0,0 +1,103 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import AppointmentDetails from './appointment-details.component';
4
+ import { type Appointment } from '../../types';
5
+
6
+ const appointment: Appointment = {
7
+ uuid: '7cd38a6d-377e-491b-8284-b04cf8b8c6d8',
8
+ appointmentNumber: '0000',
9
+ patient: {
10
+ identifier: '100GEJ',
11
+ identifiers: [],
12
+ name: 'John Wilson',
13
+ uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e',
14
+ gender: 'M',
15
+ age: '34',
16
+ },
17
+ service: {
18
+ appointmentServiceId: 1,
19
+ name: 'Outpatient',
20
+ description: null,
21
+ startTime: '',
22
+ endTime: '',
23
+ maxAppointmentsLimit: null,
24
+ durationMins: null,
25
+ location: {
26
+ uuid: '8d6c993e-c2cc-11de-8d13-0010c6dffd0f',
27
+ },
28
+ uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90',
29
+ initialAppointmentStatus: 'Scheduled',
30
+ creatorName: null,
31
+ },
32
+ provider: {
33
+ uuid: 'f9badd80-ab76-11e2-9e96-0800200c9a66',
34
+ person: { uuid: '24252571-dd5a-11e6-9d9c-0242ac150002', display: 'Dr James Cook' },
35
+ },
36
+ location: { name: 'HIV Clinic', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' },
37
+ startDateTime: 1630326900000,
38
+ endDateTime: 1630327200000,
39
+ appointmentKind: 'WalkIn',
40
+ status: 'Scheduled',
41
+ comments: 'Some comments',
42
+ additionalInfo: null,
43
+ providers: [{ uuid: '24252571-dd5a-11e6-9d9c-0242ac150002', display: 'Dr James Cook' }],
44
+ recurring: false,
45
+ voided: false,
46
+ teleconsultationLink: null,
47
+ extensions: [],
48
+ };
49
+
50
+ jest.mock('../../hooks/usePatientAppointmentHistory', () => ({
51
+ usePatientAppointmentHistory: () => ({
52
+ appointmentsCount: {
53
+ completedAppointments: 1,
54
+ missedAppointments: 2,
55
+ cancelledAppointments: 3,
56
+ upcomingAppointments: 4,
57
+ },
58
+ }),
59
+ }));
60
+
61
+ jest.mock('@openmrs/esm-framework', () => {
62
+ const originalModule = jest.requireActual('@openmrs/esm-framework');
63
+ return {
64
+ ...originalModule,
65
+ usePatient: jest.fn().mockImplementation((...args) => ({
66
+ patient: {
67
+ birthDate: '22-Mar-2020',
68
+ telecom: [
69
+ {
70
+ uuid: 'tel-uuid-1',
71
+ value: '0899129989932',
72
+ },
73
+ ],
74
+ },
75
+ })),
76
+ };
77
+ });
78
+
79
+ test('renders appointment details correctly', async () => {
80
+ const { getByText } = render(<AppointmentDetails appointment={appointment} />);
81
+ expect(getByText(/Patient name/i)).toBeInTheDocument();
82
+ expect(getByText(/John Wilson/i)).toBeInTheDocument();
83
+ expect(getByText(/Age/i)).toBeInTheDocument();
84
+ expect(getByText(/34/i)).toBeInTheDocument();
85
+ expect(getByText(/Gender/i)).toBeInTheDocument();
86
+ expect(getByText(/Male/i)).toBeInTheDocument();
87
+ expect(getByText(/Date of birth/i)).toBeInTheDocument();
88
+ expect(getByText(/Date of birth/i)).toBeInTheDocument();
89
+ expect(getByText(/22-Mar-2020/i)).toBeInTheDocument();
90
+ expect(getByText(/Contact 1/i)).toBeInTheDocument();
91
+ expect(getByText(/0899129989932/i)).toBeInTheDocument();
92
+ expect(getByText(/Appointment Notes/i)).toBeInTheDocument();
93
+ expect(getByText(/Some comments/i)).toBeInTheDocument();
94
+ expect(getByText(/Appointment History/i)).toBeInTheDocument();
95
+ expect(getByText(/Completed/i)).toBeInTheDocument();
96
+ expect(getByText('1', { exact: true })).toBeInTheDocument();
97
+ expect(getByText(/Missed/i)).toBeInTheDocument();
98
+ expect(getByText('2', { exact: true })).toBeInTheDocument();
99
+ expect(getByText(/Cancelled/i)).toBeInTheDocument();
100
+ expect(getByText('3', { exact: true })).toBeInTheDocument();
101
+ expect(getByText(/Upcoming/i)).toBeInTheDocument();
102
+ expect(getByText('4', { exact: true })).toBeInTheDocument();
103
+ });