@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.
- package/.turbo/turbo-build.log +27 -25
- package/dist/111.js +2 -0
- package/dist/111.js.map +1 -0
- package/dist/123.js +2 -0
- package/dist/{198.js.LICENSE.txt → 123.js.LICENSE.txt} +0 -6
- package/dist/123.js.map +1 -0
- package/dist/130.js +1 -1
- package/dist/130.js.map +1 -1
- package/dist/{787.js → 139.js} +1 -1
- package/dist/139.js.map +1 -0
- package/dist/228.js +1 -0
- package/dist/236.js +1 -0
- package/dist/240.js +1 -0
- package/dist/261.js +1 -0
- package/dist/269.js +1 -1
- package/dist/269.js.map +1 -1
- package/dist/271.js +1 -1
- package/dist/272.js +1 -0
- package/dist/319.js +1 -1
- package/dist/336.js +1 -0
- package/dist/378.js +1 -0
- package/dist/443.js +1 -0
- package/dist/443.js.map +1 -0
- package/dist/460.js +1 -1
- package/dist/529.js +1 -1
- package/dist/529.js.map +1 -1
- package/dist/539.js +1 -0
- package/dist/566.js +1 -0
- package/dist/574.js +1 -1
- package/dist/581.js +1 -1
- package/dist/644.js +1 -1
- package/dist/652.js +1 -0
- package/dist/673.js +1 -0
- package/dist/705.js +1 -0
- package/dist/711.js +1 -1
- package/dist/711.js.map +1 -1
- package/dist/727.js +1 -0
- package/dist/737.js +1 -0
- package/dist/744.js +1 -0
- package/dist/757.js +1 -1
- package/dist/788.js +1 -1
- package/dist/807.js +1 -1
- package/dist/833.js +1 -1
- package/dist/899.js +1 -0
- package/dist/923.js +1 -0
- package/dist/923.js.map +1 -0
- package/dist/kenyaemr-esm-appointments-app.js +1 -1
- package/dist/kenyaemr-esm-appointments-app.js.buildmanifest.json +454 -102
- package/dist/kenyaemr-esm-appointments-app.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package-lock.json +6416 -0
- package/package.json +5 -5
- package/src/appointments/appointment-tabs.component.tsx +1 -1
- package/src/appointments/common-components/appointments-table.component.tsx +0 -1
- package/src/appointments/common-components/appointments-table.test.tsx +7 -3
- package/src/appointments/common-components/checkin-button.component.tsx +5 -1
- package/src/appointments/common-components/end-appointment.modal.tsx +2 -2
- package/src/appointments/scheduled/scheduled-appointments.component.tsx +1 -1
- package/src/appointments/unscheduled/unscheduled-appointments.component.tsx +6 -6
- package/src/appointments/unscheduled/unscheduled-appointments.test.tsx +19 -4
- package/src/appointments.component.tsx +7 -7
- package/src/appointments.test.tsx +11 -1
- package/src/calendar/appointments-calendar-view.component.tsx +2 -2
- package/src/calendar/monthly/monthly-workload-view.component.tsx +7 -7
- package/src/config-schema.ts +6 -70
- package/src/form/appointments-form.component.tsx +84 -73
- package/src/form/appointments-form.resource.ts +4 -4
- package/src/form/appointments-form.scss +1 -1
- package/src/form/appointments-form.test.tsx +142 -110
- package/src/patient-appointments/patient-appointments-action-menu.component.tsx +2 -4
- package/src/patient-appointments/patient-appointments-base.test.tsx +1 -1
- package/src/patient-appointments/patient-appointments-cancel.modal.tsx +6 -1
- package/src/patient-appointments/patient-appointments-cancel.scss +29 -0
- package/src/patient-appointments/patient-upcoming-appointments-card.component.tsx +64 -8
- package/src/routes.json +1 -1
- package/src/types/index.ts +1 -1
- package/src/workload/monthly-view-workload/monthly-view.component.tsx +8 -6
- package/src/workload/monthly-view-workload/monthlyWorkCard.tsx +3 -2
- package/src/workload/workload-card.component.tsx +5 -3
- package/src/workload/workload.component.tsx +13 -20
- package/src/workload/workload.resource.ts +3 -0
- package/translations/am.json +4 -2
- package/translations/ar.json +24 -22
- package/translations/de.json +168 -0
- package/translations/en.json +3 -1
- package/translations/es.json +58 -56
- package/translations/fr.json +4 -2
- package/translations/he.json +93 -91
- package/translations/hi.json +168 -0
- package/translations/hi_IN.json +168 -0
- package/translations/id.json +168 -0
- package/translations/it.json +168 -0
- package/translations/km.json +4 -2
- package/translations/ne.json +168 -0
- package/translations/pt.json +168 -0
- package/translations/pt_BR.json +168 -0
- package/translations/qu.json +168 -0
- package/translations/si.json +168 -0
- package/translations/sw.json +168 -0
- package/translations/sw_KE.json +168 -0
- package/translations/tr.json +168 -0
- package/translations/tr_TR.json +168 -0
- package/translations/uk.json +168 -0
- package/translations/vi.json +168 -0
- package/translations/zh.json +4 -2
- package/translations/zh_CN.json +4 -2
- package/dist/198.js +0 -2
- package/dist/198.js.map +0 -1
- package/dist/265.js +0 -1
- package/dist/265.js.map +0 -1
- package/dist/440.js +0 -2
- package/dist/440.js.map +0 -1
- package/dist/501.js +0 -1
- package/dist/501.js.map +0 -1
- package/dist/787.js.map +0 -1
- package/src/hooks/useDefaultLocation.ts +0 -14
- /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 {
|
|
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 {
|
|
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
|
|
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(/
|
|
113
|
-
expect(screen.getByLabelText(
|
|
114
|
-
expect(screen.getByLabelText(/
|
|
115
|
-
expect(screen.getByLabelText(/
|
|
116
|
-
expect(screen.
|
|
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: /
|
|
120
|
-
expect(screen.getByRole('option', { name: /
|
|
121
|
-
expect(screen.
|
|
122
|
-
expect(screen.getByRole('option', { name:
|
|
123
|
-
expect(screen.getByRole('option', { name:
|
|
124
|
-
expect(screen.getByRole('option', { name: /
|
|
125
|
-
expect(screen.getByRole('option', { name: /
|
|
126
|
-
expect(screen.getByRole('
|
|
127
|
-
expect(screen.getByRole('textbox', { name:
|
|
128
|
-
expect(screen.getByRole('
|
|
129
|
-
expect(screen.getByRole('button', { name: /
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
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.
|
|
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.
|
|
189
|
-
|
|
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
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
await user.
|
|
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('
|
|
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
|
|
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
|
|
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
|
-
|
|
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
package/src/types/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
|
5
|
+
interface WorkloadCardProps {
|
|
6
|
+
count: number;
|
|
6
7
|
day: string;
|
|
7
8
|
date: string;
|
|
8
|
-
count: number;
|
|
9
9
|
isActive: boolean;
|
|
10
10
|
}
|
|
11
|
-
|
|
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;
|