@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
package/dist/routes.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":"^2.2.0"},"modals":[{"name":"end-appointment-modal","component":"endAppointmentModal"}],"extensions":[{"name":"home-appointments","slot":"homepage-widgets-slot","component":"homeAppointments","order":1},{"name":"clinical-appointments-dashboard-link","slot":"homepage-dashboard-slot","component":"appointmentsDashboardLink","meta":{"name":"appointments","slot":"clinical-appointments-dashboard-slot","title":"Appointments"}},{"component":"root","name":"clinical-appointments-dashboard","slot":"clinical-appointments-dashboard-slot"},{"name":"appointments-calendar-dashboard-link","slot":"calendar-dashboard-slot","component":"appointmentsCalendarDashboardLink"},{"name":"check-in-appointment-modal","slot":"todays-appointment-slot","component":"checkInModal"},{"name":"todays-appointments-dashboard","slot":"todays-appointment-slot","component":"homeAppointments"},{"name":"expected-appointments-panel","slot":"scheduled-appointments-panels-slot","component":"appointmentsList"},{"name":"checked-in-appointments-panel","slot":"scheduled-appointments-panels-slot","component":"appointmentsList"},{"name":"completed-appointments-panel","slot":"scheduled-appointments-panels-slot","component":"appointmentsList"},{"name":"missed-appointments-panel","slot":"scheduled-appointments-panels-slot","component":"appointmentsList"},{"name":"cancelled-appointments-panel","slot":"scheduled-appointments-panels-slot","component":"appointmentsList"},{"name":"early-appointments-panel","component":"earlyAppointments"},{"name":"appointments-form-workspace","component":"appointmentsFormWorkspace","meta":{"title":{"key":"createNewAppointment","default":"Create new appointment"}}},{"name":"patient-appointments-summary-dashboard","component":"patientAppointmentsSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-appointments-dashboard-slot","title":"Appointments","path":"Appointments"}},{"name":"patientAppointments-details-widget","component":"patientAppointmentsDetailedSummary","slot":"patient-chart-appointments-dashboard-slot","meta":{"columnSpan":1}},{"name":"patient-upcoming-appointment-widget","component":"patientUpcomingAppointmentsWidget","slot":"upcoming-appointment-slot"},{"name":"patient-appointment-cancel-confirmation-dialog","component":"patientAppointmentsCancelConfirmationDialog"},{"name":"edit-appointments-form","component":"appointementForm","meta":{"title":{"key":"editAppointment","default":"Edit Appointment"}}},{"name":"search-patient","component":"searchPatient"},{"name":"create-appointment","component":"appointementForm","meta":{"title":{"key":"appointmentForm","default":"Appointment Form"}}},{"name":"add-appointment","component":"appointementForm","meta":{"title":{"key":"createNewAppointment","default":"Create new appointment"}}}],"version":"7.0.2-pre.65"}
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kenyaemr/esm-appointments-app",
|
|
3
|
+
"version": "7.0.2-pre.65",
|
|
4
|
+
"description": "Appointments front-end module for the OpenMRS SPA",
|
|
5
|
+
"browser": "dist/kenyaemr-esm-appointments-app.js",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"source": true,
|
|
8
|
+
"license": "MPL-2.0",
|
|
9
|
+
"homepage": "https://github.com/openmrs/openmrs-esm-patient-management#readme",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "openmrs develop",
|
|
12
|
+
"serve": "webpack serve --mode=development",
|
|
13
|
+
"debug": "npm run serve",
|
|
14
|
+
"build": "webpack --mode production",
|
|
15
|
+
"analyze": "webpack --mode=production --env.analyze=true",
|
|
16
|
+
"lint": "eslint src --ext tsx",
|
|
17
|
+
"test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests --color",
|
|
18
|
+
"test-watch": "cross-env TZ=UTC jest --watch --config jest.config.js",
|
|
19
|
+
"coverage": "yarn test --coverage",
|
|
20
|
+
"typescript": "tsc",
|
|
21
|
+
"extract-translations": "i18next 'src/**/*.component.tsx' 'src/**/*.extension.tsx' 'src/**/*.workspace.tsx' 'src/index.ts' --config ../../tools/i18next-parser.config.js"
|
|
22
|
+
},
|
|
23
|
+
"browserslist": [
|
|
24
|
+
"extends browserslist-config-openmrs"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"openmrs"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/openmrs/openmrs-esm-patient-management.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/openmrs/openmrs-esm-patient-management/issues"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@carbon/react": "~1.37.0",
|
|
41
|
+
"formik": "^2.2.9",
|
|
42
|
+
"lodash-es": "^4.17.15",
|
|
43
|
+
"yup": "^0.32.11"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@openmrs/esm-framework": "5.x",
|
|
47
|
+
"@openmrs/esm-patient-common-lib": "7.x",
|
|
48
|
+
"react": "18.x",
|
|
49
|
+
"react-i18next": "11.x",
|
|
50
|
+
"react-router-dom": "6.x",
|
|
51
|
+
"swr": "2.x"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"webpack": "^5.74.0"
|
|
55
|
+
},
|
|
56
|
+
"stableVersion": "7.0.1"
|
|
57
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import { type AppointmentService } from '../../types';
|
|
3
|
+
|
|
4
|
+
const appointmentServiceInitialValue: AppointmentService = {
|
|
5
|
+
appointmentServiceId: 0,
|
|
6
|
+
creatorName: '',
|
|
7
|
+
description: '',
|
|
8
|
+
durationMins: 0,
|
|
9
|
+
endTime: '',
|
|
10
|
+
initialAppointmentStatus: '',
|
|
11
|
+
location: { uuid: '', display: '' },
|
|
12
|
+
maxAppointmentsLimit: 0,
|
|
13
|
+
name: '',
|
|
14
|
+
startTime: '',
|
|
15
|
+
uuid: '',
|
|
16
|
+
color: '',
|
|
17
|
+
startTimeTimeFormat: new Date().getHours() >= 12 ? 'PM' : 'AM',
|
|
18
|
+
endTimeTimeFormat: new Date().getHours() >= 12 ? 'PM' : 'AM',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const addNewAppointmentService = (payload) => {
|
|
22
|
+
return openmrsFetch(`${restBaseUrl}/appointmentService`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
body: payload,
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const useAppointmentServices = () => {
|
|
30
|
+
return { appointmentServiceInitialValue, addNewAppointmentService };
|
|
31
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as Yup from 'yup';
|
|
2
|
+
import { OpenmrsResource } from '@openmrs/esm-framework';
|
|
3
|
+
|
|
4
|
+
export const validationSchema = Yup.object({
|
|
5
|
+
description: Yup.string().optional(),
|
|
6
|
+
durationMins: Yup.number().required('durationMinsRequired'),
|
|
7
|
+
endTime: Yup.string().required('endTimeRequired'),
|
|
8
|
+
initialAppointmentStatus: Yup.string().optional(),
|
|
9
|
+
location: Yup.object({ uuid: Yup.string(), display: Yup.string() }).required('locationRequired'),
|
|
10
|
+
maxAppointmentsLimit: Yup.number().required('maxAppointmentLimitRequired'),
|
|
11
|
+
name: Yup.string().required('appointmentServiceNameRequired'),
|
|
12
|
+
specialityUuid: Yup.string().optional(),
|
|
13
|
+
startTime: Yup.string().required('startTimeRequired'),
|
|
14
|
+
color: Yup.string().required('colorRequired'),
|
|
15
|
+
startTimeTimeFormat: Yup.string().required('startTimeFormatRequired'),
|
|
16
|
+
endTimeTimeFormat: Yup.string().required('endTimeFormatRequired'),
|
|
17
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Button, ButtonSet, Dropdown, Layer, SelectItem, TextInput, TimePicker, TimePickerSelect } from '@carbon/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { Form, Formik, type FormikHelpers } from 'formik';
|
|
5
|
+
import { validationSchema } from './appointment-services-validation';
|
|
6
|
+
import { useAppointmentServices } from './appointment-services-hook';
|
|
7
|
+
import { showSnackbar, useLocations } from '@openmrs/esm-framework';
|
|
8
|
+
import type { AppointmentService } from '../../types';
|
|
9
|
+
import { closeOverlay } from '../../hooks/useOverlay';
|
|
10
|
+
import styles from './appointment-services.scss';
|
|
11
|
+
import { appointmentLocationTagName } from '../../constants';
|
|
12
|
+
|
|
13
|
+
interface AppointmentServicesProps {}
|
|
14
|
+
|
|
15
|
+
const AppointmentServices: React.FC<AppointmentServicesProps> = () => {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const { appointmentServiceInitialValue, addNewAppointmentService } = useAppointmentServices();
|
|
18
|
+
|
|
19
|
+
const locations = useLocations(appointmentLocationTagName);
|
|
20
|
+
const handleSubmit = async (values: AppointmentService, helpers: FormikHelpers<AppointmentService>) => {
|
|
21
|
+
const payload = {
|
|
22
|
+
name: values.name,
|
|
23
|
+
startTime: values.startTime.concat(':00'),
|
|
24
|
+
endTime: values.endTime.concat(':00'),
|
|
25
|
+
durationMins: values.durationMins,
|
|
26
|
+
color: values.color,
|
|
27
|
+
locationUuid: values.location.uuid,
|
|
28
|
+
};
|
|
29
|
+
addNewAppointmentService(payload).then(
|
|
30
|
+
({ status }) => {
|
|
31
|
+
if (status === 200) {
|
|
32
|
+
showSnackbar({
|
|
33
|
+
isLowContrast: true,
|
|
34
|
+
kind: 'success',
|
|
35
|
+
subtitle: t('appointmentServiceCreate', 'Appointment service created successfully'),
|
|
36
|
+
title: t('appointmentService', 'Appointment service'),
|
|
37
|
+
});
|
|
38
|
+
closeOverlay();
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
(error) => {
|
|
42
|
+
showSnackbar({
|
|
43
|
+
title: t('errorCreatingAppointmentService', 'Error creating appointment service'),
|
|
44
|
+
kind: 'error',
|
|
45
|
+
subtitle: error?.message,
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
return (
|
|
51
|
+
<Formik
|
|
52
|
+
onSubmit={handleSubmit}
|
|
53
|
+
isInitialValid={false}
|
|
54
|
+
validationSchema={validationSchema}
|
|
55
|
+
initialValues={appointmentServiceInitialValue}>
|
|
56
|
+
{(props) => {
|
|
57
|
+
return (
|
|
58
|
+
<Form onSubmit={props.handleSubmit} className={styles.appointmentServiceContainer}>
|
|
59
|
+
<p className={styles.formTitle}>{t('createAppointmentService', 'Create appointment service')}</p>
|
|
60
|
+
<Layer>
|
|
61
|
+
<TextInput
|
|
62
|
+
id="name"
|
|
63
|
+
invalidText={t(props.errors.name)}
|
|
64
|
+
labelText={t('appointmentServiceName', 'Appointment service name')}
|
|
65
|
+
placeholder={t('appointmentServiceName', 'Appointment service name')}
|
|
66
|
+
invalid={!!(props.touched && props.errors.name)}
|
|
67
|
+
onChange={props.handleChange}
|
|
68
|
+
value={props.values.name}
|
|
69
|
+
name="name"
|
|
70
|
+
onBlue={props.handleBlur}
|
|
71
|
+
/>
|
|
72
|
+
</Layer>
|
|
73
|
+
<Layer>
|
|
74
|
+
<TimePicker
|
|
75
|
+
className={styles.timePickerInput}
|
|
76
|
+
invalid={!!(props.touched && props.errors.startTime)}
|
|
77
|
+
pattern="([\d]+:[\d]{2})"
|
|
78
|
+
name="startTime"
|
|
79
|
+
value={props.values.startTime}
|
|
80
|
+
onChange={props.handleChange}
|
|
81
|
+
labelText={t('startTime', 'Start Time')}
|
|
82
|
+
id="start-time-picker">
|
|
83
|
+
<TimePickerSelect
|
|
84
|
+
name="startTimeTimeFormat"
|
|
85
|
+
onChange={props.handleChange}
|
|
86
|
+
value={props.values.startTimeTimeFormat}
|
|
87
|
+
invalid={!!(props.touched && props.errors.startTimeTimeFormat)}
|
|
88
|
+
id="start-time-picker"
|
|
89
|
+
labelText={t('time', 'Time')}
|
|
90
|
+
aria-label={t('time', 'Time')}>
|
|
91
|
+
<SelectItem value="AM" text="AM" />
|
|
92
|
+
<SelectItem value="PM" text="PM" />
|
|
93
|
+
</TimePickerSelect>
|
|
94
|
+
</TimePicker>
|
|
95
|
+
</Layer>
|
|
96
|
+
|
|
97
|
+
<Layer>
|
|
98
|
+
<TimePicker
|
|
99
|
+
invalid={!!(props.touched && props.errors.endTime)}
|
|
100
|
+
className={styles.timePickerInput}
|
|
101
|
+
pattern="([\d]+:[\d]{2})"
|
|
102
|
+
value={props.values.endTime}
|
|
103
|
+
name="endTime"
|
|
104
|
+
onChange={props.handleChange}
|
|
105
|
+
labelText={t('endTime', 'End Time')}
|
|
106
|
+
id="end-time-picker">
|
|
107
|
+
<TimePickerSelect
|
|
108
|
+
name="endTimeTimeFormat"
|
|
109
|
+
onChange={props.handleChange}
|
|
110
|
+
id="end-time-picker"
|
|
111
|
+
value={props.values.endTimeTimeFormat}
|
|
112
|
+
labelText={t('time', 'Time')}
|
|
113
|
+
aria-label={t('time', 'Time')}>
|
|
114
|
+
<SelectItem value="AM" text="AM" />
|
|
115
|
+
<SelectItem value="PM" text="PM" />
|
|
116
|
+
</TimePickerSelect>
|
|
117
|
+
</TimePicker>
|
|
118
|
+
</Layer>
|
|
119
|
+
|
|
120
|
+
<Layer>
|
|
121
|
+
<TextInput
|
|
122
|
+
id="durationMins"
|
|
123
|
+
invalidText={props.errors.durationMins}
|
|
124
|
+
labelText={t('durationMins', 'Duration min')}
|
|
125
|
+
placeholder={t('durationMins', 'Duration min')}
|
|
126
|
+
invalid={!!(props.touched && props.errors.durationMins)}
|
|
127
|
+
onChange={props.handleChange}
|
|
128
|
+
value={props.values.durationMins}
|
|
129
|
+
name="durationMins"
|
|
130
|
+
/>
|
|
131
|
+
</Layer>
|
|
132
|
+
|
|
133
|
+
<Layer>
|
|
134
|
+
<Dropdown
|
|
135
|
+
id="default"
|
|
136
|
+
titleText={t('selectLocation', 'Select location')}
|
|
137
|
+
label={t('selectLocation', 'Select location')}
|
|
138
|
+
items={locations}
|
|
139
|
+
itemToString={(item) => (item ? item.display : '')}
|
|
140
|
+
selectedItem={props.values.location}
|
|
141
|
+
invalid={!!(props.touched && props.errors.location?.uuid)}
|
|
142
|
+
name="location"
|
|
143
|
+
onChange={({ selectedItem }) => props.setValues({ ...props.values, location: selectedItem })}
|
|
144
|
+
/>
|
|
145
|
+
</Layer>
|
|
146
|
+
|
|
147
|
+
<Layer>
|
|
148
|
+
<TextInput
|
|
149
|
+
invalid={!!(props.touched && props.errors.color)}
|
|
150
|
+
onChange={props.handleChange}
|
|
151
|
+
invalidText={props.errors.color}
|
|
152
|
+
labelText={t('appointmentColor', 'Appointment color')}
|
|
153
|
+
type="color"
|
|
154
|
+
name="color"
|
|
155
|
+
/>
|
|
156
|
+
</Layer>
|
|
157
|
+
|
|
158
|
+
<ButtonSet className={styles.buttonSet}>
|
|
159
|
+
<Button
|
|
160
|
+
onClick={closeOverlay}
|
|
161
|
+
style={{ maxWidth: 'none', width: '50%' }}
|
|
162
|
+
className={styles.button}
|
|
163
|
+
kind="secondary">
|
|
164
|
+
{t('discard', 'Discard')}
|
|
165
|
+
</Button>
|
|
166
|
+
<Button
|
|
167
|
+
disabled={!props.isValid}
|
|
168
|
+
style={{ maxWidth: 'none', width: '50%' }}
|
|
169
|
+
className={styles.button}
|
|
170
|
+
kind="primary"
|
|
171
|
+
type="submit">
|
|
172
|
+
{t('save', 'Save')}
|
|
173
|
+
</Button>
|
|
174
|
+
</ButtonSet>
|
|
175
|
+
</Form>
|
|
176
|
+
);
|
|
177
|
+
}}
|
|
178
|
+
</Formik>
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export default AppointmentServices;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
|
|
3
|
+
.appointmentServiceContainer {
|
|
4
|
+
margin: spacing.$spacing-05 spacing.$spacing-05;
|
|
5
|
+
|
|
6
|
+
& > div {
|
|
7
|
+
padding: spacing.$spacing-03 0;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.buttonSet {
|
|
12
|
+
position: absolute;
|
|
13
|
+
bottom: 0;
|
|
14
|
+
left: 0;
|
|
15
|
+
right: 0;
|
|
16
|
+
margin: 0 spacing.$spacing-05;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.button {
|
|
20
|
+
height: spacing.$spacing-10;
|
|
21
|
+
display: flex;
|
|
22
|
+
align-content: flex-start;
|
|
23
|
+
align-items: baseline;
|
|
24
|
+
width: auto;
|
|
25
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Tab, TabList, Tabs, TabPanel, TabPanels } from '@carbon/react';
|
|
4
|
+
|
|
5
|
+
import { type ConfigObject } from '../config-schema';
|
|
6
|
+
import { useConfig } from '@openmrs/esm-framework';
|
|
7
|
+
import ScheduledAppointments from './scheduled/scheduled-appointments.component';
|
|
8
|
+
import UnscheduledAppointments from './unscheduled/unscheduled-appointments.component';
|
|
9
|
+
import styles from './appointment-tabs.scss';
|
|
10
|
+
|
|
11
|
+
interface AppointmentTabsProps {
|
|
12
|
+
appointmentServiceType: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const AppointmentTabs: React.FC<AppointmentTabsProps> = ({ appointmentServiceType }) => {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const [activeTabIndex, setActiveTabIndex] = useState<number>(0);
|
|
18
|
+
const { showUnscheduledAppointmentsTab } = useConfig<ConfigObject>();
|
|
19
|
+
|
|
20
|
+
const handleTabChange = ({ selectedIndex }: { selectedIndex: number }) => {
|
|
21
|
+
setActiveTabIndex(selectedIndex);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className={styles.appointmentList} data-testid="appointment-list">
|
|
26
|
+
{showUnscheduledAppointmentsTab ? (
|
|
27
|
+
<Tabs selectedIndex={activeTabIndex} onChange={handleTabChange} className={styles.tabs}>
|
|
28
|
+
<TabList style={{ paddingLeft: '1rem' }} aria-label="Appointment tabs" contained>
|
|
29
|
+
<Tab className={styles.tab}>{t('scheduled', 'Scheduled')}</Tab>
|
|
30
|
+
<Tab className={styles.tab}>{t('unscheduled', 'Unscheduled')}</Tab>
|
|
31
|
+
</TabList>
|
|
32
|
+
<TabPanels>
|
|
33
|
+
<TabPanel className={styles.tabPanel}>
|
|
34
|
+
<ScheduledAppointments appointmentServiceType={appointmentServiceType} />
|
|
35
|
+
</TabPanel>
|
|
36
|
+
<TabPanel className={styles.tabPanel}>
|
|
37
|
+
<UnscheduledAppointments />
|
|
38
|
+
</TabPanel>
|
|
39
|
+
</TabPanels>
|
|
40
|
+
</Tabs>
|
|
41
|
+
) : (
|
|
42
|
+
<ScheduledAppointments appointmentServiceType={appointmentServiceType} />
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default AppointmentTabs;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
@import '../root.scss';
|
|
5
|
+
|
|
6
|
+
.appointmentList {
|
|
7
|
+
margin: 1rem;
|
|
8
|
+
|
|
9
|
+
& > div {
|
|
10
|
+
background-color: $ui-02;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.tabs {
|
|
15
|
+
grid-column: span 2;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.tab {
|
|
19
|
+
min-width: 12rem;
|
|
20
|
+
|
|
21
|
+
&:active,
|
|
22
|
+
&:focus {
|
|
23
|
+
outline: 2px solid var(--brand-03) !important;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&[aria-selected='true'] {
|
|
27
|
+
box-shadow: inset 0 2px 0 0 var(--brand-03) !important;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.tabPanel {
|
|
32
|
+
padding: 0;
|
|
33
|
+
margin: 1rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.calendarButton {
|
|
37
|
+
float: right;
|
|
38
|
+
position: absolute;
|
|
39
|
+
right: 0;
|
|
40
|
+
height: spacing.$spacing-09;
|
|
41
|
+
min-height: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.downloadButton {
|
|
45
|
+
margin: spacing.$spacing-05;
|
|
46
|
+
& > button {
|
|
47
|
+
border: 1px solid colors.$blue-60;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
.downloadLink {
|
|
51
|
+
text-decoration: none;
|
|
52
|
+
color: inherit;
|
|
53
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { openmrsFetch } from '@openmrs/esm-framework';
|
|
5
|
+
import { renderWithSwr, waitForLoadingToFinish } from 'tools';
|
|
6
|
+
import { mockAppointmentsData } from '__mocks__';
|
|
7
|
+
import AppointmentTabs from './appointment-tabs.component';
|
|
8
|
+
|
|
9
|
+
const mockOpenmrsFetch = openmrsFetch as jest.Mock;
|
|
10
|
+
|
|
11
|
+
describe('AppointmentTabs', () => {
|
|
12
|
+
xit(`renders tabs showing different appointment lists`, async () => {
|
|
13
|
+
const user = userEvent.setup();
|
|
14
|
+
|
|
15
|
+
mockOpenmrsFetch.mockReturnValueOnce({ data: mockAppointmentsData.data });
|
|
16
|
+
|
|
17
|
+
renderAppointmentTabs();
|
|
18
|
+
|
|
19
|
+
await waitForLoadingToFinish();
|
|
20
|
+
|
|
21
|
+
const scheduledAppointmentsTab = screen.getByRole('tab', { name: /^scheduled$/i });
|
|
22
|
+
const unsheduledAppointment = screen.getByRole('tab', { name: /^unscheduled$/i });
|
|
23
|
+
const pendingAppointments = screen.getByRole('tab', { name: /^unscheduled$/i });
|
|
24
|
+
|
|
25
|
+
expect(scheduledAppointmentsTab).toBeInTheDocument();
|
|
26
|
+
expect(unsheduledAppointment).toBeInTheDocument();
|
|
27
|
+
expect(pendingAppointments).toBeInTheDocument();
|
|
28
|
+
|
|
29
|
+
expect(scheduledAppointmentsTab).toHaveAttribute('aria-selected', 'true');
|
|
30
|
+
expect(unsheduledAppointment).toHaveAttribute('aria-selected', 'false');
|
|
31
|
+
expect(pendingAppointments).toHaveAttribute('aria-selected', 'false');
|
|
32
|
+
|
|
33
|
+
expect(screen.getByRole('button', { name: /add new appointment/i })).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText(/view calendar/i)).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
36
|
+
const expectedColumnHeaders = [/name/, /date & time/, /service type/, /provider/, /location/, /actions/];
|
|
37
|
+
expectedColumnHeaders.forEach((header) => {
|
|
38
|
+
expect(screen.getByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const expectedTableRows = [
|
|
42
|
+
/John Wilson 30-Aug-2021, 12:35 PM Outpatient HIV Clinic/,
|
|
43
|
+
/Elon Musketeer 14-Sept-2021, 07:50 AM Outpatient HIV Clinic/,
|
|
44
|
+
/Hopkins Derrick 14-Sept-2021, 12:50 PM Outpatient TB Clinic/,
|
|
45
|
+
/Amos Strong 15-Sept-2021, 01:32 PM Outpatient TB Clinic/,
|
|
46
|
+
];
|
|
47
|
+
expectedTableRows.forEach((row) => {
|
|
48
|
+
expect(screen.getByRole('row', { name: new RegExp(row, 'i') })).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function renderAppointmentTabs() {
|
|
54
|
+
renderWithSwr(<AppointmentTabs appointmentServiceType="" />);
|
|
55
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import isToday from 'dayjs/plugin/isToday';
|
|
4
|
+
import utc from 'dayjs/plugin/utc';
|
|
5
|
+
import { Button } from '@carbon/react';
|
|
6
|
+
import { TaskComplete } from '@carbon/react/icons';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import { navigate, showModal, useConfig } from '@openmrs/esm-framework';
|
|
9
|
+
import { type Appointment, AppointmentStatus } from '../../types';
|
|
10
|
+
import { type ConfigObject } from '../../config-schema';
|
|
11
|
+
import { useTodaysVisits } from '../../hooks/useTodaysVisits';
|
|
12
|
+
import CheckInButton from './checkin-button.component';
|
|
13
|
+
import styles from './appointments-actions.scss';
|
|
14
|
+
|
|
15
|
+
dayjs.extend(utc);
|
|
16
|
+
dayjs.extend(isToday);
|
|
17
|
+
|
|
18
|
+
interface AppointmentsActionsProps {
|
|
19
|
+
appointment: Appointment;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const AppointmentsActions: React.FC<AppointmentsActionsProps> = ({ appointment }) => {
|
|
23
|
+
const { t } = useTranslation();
|
|
24
|
+
const { checkInButton, checkOutButton } = useConfig<ConfigObject>();
|
|
25
|
+
const { visits, mutateVisit } = useTodaysVisits(); // TODO doesn't work if visit didn't start today? what about inpatient?
|
|
26
|
+
const patientUuid = appointment.patient.uuid;
|
|
27
|
+
const visitDate = dayjs(appointment.startDateTime);
|
|
28
|
+
const hasActiveVisitToday = visits?.some(
|
|
29
|
+
(visit) => visit?.patient?.uuid === patientUuid && visit?.startDatetime && !visit?.stopDatetime,
|
|
30
|
+
);
|
|
31
|
+
const isTodaysAppointment = visitDate.isToday();
|
|
32
|
+
const isCheckedIn = appointment.status === AppointmentStatus.CHECKEDIN;
|
|
33
|
+
const isCompleted = appointment.status === AppointmentStatus.COMPLETED;
|
|
34
|
+
const isCancelled = appointment.status === AppointmentStatus.CANCELLED;
|
|
35
|
+
|
|
36
|
+
const handleCheckout = () => {
|
|
37
|
+
if (checkOutButton.customUrl) {
|
|
38
|
+
navigate({ to: checkOutButton.customUrl, templateParams: { patientUuid, appointmentUuid: appointment.uuid } });
|
|
39
|
+
} else {
|
|
40
|
+
const dispose = showModal('end-appointment-modal', {
|
|
41
|
+
closeModal: () => {
|
|
42
|
+
mutateVisit();
|
|
43
|
+
dispose();
|
|
44
|
+
},
|
|
45
|
+
patientUuid,
|
|
46
|
+
appointmentUuid: appointment.uuid,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const renderVisitStatus = () => {
|
|
52
|
+
switch (true) {
|
|
53
|
+
case isCancelled:
|
|
54
|
+
return (
|
|
55
|
+
<Button kind="danger--ghost" iconDescription={t('cancelled', 'Cancelled')} size="sm">
|
|
56
|
+
{t('cancelled', 'Cancelled')}
|
|
57
|
+
</Button>
|
|
58
|
+
);
|
|
59
|
+
case isCompleted:
|
|
60
|
+
return (
|
|
61
|
+
<Button kind="ghost" renderIcon={TaskComplete} iconDescription={t('checkedOut', 'Checked out')} size="sm">
|
|
62
|
+
{t('checkedOut', 'Checked out')}
|
|
63
|
+
</Button>
|
|
64
|
+
);
|
|
65
|
+
case checkOutButton.enabled && isCheckedIn:
|
|
66
|
+
return (
|
|
67
|
+
<Button onClick={handleCheckout} kind="danger--tertiary" size="sm">
|
|
68
|
+
{t('checkOut', 'Check out')}
|
|
69
|
+
</Button>
|
|
70
|
+
);
|
|
71
|
+
case checkInButton.enabled && (!hasActiveVisitToday || checkInButton.showIfActiveVisit) && isTodaysAppointment: {
|
|
72
|
+
return <CheckInButton patientUuid={patientUuid} appointment={appointment} />;
|
|
73
|
+
}
|
|
74
|
+
default:
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className={styles.container}>
|
|
81
|
+
<>{renderVisitStatus()}</>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default AppointmentsActions;
|