@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.
- package/.turbo/turbo-build.log +42 -0
- package/dist/130.js +2 -0
- package/dist/130.js.LICENSE.txt +3 -0
- package/dist/130.js.map +1 -0
- package/dist/152.js +1 -0
- package/dist/152.js.map +1 -0
- package/dist/224.js +1 -0
- package/dist/224.js.map +1 -0
- package/dist/255.js +2 -0
- package/dist/255.js.LICENSE.txt +9 -0
- package/dist/255.js.map +1 -0
- package/dist/271.js +1 -0
- package/dist/303.js +1 -0
- package/dist/303.js.map +1 -0
- package/dist/309.js +1 -0
- package/dist/309.js.map +1 -0
- package/dist/319.js +1 -0
- package/dist/4.js +1 -0
- package/dist/4.js.map +1 -0
- package/dist/445.js +2 -0
- package/dist/445.js.LICENSE.txt +54 -0
- package/dist/445.js.map +1 -0
- package/dist/460.js +1 -0
- package/dist/501.js +1 -0
- package/dist/501.js.map +1 -0
- package/dist/574.js +1 -0
- package/dist/591.js +2 -0
- package/dist/591.js.LICENSE.txt +32 -0
- package/dist/591.js.map +1 -0
- package/dist/644.js +1 -0
- package/dist/729.js +1 -0
- package/dist/729.js.map +1 -0
- package/dist/757.js +1 -0
- package/dist/784.js +2 -0
- package/dist/784.js.LICENSE.txt +9 -0
- package/dist/784.js.map +1 -0
- package/dist/788.js +1 -0
- package/dist/807.js +1 -0
- package/dist/833.js +1 -0
- package/dist/857.js +2 -0
- package/dist/857.js.LICENSE.txt +5 -0
- package/dist/857.js.map +1 -0
- package/dist/904.js +1 -0
- package/dist/904.js.map +1 -0
- package/dist/kenyaemr-esm-appointments-app.js +1 -0
- package/dist/kenyaemr-esm-appointments-app.js.buildmanifest.json +699 -0
- package/dist/kenyaemr-esm-appointments-app.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +64 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +3 -0
- package/package.json +57 -0
- package/src/admin/appointment-services/appointment-services-hook.ts +31 -0
- package/src/admin/appointment-services/appointment-services-validation.ts +17 -0
- package/src/admin/appointment-services/appointment-services.component.tsx +182 -0
- package/src/admin/appointment-services/appointment-services.scss +25 -0
- package/src/appointments/appointment-tabs.component.tsx +48 -0
- package/src/appointments/appointment-tabs.scss +53 -0
- package/src/appointments/appointment-tabs.test.tsx +55 -0
- package/src/appointments/common-components/appointments-actions.component.tsx +86 -0
- package/src/appointments/common-components/appointments-actions.scss +4 -0
- package/src/appointments/common-components/appointments-actions.test.tsx +201 -0
- package/src/appointments/common-components/appointments-table.component.tsx +277 -0
- package/src/appointments/common-components/appointments-table.scss +133 -0
- package/src/appointments/common-components/appointments-table.test.tsx +134 -0
- package/src/appointments/common-components/checkin-button.component.tsx +43 -0
- package/src/appointments/common-components/end-appointment-modal.component.tsx +104 -0
- package/src/appointments/common-components/end-appointment-modal.test.tsx +80 -0
- package/src/appointments/common-components/location-select-option.component.tsx +48 -0
- package/src/appointments/details/appointment-details.component.tsx +91 -0
- package/src/appointments/details/appointment-details.scss +81 -0
- package/src/appointments/details/appointment-details.test.tsx +103 -0
- package/src/appointments/scheduled/appointments-list.component.tsx +33 -0
- package/src/appointments/scheduled/early-appointments.component.tsx +32 -0
- package/src/appointments/scheduled/scheduled-appointments.component.tsx +215 -0
- package/src/appointments/scheduled/scheduled-appointments.scss +4 -0
- package/src/appointments/unscheduled/unscheduled-appointments.component.tsx +146 -0
- package/src/appointments/unscheduled/unscheduled-appointments.test.tsx +131 -0
- package/src/appointments/utils.tsx +80 -0
- package/src/appointments.component.tsx +44 -0
- package/src/appointments.test.tsx +15 -0
- package/src/calendar/appointments-calendar-view-view.scss +24 -0
- package/src/calendar/appointments-calendar-view.component.tsx +36 -0
- package/src/calendar/appointments-calendar-view.test.tsx +22 -0
- package/src/calendar/header/calendar-header.component.tsx +34 -0
- package/src/calendar/header/calendar-header.scss +32 -0
- package/src/calendar/monthly/days-of-week.component.tsx +16 -0
- package/src/calendar/monthly/days-of-week.scss +33 -0
- package/src/calendar/monthly/monthly-calendar-view.component.tsx +34 -0
- package/src/calendar/monthly/monthly-header.module.scss +14 -0
- package/src/calendar/monthly/monthly-header.module.tsx +40 -0
- package/src/calendar/monthly/monthly-view-workload.scss +188 -0
- package/src/calendar/monthly/monthly-workload-view-expanded.component.tsx +42 -0
- package/src/calendar/monthly/monthly-workload-view.component.tsx +109 -0
- package/src/config-schema.ts +151 -0
- package/src/constants.ts +55 -0
- package/src/createDashboardLink.component.tsx +39 -0
- package/src/dashboard.meta.ts +21 -0
- package/src/declarations.d.ts +4 -0
- package/src/empty-state/empty-data-illustration.component.tsx +39 -0
- package/src/empty-state/empty-state.component.tsx +32 -0
- package/src/empty-state/empty-state.scss +69 -0
- package/src/form/appointments-form.component.tsx +891 -0
- package/src/form/appointments-form.resource.ts +165 -0
- package/src/form/appointments-form.scss +113 -0
- package/src/form/appointments-form.test.tsx +212 -0
- package/src/header/appointments-header.component.tsx +79 -0
- package/src/header/appointments-header.scss +95 -0
- package/src/header/appointments-illustration.component.tsx +22 -0
- package/src/helpers/excel.ts +61 -0
- package/src/helpers/functions.ts +82 -0
- package/src/helpers/index.ts +2 -0
- package/src/helpers/time.tsx +15 -0
- package/src/home/home-appointments.component.tsx +22 -0
- package/src/home/home-appointments.scss +10 -0
- package/src/hooks/patientAppointmentContext.ts +15 -0
- package/src/hooks/selectedDateContext.ts +10 -0
- package/src/hooks/useAppointmentList.ts +48 -0
- package/src/hooks/useAppointmentService.ts +11 -0
- package/src/hooks/useAppointmentsCalendar.ts +68 -0
- package/src/hooks/useClinicalMetrics.ts +79 -0
- package/src/hooks/useDefaultLocation.ts +14 -0
- package/src/hooks/useOverlay.tsx +45 -0
- package/src/hooks/usePatientAppointmentHistory.ts +49 -0
- package/src/hooks/useProviders.ts +18 -0
- package/src/hooks/useTodaysVisits.ts +19 -0
- package/src/hooks/useUnscheduledAppointments.ts +45 -0
- package/src/index.ts +111 -0
- package/src/metrics/appointments-metrics.component.tsx +71 -0
- package/src/metrics/appointments-metrics.scss +15 -0
- package/src/metrics/appointments-metrics.test.tsx +49 -0
- package/src/metrics/metrics-card.component.tsx +76 -0
- package/src/metrics/metrics-card.scss +77 -0
- package/src/metrics/metrics-header.component.tsx +62 -0
- package/src/metrics/metrics-header.scss +33 -0
- package/src/past-visit/encounter-list.component.tsx +54 -0
- package/src/past-visit/past-visit.component.tsx +106 -0
- package/src/past-visit/past-visit.resource.ts +25 -0
- package/src/past-visit/past-visit.scss +106 -0
- package/src/patient-appointments/patient-appointments-action-menu.component.tsx +65 -0
- package/src/patient-appointments/patient-appointments-action-menu.scss +7 -0
- package/src/patient-appointments/patient-appointments-base.component.tsx +165 -0
- package/src/patient-appointments/patient-appointments-base.scss +85 -0
- package/src/patient-appointments/patient-appointments-base.test.tsx +91 -0
- package/src/patient-appointments/patient-appointments-cancel-modal.component.tsx +66 -0
- package/src/patient-appointments/patient-appointments-detailed-summary.component.tsx +15 -0
- package/src/patient-appointments/patient-appointments-header.scss +27 -0
- package/src/patient-appointments/patient-appointments-header.tsx +42 -0
- package/src/patient-appointments/patient-appointments-overview.component.tsx +35 -0
- package/src/patient-appointments/patient-appointments-overview.scss +7 -0
- package/src/patient-appointments/patient-appointments-table.scss +0 -0
- package/src/patient-appointments/patient-appointments-table.tsx +128 -0
- package/src/patient-appointments/patient-appointments.resource.ts +72 -0
- package/src/patient-appointments/patient-upcoming-appointments-card.component.tsx +122 -0
- package/src/patient-appointments/patient-upcoming-appointments-card.scss +46 -0
- package/src/patient-search/patient-search.component.tsx +34 -0
- package/src/patient-search/patient-search.scss +23 -0
- package/src/root.component.tsx +26 -0
- package/src/root.scss +50 -0
- package/src/routes.json +153 -0
- package/src/scheduled-appointments-config-schema.ts +169 -0
- package/src/types/index.ts +189 -0
- package/src/workload/monthly-view-workload/monthly-view.component.tsx +69 -0
- package/src/workload/monthly-view-workload/monthly-workload.scss +223 -0
- package/src/workload/monthly-view-workload/monthlyWorkCard.tsx +45 -0
- package/src/workload/workload-card.component.tsx +31 -0
- package/src/workload/workload.component.tsx +47 -0
- package/src/workload/workload.resource.ts +78 -0
- package/src/workload/workload.scss +92 -0
- package/translations/am.json +148 -0
- package/translations/ar.json +148 -0
- package/translations/en.json +159 -0
- package/translations/es.json +148 -0
- package/translations/fr.json +148 -0
- package/translations/he.json +148 -0
- package/translations/km.json +148 -0
- package/translations/zh.json +148 -0
- package/translations/zh_CN.json +148 -0
- package/tsconfig.json +5 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import useSWR, { useSWRConfig } from 'swr';
|
|
3
|
+
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
4
|
+
import {
|
|
5
|
+
type AppointmentPayload,
|
|
6
|
+
type AppointmentService,
|
|
7
|
+
type AppointmentsFetchResponse,
|
|
8
|
+
type RecurringAppointmentsPayload,
|
|
9
|
+
} from '../types';
|
|
10
|
+
import isToday from 'dayjs/plugin/isToday';
|
|
11
|
+
import { useCallback } from 'react';
|
|
12
|
+
dayjs.extend(isToday);
|
|
13
|
+
|
|
14
|
+
const appointmentUrlMatcher = '/ws/rest/v1/appointment';
|
|
15
|
+
const appointmentsSearchUrl = '/ws/rest/v1/appointments/search';
|
|
16
|
+
|
|
17
|
+
export function useMutateAppointments() {
|
|
18
|
+
const { mutate } = useSWRConfig();
|
|
19
|
+
// this mutate is intentionally broad because there may be many different keys that need to be invalidated when appointments are updated
|
|
20
|
+
const mutateAppointments = useCallback(
|
|
21
|
+
() =>
|
|
22
|
+
mutate((key) => {
|
|
23
|
+
return (
|
|
24
|
+
(typeof key === 'string' && key.startsWith(appointmentUrlMatcher)) ||
|
|
25
|
+
(Array.isArray(key) && key[0].startsWith(appointmentUrlMatcher))
|
|
26
|
+
);
|
|
27
|
+
}),
|
|
28
|
+
[mutate],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
mutateAppointments,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useAppointments(patientUuid: string, startDate: string, abortController: AbortController) {
|
|
37
|
+
/*
|
|
38
|
+
SWR isn't meant to make POST requests for data fetching. This is a consequence of the API only exposing this resource via POST.
|
|
39
|
+
This works but likely isn't recommended.
|
|
40
|
+
*/
|
|
41
|
+
const fetcher = () =>
|
|
42
|
+
openmrsFetch(appointmentsSearchUrl, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
signal: abortController.signal,
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
},
|
|
48
|
+
body: {
|
|
49
|
+
patientUuid: patientUuid,
|
|
50
|
+
startDate: startDate,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const { data, error, isLoading, isValidating, mutate } = useSWR<AppointmentsFetchResponse, Error>(
|
|
55
|
+
appointmentsSearchUrl,
|
|
56
|
+
fetcher,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const appointments = data?.data?.length ? data.data : null;
|
|
60
|
+
|
|
61
|
+
const pastAppointments = appointments
|
|
62
|
+
?.sort((a, b) => (b.startDateTime > a.startDateTime ? 1 : -1))
|
|
63
|
+
?.filter(({ status }) => status !== 'Cancelled')
|
|
64
|
+
?.filter(({ startDateTime }) =>
|
|
65
|
+
dayjs(new Date(startDateTime).toISOString()).isBefore(new Date().setHours(0, 0, 0, 0)),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const upcomingAppointments = appointments
|
|
69
|
+
?.sort((a, b) => (a.startDateTime > b.startDateTime ? 1 : -1))
|
|
70
|
+
?.filter(({ status }) => status !== 'Cancelled')
|
|
71
|
+
?.filter(({ startDateTime }) => dayjs(new Date(startDateTime).toISOString()).isAfter(new Date()));
|
|
72
|
+
|
|
73
|
+
const todaysAppointments = appointments
|
|
74
|
+
?.sort((a, b) => (a.startDateTime > b.startDateTime ? 1 : -1))
|
|
75
|
+
?.filter(({ status }) => status !== 'Cancelled')
|
|
76
|
+
?.filter(({ startDateTime }) => dayjs(new Date(startDateTime).toISOString()).isToday());
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
data: data ? { pastAppointments, upcomingAppointments, todaysAppointments } : null,
|
|
80
|
+
isError: error,
|
|
81
|
+
isLoading,
|
|
82
|
+
isValidating,
|
|
83
|
+
mutate,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function useAppointmentService() {
|
|
88
|
+
const { data, error, isLoading } = useSWR<{ data: Array<AppointmentService> }, Error>(
|
|
89
|
+
`${restBaseUrl}/appointmentService/all/full`,
|
|
90
|
+
openmrsFetch,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
data: data ? data.data : null,
|
|
95
|
+
isError: error,
|
|
96
|
+
isLoading,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function saveAppointment(appointment: AppointmentPayload, abortController: AbortController) {
|
|
101
|
+
return openmrsFetch(`${restBaseUrl}/appointment`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
signal: abortController.signal,
|
|
104
|
+
headers: {
|
|
105
|
+
'Content-Type': 'application/json',
|
|
106
|
+
},
|
|
107
|
+
body: appointment,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function saveRecurringAppointments(
|
|
112
|
+
recurringAppointments: RecurringAppointmentsPayload,
|
|
113
|
+
abortController: AbortController,
|
|
114
|
+
) {
|
|
115
|
+
return openmrsFetch(`${restBaseUrl}/recurring-appointments`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
signal: abortController.signal,
|
|
118
|
+
headers: {
|
|
119
|
+
'Content-Type': 'application/json',
|
|
120
|
+
},
|
|
121
|
+
body: recurringAppointments,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// TODO refactor to use SWR?
|
|
126
|
+
export function getAppointmentsByUuid(appointmentUuid: string, abortController: AbortController) {
|
|
127
|
+
return openmrsFetch(`${restBaseUrl}/appointments/${appointmentUuid}`, {
|
|
128
|
+
signal: abortController.signal,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// TODO refactor to use SWR?
|
|
133
|
+
export function getAppointmentService(abortController: AbortController, uuid) {
|
|
134
|
+
return openmrsFetch(`${restBaseUrl}/appointmentService?uuid=` + uuid, {
|
|
135
|
+
signal: abortController.signal,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const cancelAppointment = async (toStatus: string, appointmentUuid: string) => {
|
|
140
|
+
const omrsDateFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZZ';
|
|
141
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
142
|
+
const statusChangeTime = dayjs(new Date()).format(omrsDateFormat);
|
|
143
|
+
const url = `${restBaseUrl}/appointments/${appointmentUuid}/status-change`;
|
|
144
|
+
return await openmrsFetch(url, {
|
|
145
|
+
body: { toStatus, onDate: statusChangeTime, timeZone: timeZone },
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: { 'Content-Type': 'application/json' },
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const checkAppointmentConflict = async (appointmentPayload: AppointmentPayload) => {
|
|
152
|
+
return await openmrsFetch(`${restBaseUrl}/appointments/conflicts`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
body: {
|
|
155
|
+
patientUuid: appointmentPayload.patientUuid,
|
|
156
|
+
serviceUuid: appointmentPayload.serviceUuid,
|
|
157
|
+
startDateTime: appointmentPayload.startDateTime,
|
|
158
|
+
endDateTime: appointmentPayload.endDateTime,
|
|
159
|
+
providers: [],
|
|
160
|
+
locationUuid: appointmentPayload.locationUuid,
|
|
161
|
+
appointmentKind: appointmentPayload.appointmentKind,
|
|
162
|
+
},
|
|
163
|
+
headers: { 'Content-Type': 'application/json' },
|
|
164
|
+
});
|
|
165
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/colors';
|
|
2
|
+
@use '@carbon/styles/scss/spacing';
|
|
3
|
+
@use '@carbon/styles/scss/type';
|
|
4
|
+
|
|
5
|
+
$openmrs-background-grey: #ededed;
|
|
6
|
+
|
|
7
|
+
.formWrapper {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
justify-content: space-between;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
:global(.omrs-breakpoint-lt-desktop) .formWrapper {
|
|
14
|
+
background-color: $openmrs-background-grey;
|
|
15
|
+
margin-top: spacing.$spacing-09;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
:global(.omrs-breakpoint-gt-tablet) .formWrapper {
|
|
19
|
+
background-color: colors.$white;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.formGroup {
|
|
23
|
+
display: flex;
|
|
24
|
+
margin-bottom: spacing.$spacing-02;
|
|
25
|
+
padding: spacing.$spacing-05;
|
|
26
|
+
|
|
27
|
+
checkbox {
|
|
28
|
+
color: colors.$gray-70;
|
|
29
|
+
@include type.type-style('heading-compact-02');
|
|
30
|
+
margin-bottom: spacing.$spacing-03;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
:global(.omrs-breakpoint-gt-tablet) .formGroup {
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
:global(.omrs-breakpoint-lt-desktop) .formGroup {
|
|
39
|
+
flex-direction: row;
|
|
40
|
+
|
|
41
|
+
.heading {
|
|
42
|
+
flex: 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
div {
|
|
46
|
+
flex: 3;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.heading {
|
|
51
|
+
color: colors.$gray-70;
|
|
52
|
+
@include type.type-style('heading-compact-02');
|
|
53
|
+
margin-bottom: spacing.$spacing-03;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.inputContainer {
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-flow: row wrap;
|
|
59
|
+
gap: spacing.$spacing-05;
|
|
60
|
+
align-items: center;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.dateTimeFields {
|
|
64
|
+
max-width: 100%;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:global(.omrs-breakpoint-gt-tablet) .dateTimeFields {
|
|
68
|
+
max-width: 32.25rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.button {
|
|
72
|
+
height: 4rem;
|
|
73
|
+
display: flex;
|
|
74
|
+
align-content: flex-start;
|
|
75
|
+
align-items: baseline;
|
|
76
|
+
min-width: 50%;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.loader {
|
|
80
|
+
display: flex;
|
|
81
|
+
background-color: $openmrs-background-grey;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
min-height: spacing.$spacing-09;
|
|
84
|
+
height: 100vh;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.tablet {
|
|
88
|
+
padding: spacing.$spacing-06 spacing.$spacing-05;
|
|
89
|
+
background-color: colors.$white;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.desktop {
|
|
93
|
+
padding: 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.weekSelect {
|
|
97
|
+
max-inline-size: fit-content;
|
|
98
|
+
padding-top: spacing.$spacing-03;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
:global(.cds--select-input):focus {
|
|
102
|
+
outline: 2.5px solid colors.$red-60;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.errorMessage {
|
|
106
|
+
@include type.type-style('label-02');
|
|
107
|
+
color: colors.$red-60;
|
|
108
|
+
margin-top: 0.5rem;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
:global(.cds--number input[type='number']):focus {
|
|
112
|
+
outline: 2.5px solid colors.$red-60;
|
|
113
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { mockLocations } from '__mocks__';
|
|
5
|
+
import { openmrsFetch } from '@openmrs/esm-framework';
|
|
6
|
+
import { mockUseAppointmentServiceData, mockSession } from '__mocks__';
|
|
7
|
+
import { mockPatient, renderWithSwr, waitForLoadingToFinish } from 'tools';
|
|
8
|
+
import { saveAppointment } from './appointments-form.resource';
|
|
9
|
+
import AppointmentForm from './appointments-form.component';
|
|
10
|
+
|
|
11
|
+
const testProps = {
|
|
12
|
+
closeWorkspace: jest.fn(),
|
|
13
|
+
patientUuid: mockPatient.id,
|
|
14
|
+
promptBeforeClosing: jest.fn(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const mockCreateAppointment = saveAppointment as jest.Mock;
|
|
18
|
+
const mockOpenmrsFetch = openmrsFetch as jest.Mock;
|
|
19
|
+
|
|
20
|
+
jest.mock('@openmrs/esm-framework', () => {
|
|
21
|
+
const originalModule = jest.requireActual('@openmrs/esm-framework');
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
...originalModule,
|
|
25
|
+
useLocations: jest.fn().mockImplementation(() => mockLocations.data.results),
|
|
26
|
+
useSession: jest.fn().mockImplementation(() => mockSession.data),
|
|
27
|
+
useConfig: jest.fn(() => ({
|
|
28
|
+
appointmentTypes: ['Scheduled', 'WalkIn'],
|
|
29
|
+
})),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
jest.mock('react-hook-form', () => ({
|
|
34
|
+
...jest.requireActual('react-hook-form'),
|
|
35
|
+
useForm: jest.fn().mockImplementation(() => ({
|
|
36
|
+
handleSubmit: () => jest.fn(),
|
|
37
|
+
control: {
|
|
38
|
+
register: jest.fn(),
|
|
39
|
+
unregister: jest.fn(),
|
|
40
|
+
getFieldState: jest.fn(),
|
|
41
|
+
_names: {
|
|
42
|
+
array: new Set('test'),
|
|
43
|
+
mount: new Set('test'),
|
|
44
|
+
unMount: new Set('test'),
|
|
45
|
+
watch: new Set('test'),
|
|
46
|
+
focus: 'test',
|
|
47
|
+
watchAll: false,
|
|
48
|
+
},
|
|
49
|
+
_subjects: {
|
|
50
|
+
watch: jest.fn(),
|
|
51
|
+
array: jest.fn(),
|
|
52
|
+
state: jest.fn(),
|
|
53
|
+
},
|
|
54
|
+
_getWatch: jest.fn(),
|
|
55
|
+
_formValues: [],
|
|
56
|
+
_defaultValues: [],
|
|
57
|
+
},
|
|
58
|
+
getValues: (str) => {
|
|
59
|
+
if (str === 'recurringPatternDaysOfWeek') {
|
|
60
|
+
return [];
|
|
61
|
+
} else {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
setValue: () => jest.fn(),
|
|
66
|
+
formState: () => jest.fn(),
|
|
67
|
+
watch: () => jest.fn(),
|
|
68
|
+
})),
|
|
69
|
+
Controller: ({ render }) =>
|
|
70
|
+
render({
|
|
71
|
+
field: {
|
|
72
|
+
onChange: jest.fn(),
|
|
73
|
+
onBlur: jest.fn(),
|
|
74
|
+
value: '',
|
|
75
|
+
ref: jest.fn(),
|
|
76
|
+
},
|
|
77
|
+
formState: {
|
|
78
|
+
isSubmitted: false,
|
|
79
|
+
},
|
|
80
|
+
fieldState: {
|
|
81
|
+
isTouched: false,
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
useSubscribe: () => ({
|
|
85
|
+
r: { current: { subject: { subscribe: () => jest.fn() } } },
|
|
86
|
+
}),
|
|
87
|
+
useController: () => ({
|
|
88
|
+
field: { ref: jest.fn() },
|
|
89
|
+
}),
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
jest.mock('./appointments-form.resource', () => {
|
|
93
|
+
const originalModule = jest.requireActual('./appointments-form.resource');
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
...originalModule,
|
|
97
|
+
saveAppointment: jest.fn(),
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('AppointmentForm', () => {
|
|
102
|
+
it('renders the appointments form showing all the relevant fields and values', async () => {
|
|
103
|
+
mockOpenmrsFetch.mockReturnValue(mockUseAppointmentServiceData);
|
|
104
|
+
|
|
105
|
+
renderAppointmentsForm();
|
|
106
|
+
|
|
107
|
+
await waitForLoadingToFinish();
|
|
108
|
+
|
|
109
|
+
expect(screen.getByLabelText(/Select a location/i)).toBeInTheDocument();
|
|
110
|
+
expect(screen.getByLabelText(/^Date$/i)).toBeInTheDocument();
|
|
111
|
+
expect(screen.getByLabelText(/Select a service/i)).toBeInTheDocument();
|
|
112
|
+
expect(screen.getByLabelText(/Select the type of appointment/i)).toBeInTheDocument();
|
|
113
|
+
expect(screen.getByLabelText(/Write an additional note/i)).toBeInTheDocument();
|
|
114
|
+
expect(screen.getByPlaceholderText(/Write any additional points here/i)).toBeInTheDocument();
|
|
115
|
+
expect(screen.getAllByPlaceholderText(/dd\/mm\/yyyy/i).length).toBe(2);
|
|
116
|
+
expect(screen.getByRole('option', { name: /Mosoriot/i })).toBeInTheDocument();
|
|
117
|
+
expect(screen.getByRole('option', { name: /Inpatient Ward/i })).toBeInTheDocument();
|
|
118
|
+
expect(screen.getByRole('option', { name: /AM/i })).toBeInTheDocument();
|
|
119
|
+
expect(screen.getByRole('option', { name: /PM/i })).toBeInTheDocument();
|
|
120
|
+
expect(screen.getByRole('option', { name: /Choose appointment type/i })).toBeInTheDocument();
|
|
121
|
+
expect(screen.getByRole('option', { name: /Scheduled/i })).toBeInTheDocument();
|
|
122
|
+
expect(screen.getByRole('option', { name: /WalkIn/i })).toBeInTheDocument();
|
|
123
|
+
expect(screen.getByRole('textbox', { name: /^Date$/i })).toBeInTheDocument();
|
|
124
|
+
expect(screen.getByRole('textbox', { name: /Time/i })).toBeInTheDocument();
|
|
125
|
+
expect(screen.getByRole('button', { name: /Discard/i })).toBeInTheDocument();
|
|
126
|
+
expect(screen.getByRole('button', { name: /Save and close/i })).toBeInTheDocument();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('closes the form and the workspace when the cancel button is clicked', async () => {
|
|
130
|
+
const user = userEvent.setup();
|
|
131
|
+
|
|
132
|
+
mockOpenmrsFetch.mockReturnValueOnce(mockUseAppointmentServiceData);
|
|
133
|
+
|
|
134
|
+
renderAppointmentsForm();
|
|
135
|
+
|
|
136
|
+
await waitForLoadingToFinish();
|
|
137
|
+
|
|
138
|
+
const cancelButton = screen.getByRole('button', { name: /Discard/i });
|
|
139
|
+
await user.click(cancelButton);
|
|
140
|
+
|
|
141
|
+
expect(testProps.closeWorkspace).toHaveBeenCalledTimes(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('renders a success snackbar upon successfully scheduling an appointment', async () => {
|
|
145
|
+
const user = userEvent.setup();
|
|
146
|
+
|
|
147
|
+
mockOpenmrsFetch.mockReturnValue({ data: mockUseAppointmentServiceData });
|
|
148
|
+
mockCreateAppointment.mockResolvedValue({ status: 200, statusText: 'Ok' });
|
|
149
|
+
|
|
150
|
+
renderAppointmentsForm();
|
|
151
|
+
|
|
152
|
+
await waitForLoadingToFinish();
|
|
153
|
+
|
|
154
|
+
const saveButton = screen.getByRole('button', { name: /Save and close/i });
|
|
155
|
+
const dateInput = screen.getByRole('textbox', { name: /^Date$/i });
|
|
156
|
+
const timeInput = screen.getByRole('textbox', { name: /Time/i });
|
|
157
|
+
const timeFormat = screen.getByRole('combobox', { name: /Time/i });
|
|
158
|
+
const serviceSelect = screen.getByRole('combobox', { name: /Select a service/i });
|
|
159
|
+
const appointmentTypeSelect = screen.getByRole('combobox', { name: /Select the type of appointment/i });
|
|
160
|
+
|
|
161
|
+
expect(saveButton).not.toBeDisabled();
|
|
162
|
+
|
|
163
|
+
await user.clear(dateInput);
|
|
164
|
+
await user.type(dateInput, '4/4/2021');
|
|
165
|
+
await user.clear(timeInput);
|
|
166
|
+
await user.type(timeInput, '09:30');
|
|
167
|
+
await user.selectOptions(timeFormat, 'AM');
|
|
168
|
+
await user.selectOptions(serviceSelect, ['Outpatient']);
|
|
169
|
+
await user.selectOptions(appointmentTypeSelect, ['Scheduled']);
|
|
170
|
+
|
|
171
|
+
await user.click(saveButton);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('renders an error snackbar if there was a problem scheduling an appointment', async () => {
|
|
175
|
+
const user = userEvent.setup();
|
|
176
|
+
|
|
177
|
+
const error = {
|
|
178
|
+
message: 'Internal Server Error',
|
|
179
|
+
response: {
|
|
180
|
+
status: 500,
|
|
181
|
+
statusText: 'Internal Server Error',
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
mockOpenmrsFetch.mockReturnValueOnce({ data: mockUseAppointmentServiceData });
|
|
186
|
+
mockCreateAppointment.mockRejectedValueOnce(error);
|
|
187
|
+
|
|
188
|
+
renderAppointmentsForm();
|
|
189
|
+
|
|
190
|
+
await waitForLoadingToFinish();
|
|
191
|
+
|
|
192
|
+
const saveButton = screen.getByRole('button', { name: /Save and close/i });
|
|
193
|
+
const dateInput = screen.getByRole('textbox', { name: /^Date$/i });
|
|
194
|
+
const timeInput = screen.getByRole('textbox', { name: /Time/i });
|
|
195
|
+
const timeFormat = screen.getByRole('combobox', { name: /Time/i });
|
|
196
|
+
const serviceSelect = screen.getByRole('combobox', { name: /Select a service/i });
|
|
197
|
+
const appointmentTypeSelect = screen.getByRole('combobox', { name: /Select the type of appointment/i });
|
|
198
|
+
|
|
199
|
+
await user.clear(dateInput);
|
|
200
|
+
await user.type(dateInput, '4/4/2021');
|
|
201
|
+
await user.clear(timeInput);
|
|
202
|
+
await user.type(timeInput, '09:30');
|
|
203
|
+
await user.selectOptions(timeFormat, 'AM');
|
|
204
|
+
await user.selectOptions(serviceSelect, ['Outpatient']);
|
|
205
|
+
await user.selectOptions(appointmentTypeSelect, ['Scheduled']);
|
|
206
|
+
await user.click(saveButton);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
function renderAppointmentsForm() {
|
|
211
|
+
renderWithSwr(<AppointmentForm {...testProps} />);
|
|
212
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { DatePicker, DatePickerInput, Dropdown } from '@carbon/react';
|
|
5
|
+
import { Location } from '@carbon/react/icons';
|
|
6
|
+
import { useSession } from '@openmrs/esm-framework';
|
|
7
|
+
import { useAppointmentServices } from '../hooks/useAppointmentService';
|
|
8
|
+
import AppointmentsIllustration from './appointments-illustration.component';
|
|
9
|
+
import styles from './appointments-header.scss';
|
|
10
|
+
import SelectedDateContext from '../hooks/selectedDateContext';
|
|
11
|
+
import { omrsDateFormat } from '../constants';
|
|
12
|
+
|
|
13
|
+
interface AppointmentHeaderProps {
|
|
14
|
+
title: string;
|
|
15
|
+
appointmentServiceType?: string;
|
|
16
|
+
onChange?: (evt) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const AppointmentsHeader: React.FC<AppointmentHeaderProps> = ({ title, appointmentServiceType, onChange }) => {
|
|
20
|
+
const { t } = useTranslation();
|
|
21
|
+
const session = useSession();
|
|
22
|
+
const { selectedDate, setSelectedDate } = useContext(SelectedDateContext);
|
|
23
|
+
const location = session?.sessionLocation?.display;
|
|
24
|
+
const { serviceTypes } = useAppointmentServices();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={styles.header} data-testid="appointments-header">
|
|
28
|
+
<div className={styles['left-justified-items']}>
|
|
29
|
+
<AppointmentsIllustration />
|
|
30
|
+
<div className={styles['page-labels']}>
|
|
31
|
+
<p>{t('appointments', 'Appointments')}</p>
|
|
32
|
+
<p className={styles['page-name']}>{title}</p>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div className={styles['right-justified-items']}>
|
|
36
|
+
<div className={styles['date-and-location']}>
|
|
37
|
+
<Location size={16} />
|
|
38
|
+
<span className={styles.value}>{location}</span>
|
|
39
|
+
<span className={styles.middot}>·</span>
|
|
40
|
+
<DatePicker
|
|
41
|
+
onChange={([date]) => setSelectedDate(dayjs(date).startOf('day').format(omrsDateFormat))}
|
|
42
|
+
value={dayjs(selectedDate).format('DD MMM YYYY')}
|
|
43
|
+
dateFormat="d-M-Y"
|
|
44
|
+
datePickerType="single">
|
|
45
|
+
<DatePickerInput
|
|
46
|
+
style={{ cursor: 'pointer', backgroundColor: 'transparent', border: 'none', maxWidth: '10rem' }}
|
|
47
|
+
id="appointment-date-picker"
|
|
48
|
+
placeholder="DD-MMM-YYYY"
|
|
49
|
+
labelText=""
|
|
50
|
+
type="text"
|
|
51
|
+
/>
|
|
52
|
+
</DatePicker>
|
|
53
|
+
</div>
|
|
54
|
+
<div className={styles.dropdownContainer}>
|
|
55
|
+
{typeof onChange === 'function' && (
|
|
56
|
+
<Dropdown
|
|
57
|
+
className={styles.dropdown}
|
|
58
|
+
aria-label="Select service type"
|
|
59
|
+
id="serviceDropdown"
|
|
60
|
+
selectedItem={
|
|
61
|
+
serviceTypes.find((service) => service.uuid === appointmentServiceType) || { name: 'All', uuid: '' }
|
|
62
|
+
}
|
|
63
|
+
items={[{ name: 'All', uuid: '' }, ...serviceTypes]}
|
|
64
|
+
itemToString={(item) => (item ? item.name : '')}
|
|
65
|
+
label={t('selectServiceType', 'Select service type')}
|
|
66
|
+
type="inline"
|
|
67
|
+
size="sm"
|
|
68
|
+
direction="bottom"
|
|
69
|
+
titleText={t('view', 'View')}
|
|
70
|
+
onChange={({ selectedItem }) => onChange(selectedItem?.uuid)}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default AppointmentsHeader;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/styles/scss/spacing';
|
|
3
|
+
@use '@carbon/styles/scss/type';
|
|
4
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
5
|
+
|
|
6
|
+
.header {
|
|
7
|
+
@include type.type-style('body-compact-02');
|
|
8
|
+
color: $text-02;
|
|
9
|
+
height: spacing.$spacing-12;
|
|
10
|
+
background-color: $ui-02;
|
|
11
|
+
border-bottom: 1px solid $ui-03;
|
|
12
|
+
display: flex;
|
|
13
|
+
justify-content: space-between;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.left-justified-items {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: row;
|
|
19
|
+
align-items: center;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.right-justified-items {
|
|
23
|
+
@include type.type-style('body-compact-02');
|
|
24
|
+
color: $text-02;
|
|
25
|
+
margin: 0.5rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.page-name {
|
|
29
|
+
@include type.type-style('heading-04');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.page-labels {
|
|
33
|
+
margin: 1rem 0;
|
|
34
|
+
|
|
35
|
+
p:first-of-type {
|
|
36
|
+
margin-bottom: 0.25rem;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.date-and-location {
|
|
41
|
+
display: flex;
|
|
42
|
+
justify-content: flex-end;
|
|
43
|
+
align-items: center;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.dropdownContainer {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
justify-content: flex-end;
|
|
50
|
+
margin-top: 0.25rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.value {
|
|
54
|
+
margin-left: 0.25rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.middot {
|
|
58
|
+
margin: 0 0.5rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.view {
|
|
62
|
+
@include type.type-style('label-01');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.datePicker {
|
|
66
|
+
background-color: transparent;
|
|
67
|
+
width: 10rem;
|
|
68
|
+
border: none;
|
|
69
|
+
& > input {
|
|
70
|
+
color: colors.$blue-10;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.dropdown {
|
|
75
|
+
:global(.cds--list-box__field) {
|
|
76
|
+
width: 14rem;
|
|
77
|
+
overflow: hidden;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
:global(.cds--dropdown--inline .cds--list-box__menu) {
|
|
81
|
+
left: -4rem;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Overriding styles for RTL support
|
|
86
|
+
html[dir='rtl'] {
|
|
87
|
+
.date-and-location {
|
|
88
|
+
& > svg {
|
|
89
|
+
order: -1;
|
|
90
|
+
}
|
|
91
|
+
& > span:nth-child(2) {
|
|
92
|
+
order: -2;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const AppointmentsIllustration: React.FC = () => {
|
|
4
|
+
return (
|
|
5
|
+
<svg width="92" height="94" viewBox="0 0 92 94" xmlns="http://www.w3.org/2000/svg">
|
|
6
|
+
<title>Patient queue illustration</title>
|
|
7
|
+
<g fill="none" fillRule="evenodd">
|
|
8
|
+
<path fill="#FFF" d="M0 0h92v94H0z" />
|
|
9
|
+
<path
|
|
10
|
+
d="M40 32c.84-.602 1.12-1.797 1-3 .12-5.005-3.96-9-9-9s-9.12 3.995-9 9c-.12 3.572 2.1 6.706 5 8-6.76 1.741-12 7.91-12 15v15h28V32h-4zM76 67V52c0-7.09-5.24-13.278-12-15 2.9-1.294 5.12-4.428 5-8 .12-5.005-3.96-9-9-9s-9.12 3.995-9 9c-.12 1.203.14 2.398 1 3h-4v35h28z"
|
|
11
|
+
fill="#CEE6E5"
|
|
12
|
+
/>
|
|
13
|
+
<path
|
|
14
|
+
d="M32 75V60.312c0-7.402 5.24-13.59 12.3-15.216-3.2-1.39-5.42-4.523-5.42-8.166 0-4.935 4.08-8.93 9.12-8.93 5.04 0 9.12 3.995 9.12 8.93 0 3.642-2.22 6.776-5.42 8.166C58.76 46.741 64 52.91 64 60.313V75"
|
|
15
|
+
fill="#7BBCB9"
|
|
16
|
+
/>
|
|
17
|
+
</g>
|
|
18
|
+
</svg>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default AppointmentsIllustration;
|