@kenyaemr/esm-appointments-app 8.1.1-pre.129 → 8.1.2-pre.152

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 (119) hide show
  1. package/.turbo/turbo-build.log +27 -25
  2. package/dist/111.js +2 -0
  3. package/dist/111.js.map +1 -0
  4. package/dist/123.js +2 -0
  5. package/dist/{198.js.LICENSE.txt → 123.js.LICENSE.txt} +0 -6
  6. package/dist/123.js.map +1 -0
  7. package/dist/130.js +1 -1
  8. package/dist/130.js.map +1 -1
  9. package/dist/{787.js → 139.js} +1 -1
  10. package/dist/139.js.map +1 -0
  11. package/dist/228.js +1 -0
  12. package/dist/236.js +1 -0
  13. package/dist/240.js +1 -0
  14. package/dist/261.js +1 -0
  15. package/dist/269.js +1 -1
  16. package/dist/269.js.map +1 -1
  17. package/dist/271.js +1 -1
  18. package/dist/272.js +1 -0
  19. package/dist/319.js +1 -1
  20. package/dist/336.js +1 -0
  21. package/dist/378.js +1 -0
  22. package/dist/443.js +1 -0
  23. package/dist/443.js.map +1 -0
  24. package/dist/460.js +1 -1
  25. package/dist/529.js +1 -1
  26. package/dist/529.js.map +1 -1
  27. package/dist/539.js +1 -0
  28. package/dist/566.js +1 -0
  29. package/dist/574.js +1 -1
  30. package/dist/581.js +1 -1
  31. package/dist/644.js +1 -1
  32. package/dist/652.js +1 -0
  33. package/dist/673.js +1 -0
  34. package/dist/705.js +1 -0
  35. package/dist/711.js +1 -1
  36. package/dist/711.js.map +1 -1
  37. package/dist/727.js +1 -0
  38. package/dist/737.js +1 -0
  39. package/dist/744.js +1 -0
  40. package/dist/757.js +1 -1
  41. package/dist/788.js +1 -1
  42. package/dist/807.js +1 -1
  43. package/dist/833.js +1 -1
  44. package/dist/899.js +1 -0
  45. package/dist/923.js +1 -0
  46. package/dist/923.js.map +1 -0
  47. package/dist/kenyaemr-esm-appointments-app.js +1 -1
  48. package/dist/kenyaemr-esm-appointments-app.js.buildmanifest.json +454 -102
  49. package/dist/kenyaemr-esm-appointments-app.js.map +1 -1
  50. package/dist/main.js +1 -1
  51. package/dist/main.js.map +1 -1
  52. package/dist/routes.json +1 -1
  53. package/package-lock.json +6416 -0
  54. package/package.json +5 -5
  55. package/src/appointments/appointment-tabs.component.tsx +1 -1
  56. package/src/appointments/common-components/appointments-table.component.tsx +0 -1
  57. package/src/appointments/common-components/appointments-table.test.tsx +7 -3
  58. package/src/appointments/common-components/checkin-button.component.tsx +5 -1
  59. package/src/appointments/common-components/end-appointment.modal.tsx +2 -2
  60. package/src/appointments/scheduled/scheduled-appointments.component.tsx +1 -1
  61. package/src/appointments/unscheduled/unscheduled-appointments.component.tsx +6 -6
  62. package/src/appointments/unscheduled/unscheduled-appointments.test.tsx +19 -4
  63. package/src/appointments.component.tsx +7 -7
  64. package/src/appointments.test.tsx +11 -1
  65. package/src/calendar/appointments-calendar-view.component.tsx +2 -2
  66. package/src/calendar/monthly/monthly-workload-view.component.tsx +7 -7
  67. package/src/config-schema.ts +6 -70
  68. package/src/form/appointments-form.component.tsx +84 -73
  69. package/src/form/appointments-form.resource.ts +4 -4
  70. package/src/form/appointments-form.scss +1 -1
  71. package/src/form/appointments-form.test.tsx +142 -110
  72. package/src/patient-appointments/patient-appointments-action-menu.component.tsx +2 -4
  73. package/src/patient-appointments/patient-appointments-base.test.tsx +1 -1
  74. package/src/patient-appointments/patient-appointments-cancel.modal.tsx +6 -1
  75. package/src/patient-appointments/patient-appointments-cancel.scss +29 -0
  76. package/src/patient-appointments/patient-upcoming-appointments-card.component.tsx +64 -8
  77. package/src/routes.json +1 -1
  78. package/src/types/index.ts +1 -1
  79. package/src/workload/monthly-view-workload/monthly-view.component.tsx +8 -6
  80. package/src/workload/monthly-view-workload/monthlyWorkCard.tsx +3 -2
  81. package/src/workload/workload-card.component.tsx +5 -3
  82. package/src/workload/workload.component.tsx +13 -20
  83. package/src/workload/workload.resource.ts +3 -0
  84. package/translations/am.json +4 -2
  85. package/translations/ar.json +24 -22
  86. package/translations/de.json +168 -0
  87. package/translations/en.json +3 -1
  88. package/translations/es.json +58 -56
  89. package/translations/fr.json +4 -2
  90. package/translations/he.json +93 -91
  91. package/translations/hi.json +168 -0
  92. package/translations/hi_IN.json +168 -0
  93. package/translations/id.json +168 -0
  94. package/translations/it.json +168 -0
  95. package/translations/km.json +4 -2
  96. package/translations/ne.json +168 -0
  97. package/translations/pt.json +168 -0
  98. package/translations/pt_BR.json +168 -0
  99. package/translations/qu.json +168 -0
  100. package/translations/si.json +168 -0
  101. package/translations/sw.json +168 -0
  102. package/translations/sw_KE.json +168 -0
  103. package/translations/tr.json +168 -0
  104. package/translations/tr_TR.json +168 -0
  105. package/translations/uk.json +168 -0
  106. package/translations/vi.json +168 -0
  107. package/translations/zh.json +4 -2
  108. package/translations/zh_CN.json +4 -2
  109. package/dist/198.js +0 -2
  110. package/dist/198.js.map +0 -1
  111. package/dist/265.js +0 -1
  112. package/dist/265.js.map +0 -1
  113. package/dist/440.js +0 -2
  114. package/dist/440.js.map +0 -1
  115. package/dist/501.js +0 -1
  116. package/dist/501.js.map +0 -1
  117. package/dist/787.js.map +0 -1
  118. package/src/hooks/useDefaultLocation.ts +0 -14
  119. /package/dist/{440.js.LICENSE.txt → 111.js.LICENSE.txt} +0 -0
@@ -5,14 +5,16 @@ import {
5
5
  type FetchResponse,
6
6
  getDefaultsFromConfigSchema,
7
7
  openmrsFetch,
8
+ showSnackbar,
8
9
  useConfig,
9
10
  useLocations,
10
11
  useSession,
11
12
  } from '@openmrs/esm-framework';
12
- import { mockUseAppointmentServiceData, mockSession, mockLocations } from '__mocks__';
13
+ import { configSchema, type ConfigObject } from '../config-schema';
14
+ import { mockUseAppointmentServiceData, mockSession, mockLocations, mockProviders } from '__mocks__';
13
15
  import { mockPatient, renderWithSwr, waitForLoadingToFinish } from 'tools';
14
16
  import { saveAppointment } from './appointments-form.resource';
15
- import { configSchema, type ConfigObject } from '../config-schema';
17
+ import { useProviders } from '../hooks/useProviders';
16
18
  import AppointmentForm from './appointments-form.component';
17
19
 
18
20
  const defaultProps = {
@@ -20,79 +22,40 @@ const defaultProps = {
20
22
  closeWorkspace: jest.fn(),
21
23
  patientUuid: mockPatient.id,
22
24
  promptBeforeClosing: jest.fn(),
25
+ closeWorkspaceWithSavedChanges: jest.fn(),
26
+ setTitle: jest.fn(),
23
27
  };
24
28
 
25
- const mockCreateAppointment = jest.mocked(saveAppointment);
26
29
  const mockOpenmrsFetch = jest.mocked(openmrsFetch);
30
+ const mockSaveAppointment = jest.mocked(saveAppointment);
31
+ const mockShowSnackbar = jest.mocked(showSnackbar);
27
32
  const mockUseConfig = jest.mocked(useConfig<ConfigObject>);
28
33
  const mockUseLocations = jest.mocked(useLocations);
34
+ const mockUseProviders = jest.mocked(useProviders);
29
35
  const mockUseSession = jest.mocked(useSession);
30
36
 
31
- jest.mock('react-hook-form', () => ({
32
- ...jest.requireActual('react-hook-form'),
33
- useForm: jest.fn().mockImplementation(() => ({
34
- handleSubmit: () => jest.fn(),
35
- control: {
36
- register: jest.fn(),
37
- unregister: jest.fn(),
38
- getFieldState: jest.fn(),
39
- _names: {
40
- array: new Set('test'),
41
- mount: new Set('test'),
42
- unMount: new Set('test'),
43
- watch: new Set('test'),
44
- focus: 'test',
45
- watchAll: false,
46
- },
47
- _subjects: {
48
- watch: jest.fn(),
49
- array: jest.fn(),
50
- state: jest.fn(),
51
- },
52
- _getWatch: jest.fn(),
53
- _formValues: [],
54
- _defaultValues: [],
55
- },
56
- getValues: (str) => {
57
- if (str === 'recurringPatternDaysOfWeek') {
58
- return [];
59
- } else {
60
- return null;
61
- }
62
- },
63
- setValue: () => jest.fn(),
64
- formState: () => jest.fn(),
65
- watch: () => jest.fn(),
66
- })),
67
- Controller: ({ render }) =>
68
- render({
69
- field: {
70
- onChange: jest.fn(),
71
- onBlur: jest.fn(),
72
- value: '',
73
- ref: jest.fn(),
74
- },
75
- formState: {
76
- isSubmitted: false,
77
- },
78
- fieldState: {
79
- isTouched: false,
80
- },
81
- }),
82
- useSubscribe: () => ({
83
- r: { current: { subject: { subscribe: () => jest.fn() } } },
84
- }),
85
- useController: () => ({
86
- field: { ref: jest.fn() },
87
- }),
88
- }));
89
-
90
37
  jest.mock('./appointments-form.resource', () => ({
91
38
  ...jest.requireActual('./appointments-form.resource'),
92
39
  saveAppointment: jest.fn(),
93
40
  }));
94
41
 
42
+ jest.mock('../hooks/useProviders', () => ({
43
+ ...jest.requireActual('../hooks/useProviders'),
44
+ useProviders: jest.fn(),
45
+ }));
46
+
47
+ jest.mock('../workload/workload.resource', () => ({
48
+ ...jest.requireActual('../workload/workload.resource'),
49
+ getMonthlyCalendarDistribution: jest.fn(),
50
+ useAppointmentSummary: jest.fn(),
51
+ useCalendarDistribution: jest.fn(),
52
+ useMonthlyCalendarDistribution: jest.fn().mockReturnValue([]),
53
+ useMonthlyAppointmentSummary: jest.fn().mockReturnValue([]),
54
+ }));
55
+
95
56
  describe('AppointmentForm', () => {
57
+ const dateTimeRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3}Z|\+00:00)$/;
58
+
96
59
  beforeEach(() => {
97
60
  mockUseConfig.mockReturnValue({
98
61
  ...getDefaultsFromConfigSchema(configSchema),
@@ -100,36 +63,42 @@ describe('AppointmentForm', () => {
100
63
  });
101
64
  mockUseLocations.mockReturnValue(mockLocations.data.results);
102
65
  mockUseSession.mockReturnValue(mockSession.data);
66
+ mockUseProviders.mockReturnValue({
67
+ providers: mockProviders.data,
68
+ isLoading: false,
69
+ error: null,
70
+ isValidating: false,
71
+ });
103
72
  });
104
73
 
105
- it('renders the appointments form showing all the relevant fields and values', async () => {
74
+ it('renders the appointments form', async () => {
106
75
  mockOpenmrsFetch.mockResolvedValue(mockUseAppointmentServiceData as unknown as FetchResponse);
107
76
 
108
77
  renderWithSwr(<AppointmentForm {...defaultProps} />);
109
78
 
110
79
  await waitForLoadingToFinish();
111
80
 
112
- expect(screen.getByLabelText(/Select a location/i)).toBeInTheDocument();
113
- expect(screen.getByLabelText(/^Date$/i)).toBeInTheDocument();
114
- expect(screen.getByLabelText(/Select a service/i)).toBeInTheDocument();
115
- expect(screen.getByLabelText(/Select the type of appointment/i)).toBeInTheDocument();
116
- expect(screen.getByLabelText(/Write an additional note/i)).toBeInTheDocument();
117
- expect(screen.getByPlaceholderText(/Write any additional points here/i)).toBeInTheDocument();
81
+ expect(screen.getByLabelText(/select a location/i)).toBeInTheDocument();
82
+ expect(screen.getByLabelText(/select a service/i)).toBeInTheDocument();
83
+ expect(screen.getByLabelText(/select the type of appointment/i)).toBeInTheDocument();
84
+ expect(screen.getByLabelText(/write an additional note/i)).toBeInTheDocument();
85
+ expect(screen.getByPlaceholderText(/write any additional points here/i)).toBeInTheDocument();
118
86
  expect(screen.getAllByPlaceholderText(/dd\/mm\/yyyy/i).length).toBe(2);
119
- expect(screen.getByRole('option', { name: /Mosoriot/i })).toBeInTheDocument();
120
- expect(screen.getByRole('option', { name: /Inpatient Ward/i })).toBeInTheDocument();
121
- expect(screen.getByRole('option', { name: /AM/i })).toBeInTheDocument();
122
- expect(screen.getByRole('option', { name: /PM/i })).toBeInTheDocument();
123
- expect(screen.getByRole('option', { name: /Choose appointment type/i })).toBeInTheDocument();
124
- expect(screen.getByRole('option', { name: /Scheduled/i })).toBeInTheDocument();
125
- expect(screen.getByRole('option', { name: /WalkIn/i })).toBeInTheDocument();
126
- expect(screen.getByRole('textbox', { name: /^Date$/i })).toBeInTheDocument();
127
- expect(screen.getByRole('textbox', { name: /Time/i })).toBeInTheDocument();
128
- expect(screen.getByRole('button', { name: /Discard/i })).toBeInTheDocument();
129
- expect(screen.getByRole('button', { name: /Save and close/i })).toBeInTheDocument();
87
+ expect(screen.getByRole('option', { name: /mosoriot/i })).toBeInTheDocument();
88
+ expect(screen.getByRole('option', { name: /inpatient ward/i })).toBeInTheDocument();
89
+ expect(screen.getByLabelText(/^date$/i)).toBeInTheDocument();
90
+ expect(screen.getByRole('option', { name: /^am$/i })).toBeInTheDocument();
91
+ expect(screen.getByRole('option', { name: /^pm$/i })).toBeInTheDocument();
92
+ expect(screen.getByRole('option', { name: /choose appointment type/i })).toBeInTheDocument();
93
+ expect(screen.getByRole('option', { name: /scheduled/i })).toBeInTheDocument();
94
+ expect(screen.getByRole('option', { name: /walkin/i })).toBeInTheDocument();
95
+ expect(screen.getByRole('textbox', { name: /^date$/i })).toBeInTheDocument();
96
+ expect(screen.getByRole('textbox', { name: /time/i })).toBeInTheDocument();
97
+ expect(screen.getByRole('button', { name: /discard/i })).toBeInTheDocument();
98
+ expect(screen.getByRole('button', { name: /save and close/i })).toBeInTheDocument();
130
99
  });
131
100
 
132
- it('closes the form and the workspace when the cancel button is clicked', async () => {
101
+ it('closes the workspace when the cancel button is clicked', async () => {
133
102
  const user = userEvent.setup();
134
103
 
135
104
  mockOpenmrsFetch.mockResolvedValueOnce(mockUseAppointmentServiceData as unknown as FetchResponse);
@@ -144,34 +113,64 @@ describe('AppointmentForm', () => {
144
113
  expect(defaultProps.closeWorkspace).toHaveBeenCalledTimes(1);
145
114
  });
146
115
 
147
- it('renders a success snackbar upon successfully scheduling an appointment', async () => {
116
+ it('renders a success snackbar upon successfully scheduling an appointment', async () => {
148
117
  const user = userEvent.setup();
149
118
 
150
119
  mockOpenmrsFetch.mockResolvedValue({ data: mockUseAppointmentServiceData } as unknown as FetchResponse);
151
- mockCreateAppointment.mockResolvedValue({ status: 200, statusText: 'Ok' } as FetchResponse);
120
+ mockSaveAppointment.mockResolvedValue({ status: 200, statusText: 'Ok' } as FetchResponse);
152
121
 
153
122
  renderWithSwr(<AppointmentForm {...defaultProps} />);
154
123
 
155
124
  await waitForLoadingToFinish();
156
125
 
157
- const saveButton = screen.getByRole('button', { name: /Save and close/i });
158
- const dateInput = screen.getByRole('textbox', { name: /^Date$/i });
159
- const timeInput = screen.getByRole('textbox', { name: /Time/i });
160
- const timeFormat = screen.getByRole('combobox', { name: /Time/i });
161
- const serviceSelect = screen.getByRole('combobox', { name: /Select a service/i });
162
- const appointmentTypeSelect = screen.getByRole('combobox', { name: /Select the type of appointment/i });
163
-
164
- expect(saveButton).toBeEnabled();
126
+ const locationSelect = screen.getByRole('combobox', { name: /select a location/i });
127
+ const serviceSelect = screen.getByRole('combobox', { name: /select a service/i });
128
+ const appointmentTypeSelect = screen.getByRole('combobox', { name: /select the type of appointment/i });
129
+ const providerSelect = screen.getByRole('combobox', { name: /select a provider/i });
130
+ const dateInput = screen.getByRole('textbox', { name: /^date$/i });
131
+ const timeInput = screen.getByRole('textbox', { name: /time/i });
132
+ const timeFormat = screen.getByRole('combobox', { name: /time/i });
133
+ const saveButton = screen.getByRole('button', { name: /save and close/i });
165
134
 
166
- await user.clear(dateInput);
167
- await user.type(dateInput, '4/4/2021');
168
- await user.clear(timeInput);
169
- await user.type(timeInput, '09:30');
170
- await user.selectOptions(timeFormat, 'AM');
135
+ await user.selectOptions(locationSelect, ['Inpatient Ward']);
171
136
  await user.selectOptions(serviceSelect, ['Outpatient']);
172
137
  await user.selectOptions(appointmentTypeSelect, ['Scheduled']);
138
+ await user.selectOptions(providerSelect, ['doctor - James Cook']);
139
+
140
+ const date = '4/4/2021';
141
+ const time = '09:30';
173
142
 
143
+ await user.type(dateInput, date);
144
+ await user.type(timeInput, time);
145
+ await user.tab();
146
+ await user.selectOptions(timeFormat, 'AM');
174
147
  await user.click(saveButton);
148
+
149
+ expect(mockSaveAppointment).toHaveBeenCalledTimes(1);
150
+ expect(mockSaveAppointment).toHaveBeenCalledWith(
151
+ {
152
+ appointmentKind: 'Scheduled',
153
+ comments: '',
154
+ dateAppointmentScheduled: expect.stringMatching(dateTimeRegex),
155
+ endDateTime: expect.stringMatching(dateTimeRegex),
156
+ locationUuid: 'b1a8b05e-3542-4037-bbd3-998ee9c40574',
157
+ patientUuid: mockPatient.id,
158
+ providers: [{ uuid: 'f9badd80-ab76-11e2-9e96-0800200c9a66' }],
159
+ serviceUuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90',
160
+ startDateTime: expect.stringMatching(dateTimeRegex),
161
+ status: '',
162
+ uuid: undefined,
163
+ },
164
+ new AbortController(),
165
+ );
166
+
167
+ expect(mockShowSnackbar).toHaveBeenCalledTimes(1);
168
+ expect(mockShowSnackbar).toHaveBeenCalledWith({
169
+ kind: 'success',
170
+ isLowContrast: true,
171
+ subtitle: 'It is now visible on the Appointments page',
172
+ title: 'Appointment scheduled',
173
+ });
175
174
  });
176
175
 
177
176
  it('renders an error snackbar if there was a problem scheduling an appointment', async () => {
@@ -185,27 +184,60 @@ describe('AppointmentForm', () => {
185
184
  },
186
185
  };
187
186
 
188
- mockOpenmrsFetch.mockResolvedValueOnce({ data: mockUseAppointmentServiceData } as unknown as FetchResponse);
189
- mockCreateAppointment.mockRejectedValueOnce(error);
187
+ mockOpenmrsFetch.mockResolvedValue({ data: mockUseAppointmentServiceData } as unknown as FetchResponse);
188
+ mockSaveAppointment.mockRejectedValue(error);
190
189
 
191
190
  renderWithSwr(<AppointmentForm {...defaultProps} />);
192
191
 
193
192
  await waitForLoadingToFinish();
194
193
 
195
- const saveButton = screen.getByRole('button', { name: /Save and close/i });
196
- const dateInput = screen.getByRole('textbox', { name: /^Date$/i });
197
- const timeInput = screen.getByRole('textbox', { name: /Time/i });
198
- const timeFormat = screen.getByRole('combobox', { name: /Time/i });
199
- const serviceSelect = screen.getByRole('combobox', { name: /Select a service/i });
200
- const appointmentTypeSelect = screen.getByRole('combobox', { name: /Select the type of appointment/i });
201
-
202
- await user.clear(dateInput);
203
- await user.type(dateInput, '4/4/2021');
204
- await user.clear(timeInput);
205
- await user.type(timeInput, '09:30');
206
- await user.selectOptions(timeFormat, 'AM');
194
+ const locationSelect = screen.getByRole('combobox', { name: /select a location/i });
195
+ const serviceSelect = screen.getByRole('combobox', { name: /select a service/i });
196
+ const appointmentTypeSelect = screen.getByRole('combobox', { name: /select the type of appointment/i });
197
+ const providerSelect = screen.getByRole('combobox', { name: /select a provider/i });
198
+ const dateInput = screen.getByRole('textbox', { name: /^date$/i });
199
+ const timeInput = screen.getByRole('textbox', { name: /time/i });
200
+ const timeFormat = screen.getByRole('combobox', { name: /time/i });
201
+ const saveButton = screen.getByRole('button', { name: /save and close/i });
202
+
203
+ await user.selectOptions(locationSelect, ['Inpatient Ward']);
207
204
  await user.selectOptions(serviceSelect, ['Outpatient']);
208
205
  await user.selectOptions(appointmentTypeSelect, ['Scheduled']);
206
+ await user.selectOptions(providerSelect, ['doctor - James Cook']);
207
+
208
+ const date = '4/4/2021';
209
+ const time = '09:30';
210
+
211
+ await user.type(dateInput, date);
212
+ await user.type(timeInput, time);
213
+ await user.tab();
214
+ await user.selectOptions(timeFormat, 'AM');
209
215
  await user.click(saveButton);
216
+
217
+ expect(mockSaveAppointment).toHaveBeenCalledTimes(1);
218
+ expect(mockSaveAppointment).toHaveBeenCalledWith(
219
+ {
220
+ appointmentKind: 'Scheduled',
221
+ comments: '',
222
+ dateAppointmentScheduled: expect.stringMatching(dateTimeRegex),
223
+ endDateTime: expect.stringMatching(dateTimeRegex),
224
+ locationUuid: 'b1a8b05e-3542-4037-bbd3-998ee9c40574',
225
+ patientUuid: mockPatient.id,
226
+ providers: [{ uuid: 'f9badd80-ab76-11e2-9e96-0800200c9a66' }],
227
+ serviceUuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90',
228
+ startDateTime: expect.stringMatching(dateTimeRegex),
229
+ status: '',
230
+ uuid: undefined,
231
+ },
232
+ new AbortController(),
233
+ );
234
+
235
+ expect(mockShowSnackbar).toHaveBeenCalledTimes(1);
236
+ expect(mockShowSnackbar).toHaveBeenCalledWith({
237
+ isLowContrast: false,
238
+ kind: 'error',
239
+ subtitle: 'Internal Server Error',
240
+ title: 'Error scheduling appointment',
241
+ });
210
242
  });
211
243
  });
@@ -1,13 +1,11 @@
1
1
  import React, { useCallback, useContext } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
-
4
3
  import { Layer, OverflowMenu, OverflowMenuItem } from '@carbon/react';
5
4
  import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';
6
5
  import { launchWorkspace, showModal, useLayoutType } from '@openmrs/esm-framework';
7
6
  import type { Appointment } from '../types';
8
- import styles from './patient-appointments-action-menu.scss';
9
-
10
7
  import PatientAppointmentContext, { PatientAppointmentContextTypes } from '../hooks/patientAppointmentContext';
8
+ import styles from './patient-appointments-action-menu.scss';
11
9
 
12
10
  interface appointmentsActionMenuProps {
13
11
  appointment: Appointment;
@@ -32,7 +30,7 @@ export const PatientAppointmentsActionMenu = ({ appointment, patientUuid }: appo
32
30
  appointment,
33
31
  });
34
32
  }
35
- }, [appointment, t]);
33
+ }, [appointment, patientAppointmentContext, t]);
36
34
 
37
35
  const launchCancelAppointmentDialog = () => {
38
36
  const dispose = showModal('patient-appointment-cancel-confirmation-dialog', {
@@ -14,7 +14,7 @@ const testProps = {
14
14
 
15
15
  const mockOpenmrsFetch = jest.mocked(openmrsFetch);
16
16
 
17
- describe('AppointmensOverview', () => {
17
+ describe('AppointmentsOverview', () => {
18
18
  it('renders an empty state if appointments data is unavailable', async () => {
19
19
  mockOpenmrsFetch.mockResolvedValueOnce({
20
20
  data: [],
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
3
3
  import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
4
4
  import { showSnackbar } from '@openmrs/esm-framework';
5
5
  import { changeAppointmentStatus, usePatientAppointments } from './patient-appointments.resource';
6
+ import styles from './patient-appointments-cancel.scss';
6
7
 
7
8
  interface PatientCancelAppointmentModalProps {
8
9
  closeCancelModal: () => void;
@@ -47,7 +48,11 @@ const PatientCancelAppointmentModal: React.FC<PatientCancelAppointmentModalProps
47
48
 
48
49
  return (
49
50
  <div>
50
- <ModalHeader closeModal={closeCancelModal} title={t('cancelAppointment', 'Cancel appointment')} />
51
+ <ModalHeader
52
+ className={styles.modalHeader}
53
+ closeModal={closeCancelModal}
54
+ title={t('cancelAppointment', 'Cancel appointment')}
55
+ />
51
56
  <ModalBody>
52
57
  <p>{t('cancelAppointmentModalConfirmationText', 'Are you sure you want to cancel this appointment?')}</p>
53
58
  </ModalBody>
@@ -0,0 +1,29 @@
1
+ @use '@carbon/layout';
2
+
3
+ .modalHeader {
4
+ :global {
5
+ .cds--modal-close-button {
6
+ position: absolute;
7
+ inset-block-start: 0;
8
+ inset-inline-end: 0;
9
+ margin: 0;
10
+ margin-top: calc(-1 * #{layout.$spacing-05});
11
+ }
12
+
13
+ .cds--modal-close {
14
+ background-color: rgba(0, 0, 0, 0);
15
+
16
+ &:hover {
17
+ background-color: var(--cds-layer-hover);
18
+ }
19
+ }
20
+
21
+ .cds--popover--left > .cds--popover > .cds--popover-content {
22
+ transform: translate(-4rem, 0.85rem);
23
+ }
24
+
25
+ .cds--popover--left > .cds--popover > .cds--popover-caret {
26
+ transform: translate(-3.75rem, 1.25rem);
27
+ }
28
+ }
29
+ }
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useMemo, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import {
4
4
  InlineLoading,
@@ -10,31 +10,84 @@ import {
10
10
  StructuredListRow,
11
11
  StructuredListWrapper,
12
12
  } from '@carbon/react';
13
- import { formatDate, parseDate } from '@openmrs/esm-framework';
14
- import { usePatientAppointments } from './patient-appointments.resource';
13
+ import { formatDate, parseDate, showSnackbar, type Visit } from '@openmrs/esm-framework';
14
+ import { changeAppointmentStatus, usePatientAppointments } from './patient-appointments.resource';
15
15
  import { ErrorState } from '@openmrs/esm-patient-common-lib';
16
16
  import styles from './patient-upcoming-appointments-card.scss';
17
17
  import dayjs from 'dayjs';
18
18
  import { type Appointment } from '../types';
19
+ import { useMutateAppointments } from '../form/appointments-form.resource';
19
20
 
20
- interface PatientUpcomingAppointmentsProps {
21
+ interface VisitFormCallbacks {
22
+ onVisitCreatedOrUpdated: (visit: Visit) => Promise<any>;
23
+ }
24
+
25
+ // See VisitFormExtensionState in esm-patient-chart-app
26
+ export interface PatientUpcomingAppointmentsProps {
27
+ setVisitFormCallbacks(callbacks: VisitFormCallbacks);
28
+ visitFormOpenedFrom: string;
29
+ patientChartConfig?: {
30
+ showUpcomingAppointments: boolean;
31
+ };
21
32
  patientUuid: string;
22
- setUpcomingAppointment: (value: Appointment) => void;
23
33
  }
24
34
 
35
+ /**
36
+ * This is an extension that gets slotted into the patient chart start visit form when
37
+ * the appropriate config values are enabled.
38
+ * @param param0
39
+ * @returns
40
+ */
25
41
  const PatientUpcomingAppointmentsCard: React.FC<PatientUpcomingAppointmentsProps> = ({
26
42
  patientUuid,
27
- setUpcomingAppointment,
43
+ setVisitFormCallbacks,
44
+ patientChartConfig,
28
45
  }) => {
29
46
  const { t } = useTranslation();
30
47
  const startDate = dayjs(new Date().toISOString()).subtract(6, 'month').toISOString();
31
48
  const headerTitle = t('upcomingAppointments', 'Upcoming appointments');
32
- const [selectedAppointment, setSelectedAppointment] = useState(null);
49
+ const [selectedAppointment, setSelectedAppointment] = useState<Appointment>(null);
50
+ const { mutateAppointments } = useMutateAppointments();
51
+ const memoMutateAppointments = useMemo(() => mutateAppointments, [mutateAppointments]);
33
52
 
34
53
  const ac = useMemo<AbortController>(() => new AbortController(), []);
35
54
  useEffect(() => () => ac.abort(), [ac]);
36
55
  const { data: appointmentsData, error, isLoading } = usePatientAppointments(patientUuid, startDate, ac);
37
56
 
57
+ const onVisitCreatedOrUpdated = useMemo(
58
+ () => ({
59
+ onVisitCreatedOrUpdated: () => {
60
+ if (selectedAppointment) {
61
+ return changeAppointmentStatus('CheckedIn', selectedAppointment.uuid)
62
+ .then(() => {
63
+ memoMutateAppointments();
64
+ showSnackbar({
65
+ isLowContrast: true,
66
+ kind: 'success',
67
+ subtitle: t('appointmentMarkedChecked', 'Appointment marked as Checked In'),
68
+ title: t('appointmentCheckedIn', 'Appointment Checked In'),
69
+ });
70
+ })
71
+ .catch((error) => {
72
+ showSnackbar({
73
+ title: t('updateError', 'Error updating upcoming appointment'),
74
+ kind: 'error',
75
+ isLowContrast: false,
76
+ subtitle: error?.message,
77
+ });
78
+ });
79
+ } else {
80
+ return Promise.resolve();
81
+ }
82
+ },
83
+ }),
84
+ [selectedAppointment, memoMutateAppointments, t],
85
+ );
86
+
87
+ useEffect(() => {
88
+ setVisitFormCallbacks(onVisitCreatedOrUpdated);
89
+ }, [onVisitCreatedOrUpdated, setVisitFormCallbacks]);
90
+
38
91
  const todaysAppointments = appointmentsData?.todaysAppointments?.length ? appointmentsData?.todaysAppointments : [];
39
92
  const futureAppointments = appointmentsData?.upcomingAppointments?.length
40
93
  ? appointmentsData?.upcomingAppointments
@@ -46,9 +99,12 @@ const PatientUpcomingAppointmentsCard: React.FC<PatientUpcomingAppointmentsProps
46
99
 
47
100
  const handleRadioChange = (appointment: Appointment) => {
48
101
  setSelectedAppointment(appointment);
49
- setUpcomingAppointment(appointment);
50
102
  };
51
103
 
104
+ if (!patientChartConfig.showUpcomingAppointments) {
105
+ return <></>;
106
+ }
107
+
52
108
  if (error) {
53
109
  return <ErrorState headerTitle={headerTitle} error={error} />;
54
110
  }
package/src/routes.json CHANGED
@@ -99,7 +99,7 @@
99
99
  {
100
100
  "name": "patient-upcoming-appointment-widget",
101
101
  "component": "patientUpcomingAppointmentsWidget",
102
- "slot": "upcoming-appointment-slot"
102
+ "slot": "visit-form-top-slot"
103
103
  },
104
104
  {
105
105
  "name": "edit-appointments-form",
@@ -149,7 +149,7 @@ export interface AppointmentSummary {
149
149
  export interface Provider {
150
150
  uuid: string;
151
151
  display: string;
152
- comments: string;
152
+ comments?: string;
153
153
  response?: string;
154
154
  person: OpenmrsResource;
155
155
  name?: string;
@@ -1,11 +1,11 @@
1
- import dayjs from 'dayjs';
2
1
  import React, { useContext } from 'react';
2
+ import dayjs from 'dayjs';
3
3
  import { useTranslation } from 'react-i18next';
4
- import DaysOfWeekCard from '../../calendar/monthly/days-of-week.component';
5
4
  import { monthDays } from '../../helpers';
5
+ import DaysOfWeekCard from '../../calendar/monthly/days-of-week.component';
6
+ import MonthlyWorkloadCard from './monthlyWorkCard';
6
7
  import SelectedDateContext from '../../hooks/selectedDateContext';
7
8
  import styles from './monthly-workload.scss';
8
- import MonthlyWorkloadCard from './monthlyWorkCard';
9
9
 
10
10
  interface MonthlyCalendarViewProps {
11
11
  calendarWorkload: Array<{ count: number; date: string }>;
@@ -13,22 +13,24 @@ interface MonthlyCalendarViewProps {
13
13
  onDateClick?: (pickedDate: Date) => void;
14
14
  }
15
15
 
16
- const monthFormat = 'MMMM, YYYY';
17
16
  const MonthlyCalendarView: React.FC<MonthlyCalendarViewProps> = ({
18
17
  calendarWorkload,
19
18
  dateToDisplay = '',
20
19
  onDateClick,
21
20
  }) => {
21
+ const monthFormat = 'MMMM, YYYY';
22
+ const { t } = useTranslation();
22
23
  const { selectedDate } = useContext(SelectedDateContext);
23
24
  const daysInWeek = ['SUN', 'MON', 'TUE', 'WED', 'THUR', 'FRI', 'SAT'];
24
25
  const monthViewDate = dateToDisplay === '' ? selectedDate : dateToDisplay;
26
+ const daysInWeeks = daysInWeek.map((day) => t(day));
27
+
25
28
  const handleClick = (date: Date) => {
26
29
  if (onDateClick) {
27
30
  onDateClick(date);
28
31
  }
29
32
  };
30
- const { t } = useTranslation();
31
- const daysInWeeks = daysInWeek.map((day) => t(day));
33
+
32
34
  return (
33
35
  <div className={styles.calendarViewContainer}>
34
36
  <>
@@ -15,6 +15,7 @@ interface MonthlyWorkloadComponentProps {
15
15
  const MonthlyWorkloadCard: React.FC<MonthlyWorkloadComponentProps> = ({ date, count, isActive, selectedDate }) => {
16
16
  const layout = useLayoutType();
17
17
  const isToday = date.isSame(dayjs(), 'day');
18
+
18
19
  return (
19
20
  <div
20
21
  className={classNames(
@@ -30,7 +31,7 @@ const MonthlyWorkloadCard: React.FC<MonthlyWorkloadComponentProps> = ({ date, co
30
31
  [styles.largeDesktop]: layout !== 'small-desktop',
31
32
  },
32
33
  )}>
33
- <p>
34
+ <div>
34
35
  <b className={[styles.calendarDate, isToday ? styles.blue : ''].join(' ')}>{date.format('D')}</b>
35
36
  <div className={styles.currentData}>
36
37
  <div tabIndex={0} role="button" className={classNames(styles.tileContainer, {})}></div>
@@ -38,7 +39,7 @@ const MonthlyWorkloadCard: React.FC<MonthlyWorkloadComponentProps> = ({ date, co
38
39
  <span className={isActive ? styles.blue : ''}>{count}</span>
39
40
  </div>
40
41
  </div>
41
- </p>
42
+ </div>
42
43
  </div>
43
44
  );
44
45
  };
@@ -2,13 +2,14 @@ import React from 'react';
2
2
  import classNames from 'classnames';
3
3
  import styles from './workload.scss';
4
4
 
5
- interface WorkloadCardProp {
5
+ interface WorkloadCardProps {
6
+ count: number;
6
7
  day: string;
7
8
  date: string;
8
- count: number;
9
9
  isActive: boolean;
10
10
  }
11
- const WorkloadCard: React.FC<WorkloadCardProp> = ({ day, date, count, isActive }) => {
11
+
12
+ const WorkloadCard = ({ count, day, date, isActive }: WorkloadCardProps) => {
12
13
  return (
13
14
  <div
14
15
  tabIndex={0}
@@ -28,4 +29,5 @@ const WorkloadCard: React.FC<WorkloadCardProp> = ({ day, date, count, isActive }
28
29
  </div>
29
30
  );
30
31
  };
32
+
31
33
  export default WorkloadCard;