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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/.turbo/turbo-build.log +42 -0
  2. package/dist/130.js +2 -0
  3. package/dist/130.js.LICENSE.txt +3 -0
  4. package/dist/130.js.map +1 -0
  5. package/dist/152.js +1 -0
  6. package/dist/152.js.map +1 -0
  7. package/dist/224.js +1 -0
  8. package/dist/224.js.map +1 -0
  9. package/dist/255.js +2 -0
  10. package/dist/255.js.LICENSE.txt +9 -0
  11. package/dist/255.js.map +1 -0
  12. package/dist/271.js +1 -0
  13. package/dist/303.js +1 -0
  14. package/dist/303.js.map +1 -0
  15. package/dist/309.js +1 -0
  16. package/dist/309.js.map +1 -0
  17. package/dist/319.js +1 -0
  18. package/dist/4.js +1 -0
  19. package/dist/4.js.map +1 -0
  20. package/dist/445.js +2 -0
  21. package/dist/445.js.LICENSE.txt +54 -0
  22. package/dist/445.js.map +1 -0
  23. package/dist/460.js +1 -0
  24. package/dist/501.js +1 -0
  25. package/dist/501.js.map +1 -0
  26. package/dist/574.js +1 -0
  27. package/dist/591.js +2 -0
  28. package/dist/591.js.LICENSE.txt +32 -0
  29. package/dist/591.js.map +1 -0
  30. package/dist/644.js +1 -0
  31. package/dist/729.js +1 -0
  32. package/dist/729.js.map +1 -0
  33. package/dist/757.js +1 -0
  34. package/dist/784.js +2 -0
  35. package/dist/784.js.LICENSE.txt +9 -0
  36. package/dist/784.js.map +1 -0
  37. package/dist/788.js +1 -0
  38. package/dist/807.js +1 -0
  39. package/dist/833.js +1 -0
  40. package/dist/857.js +2 -0
  41. package/dist/857.js.LICENSE.txt +5 -0
  42. package/dist/857.js.map +1 -0
  43. package/dist/904.js +1 -0
  44. package/dist/904.js.map +1 -0
  45. package/dist/kenyaemr-esm-appointments-app.js +1 -0
  46. package/dist/kenyaemr-esm-appointments-app.js.buildmanifest.json +699 -0
  47. package/dist/kenyaemr-esm-appointments-app.js.map +1 -0
  48. package/dist/main.js +2 -0
  49. package/dist/main.js.LICENSE.txt +64 -0
  50. package/dist/main.js.map +1 -0
  51. package/dist/routes.json +1 -0
  52. package/jest.config.js +3 -0
  53. package/package.json +57 -0
  54. package/src/admin/appointment-services/appointment-services-hook.ts +31 -0
  55. package/src/admin/appointment-services/appointment-services-validation.ts +17 -0
  56. package/src/admin/appointment-services/appointment-services.component.tsx +182 -0
  57. package/src/admin/appointment-services/appointment-services.scss +25 -0
  58. package/src/appointments/appointment-tabs.component.tsx +48 -0
  59. package/src/appointments/appointment-tabs.scss +53 -0
  60. package/src/appointments/appointment-tabs.test.tsx +55 -0
  61. package/src/appointments/common-components/appointments-actions.component.tsx +86 -0
  62. package/src/appointments/common-components/appointments-actions.scss +4 -0
  63. package/src/appointments/common-components/appointments-actions.test.tsx +201 -0
  64. package/src/appointments/common-components/appointments-table.component.tsx +277 -0
  65. package/src/appointments/common-components/appointments-table.scss +133 -0
  66. package/src/appointments/common-components/appointments-table.test.tsx +134 -0
  67. package/src/appointments/common-components/checkin-button.component.tsx +43 -0
  68. package/src/appointments/common-components/end-appointment-modal.component.tsx +104 -0
  69. package/src/appointments/common-components/end-appointment-modal.test.tsx +80 -0
  70. package/src/appointments/common-components/location-select-option.component.tsx +48 -0
  71. package/src/appointments/details/appointment-details.component.tsx +91 -0
  72. package/src/appointments/details/appointment-details.scss +81 -0
  73. package/src/appointments/details/appointment-details.test.tsx +103 -0
  74. package/src/appointments/scheduled/appointments-list.component.tsx +33 -0
  75. package/src/appointments/scheduled/early-appointments.component.tsx +32 -0
  76. package/src/appointments/scheduled/scheduled-appointments.component.tsx +215 -0
  77. package/src/appointments/scheduled/scheduled-appointments.scss +4 -0
  78. package/src/appointments/unscheduled/unscheduled-appointments.component.tsx +146 -0
  79. package/src/appointments/unscheduled/unscheduled-appointments.test.tsx +131 -0
  80. package/src/appointments/utils.tsx +80 -0
  81. package/src/appointments.component.tsx +44 -0
  82. package/src/appointments.test.tsx +15 -0
  83. package/src/calendar/appointments-calendar-view-view.scss +24 -0
  84. package/src/calendar/appointments-calendar-view.component.tsx +36 -0
  85. package/src/calendar/appointments-calendar-view.test.tsx +22 -0
  86. package/src/calendar/header/calendar-header.component.tsx +34 -0
  87. package/src/calendar/header/calendar-header.scss +32 -0
  88. package/src/calendar/monthly/days-of-week.component.tsx +16 -0
  89. package/src/calendar/monthly/days-of-week.scss +33 -0
  90. package/src/calendar/monthly/monthly-calendar-view.component.tsx +34 -0
  91. package/src/calendar/monthly/monthly-header.module.scss +14 -0
  92. package/src/calendar/monthly/monthly-header.module.tsx +40 -0
  93. package/src/calendar/monthly/monthly-view-workload.scss +188 -0
  94. package/src/calendar/monthly/monthly-workload-view-expanded.component.tsx +42 -0
  95. package/src/calendar/monthly/monthly-workload-view.component.tsx +109 -0
  96. package/src/config-schema.ts +151 -0
  97. package/src/constants.ts +55 -0
  98. package/src/createDashboardLink.component.tsx +39 -0
  99. package/src/dashboard.meta.ts +21 -0
  100. package/src/declarations.d.ts +4 -0
  101. package/src/empty-state/empty-data-illustration.component.tsx +39 -0
  102. package/src/empty-state/empty-state.component.tsx +32 -0
  103. package/src/empty-state/empty-state.scss +69 -0
  104. package/src/form/appointments-form.component.tsx +891 -0
  105. package/src/form/appointments-form.resource.ts +165 -0
  106. package/src/form/appointments-form.scss +113 -0
  107. package/src/form/appointments-form.test.tsx +212 -0
  108. package/src/header/appointments-header.component.tsx +79 -0
  109. package/src/header/appointments-header.scss +95 -0
  110. package/src/header/appointments-illustration.component.tsx +22 -0
  111. package/src/helpers/excel.ts +61 -0
  112. package/src/helpers/functions.ts +82 -0
  113. package/src/helpers/index.ts +2 -0
  114. package/src/helpers/time.tsx +15 -0
  115. package/src/home/home-appointments.component.tsx +22 -0
  116. package/src/home/home-appointments.scss +10 -0
  117. package/src/hooks/patientAppointmentContext.ts +15 -0
  118. package/src/hooks/selectedDateContext.ts +10 -0
  119. package/src/hooks/useAppointmentList.ts +48 -0
  120. package/src/hooks/useAppointmentService.ts +11 -0
  121. package/src/hooks/useAppointmentsCalendar.ts +68 -0
  122. package/src/hooks/useClinicalMetrics.ts +79 -0
  123. package/src/hooks/useDefaultLocation.ts +14 -0
  124. package/src/hooks/useOverlay.tsx +45 -0
  125. package/src/hooks/usePatientAppointmentHistory.ts +49 -0
  126. package/src/hooks/useProviders.ts +18 -0
  127. package/src/hooks/useTodaysVisits.ts +19 -0
  128. package/src/hooks/useUnscheduledAppointments.ts +45 -0
  129. package/src/index.ts +111 -0
  130. package/src/metrics/appointments-metrics.component.tsx +71 -0
  131. package/src/metrics/appointments-metrics.scss +15 -0
  132. package/src/metrics/appointments-metrics.test.tsx +49 -0
  133. package/src/metrics/metrics-card.component.tsx +76 -0
  134. package/src/metrics/metrics-card.scss +77 -0
  135. package/src/metrics/metrics-header.component.tsx +62 -0
  136. package/src/metrics/metrics-header.scss +33 -0
  137. package/src/past-visit/encounter-list.component.tsx +54 -0
  138. package/src/past-visit/past-visit.component.tsx +106 -0
  139. package/src/past-visit/past-visit.resource.ts +25 -0
  140. package/src/past-visit/past-visit.scss +106 -0
  141. package/src/patient-appointments/patient-appointments-action-menu.component.tsx +65 -0
  142. package/src/patient-appointments/patient-appointments-action-menu.scss +7 -0
  143. package/src/patient-appointments/patient-appointments-base.component.tsx +165 -0
  144. package/src/patient-appointments/patient-appointments-base.scss +85 -0
  145. package/src/patient-appointments/patient-appointments-base.test.tsx +91 -0
  146. package/src/patient-appointments/patient-appointments-cancel-modal.component.tsx +66 -0
  147. package/src/patient-appointments/patient-appointments-detailed-summary.component.tsx +15 -0
  148. package/src/patient-appointments/patient-appointments-header.scss +27 -0
  149. package/src/patient-appointments/patient-appointments-header.tsx +42 -0
  150. package/src/patient-appointments/patient-appointments-overview.component.tsx +35 -0
  151. package/src/patient-appointments/patient-appointments-overview.scss +7 -0
  152. package/src/patient-appointments/patient-appointments-table.scss +0 -0
  153. package/src/patient-appointments/patient-appointments-table.tsx +128 -0
  154. package/src/patient-appointments/patient-appointments.resource.ts +72 -0
  155. package/src/patient-appointments/patient-upcoming-appointments-card.component.tsx +122 -0
  156. package/src/patient-appointments/patient-upcoming-appointments-card.scss +46 -0
  157. package/src/patient-search/patient-search.component.tsx +34 -0
  158. package/src/patient-search/patient-search.scss +23 -0
  159. package/src/root.component.tsx +26 -0
  160. package/src/root.scss +50 -0
  161. package/src/routes.json +153 -0
  162. package/src/scheduled-appointments-config-schema.ts +169 -0
  163. package/src/types/index.ts +189 -0
  164. package/src/workload/monthly-view-workload/monthly-view.component.tsx +69 -0
  165. package/src/workload/monthly-view-workload/monthly-workload.scss +223 -0
  166. package/src/workload/monthly-view-workload/monthlyWorkCard.tsx +45 -0
  167. package/src/workload/workload-card.component.tsx +31 -0
  168. package/src/workload/workload.component.tsx +47 -0
  169. package/src/workload/workload.resource.ts +78 -0
  170. package/src/workload/workload.scss +92 -0
  171. package/translations/am.json +148 -0
  172. package/translations/ar.json +148 -0
  173. package/translations/en.json +159 -0
  174. package/translations/es.json +148 -0
  175. package/translations/fr.json +148 -0
  176. package/translations/he.json +148 -0
  177. package/translations/km.json +148 -0
  178. package/translations/zh.json +148 -0
  179. package/translations/zh_CN.json +148 -0
  180. package/tsconfig.json +5 -0
  181. package/webpack.config.js +1 -0
@@ -0,0 +1 @@
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
@@ -0,0 +1,3 @@
1
+ const rootConfig = require('../../jest.config.js');
2
+
3
+ module.exports = rootConfig;
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;
@@ -0,0 +1,4 @@
1
+ .container {
2
+ display: flex;
3
+ align-items: center;
4
+ }