@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,106 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .wrapper {
6
+ margin-top: 0.5rem;
7
+ }
8
+
9
+ .bodyShort02 {
10
+ @include type.type-style('body-compact-02');
11
+ }
12
+
13
+ .bodyLong01 {
14
+ @include type.type-style('body-01');
15
+ }
16
+
17
+ .date {
18
+ @include type.type-style('label-01');
19
+ color: $text-02;
20
+ margin: 0.5rem 0rem 0.25rem;
21
+ }
22
+
23
+ .visitType {
24
+ @include type.type-style('heading-compact-02');
25
+ }
26
+
27
+ .container {
28
+ background-color: $ui-background;
29
+ border: 1px solid $grey-2;
30
+ padding: spacing.$spacing-05;
31
+ margin: spacing.$spacing-05 0rem spacing.$spacing-05;
32
+ width: 100%;
33
+ }
34
+
35
+ .tabs > :global(.cds--tab-content) {
36
+ padding: spacing.$spacing-05 0rem !important;
37
+ }
38
+
39
+ .header::after {
40
+ content: '';
41
+ display: block;
42
+ width: spacing.$spacing-07;
43
+ padding-top: 0.188rem;
44
+ border-bottom: 0.375rem solid var(--brand-03);
45
+ }
46
+
47
+ .visitContainer {
48
+ background-color: $ui-background;
49
+ display: grid;
50
+ grid-template-columns: max-content auto;
51
+ }
52
+
53
+ .flexSections {
54
+ display: flex;
55
+ }
56
+
57
+ .verticalTabs {
58
+ margin: spacing.$spacing-04 0;
59
+
60
+ &:global(.cds--tabs--scrollable .cds--tabs--scrollable__nav-item + .cds--tabs--scrollable__nav-item) {
61
+ margin-left: 0rem;
62
+ }
63
+ }
64
+
65
+ .verticalTabs > ul {
66
+ flex-direction: column !important;
67
+ }
68
+
69
+ .desktopTabs {
70
+ button {
71
+ height: 32px;
72
+ }
73
+ }
74
+
75
+ .tabletTabs {
76
+ button {
77
+ height: 48px;
78
+ }
79
+ }
80
+
81
+ .tab > button {
82
+ border-bottom: 0 !important;
83
+ border-left: 3px solid $ui-03 !important;
84
+ }
85
+
86
+ .tab > button,
87
+ .tab > button:focus,
88
+ .tab > button:active {
89
+ outline: 0 !important;
90
+ outline-offset: 0 !important;
91
+ }
92
+
93
+ .selectedTab > button {
94
+ border-left: 3px solid var(--brand-03) !important;
95
+ border-bottom: 0 !important;
96
+ font-weight: 600 !important;
97
+ }
98
+
99
+ .tabContent {
100
+ border-top: 1px solid $ui-03;
101
+ padding: spacing.$spacing-05 0;
102
+ width: 70%;
103
+ }
104
+ .textColor {
105
+ color: $color-blue-60-2;
106
+ }
@@ -0,0 +1,65 @@
1
+ import React, { useCallback, useContext } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+
4
+ import { Layer, OverflowMenu, OverflowMenuItem } from '@carbon/react';
5
+ import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';
6
+ import { launchWorkspace, showModal, useLayoutType } from '@openmrs/esm-framework';
7
+ import type { Appointment } from '../types';
8
+ import styles from './patient-appointments-action-menu.scss';
9
+
10
+ import PatientAppointmentContext, { PatientAppointmentContextTypes } from '../hooks/patientAppointmentContext';
11
+
12
+ interface appointmentsActionMenuProps {
13
+ appointment: Appointment;
14
+ patientUuid: string;
15
+ }
16
+
17
+ export const PatientAppointmentsActionMenu = ({ appointment, patientUuid }: appointmentsActionMenuProps) => {
18
+ const { t } = useTranslation();
19
+ const isTablet = useLayoutType() === 'tablet';
20
+ const patientAppointmentContext = useContext(PatientAppointmentContext);
21
+
22
+ const launchEditAppointmentForm = useCallback(() => {
23
+ if (patientAppointmentContext === PatientAppointmentContextTypes.PATIENT_CHART) {
24
+ launchPatientWorkspace('appointments-form-workspace', {
25
+ workspaceTitle: t('editAppointment', 'Edit an appointment'),
26
+ appointment,
27
+ context: 'editing',
28
+ });
29
+ } else {
30
+ launchWorkspace('edit-appointments-form', {
31
+ context: 'editing',
32
+ appointment,
33
+ });
34
+ }
35
+ }, [appointment, t]);
36
+
37
+ const launchCancelAppointmentDialog = () => {
38
+ const dispose = showModal('patient-appointment-cancel-confirmation-dialog', {
39
+ closeCancelModal: () => dispose(),
40
+ appointmentUuid: appointment.uuid,
41
+ patientUuid,
42
+ });
43
+ };
44
+
45
+ return (
46
+ <Layer className={styles.layer}>
47
+ <OverflowMenu aria-label="Edit or delete appointment" size={isTablet ? 'lg' : 'sm'} flipped align="left">
48
+ <OverflowMenuItem
49
+ className={styles.menuItem}
50
+ id="editAppointment"
51
+ onClick={launchEditAppointmentForm}
52
+ itemText={t('edit', 'Edit')}
53
+ />
54
+ <OverflowMenuItem
55
+ className={styles.menuItem}
56
+ id="cancelAppointment"
57
+ itemText={t('cancel', 'Cancel')}
58
+ onClick={launchCancelAppointmentDialog}
59
+ isDelete={true}
60
+ hasDivider
61
+ />
62
+ </OverflowMenu>
63
+ </Layer>
64
+ );
65
+ };
@@ -0,0 +1,7 @@
1
+ .layer {
2
+ height: 100%;
3
+ }
4
+
5
+ .menuItem {
6
+ max-width: none;
7
+ }
@@ -0,0 +1,165 @@
1
+ import React, { useContext, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import dayjs from 'dayjs';
4
+ import { Button, ContentSwitcher, DataTableSkeleton, InlineLoading, Layer, Switch, Tile } from '@carbon/react';
5
+ import { Add } from '@carbon/react/icons';
6
+ import { launchWorkspace, useLayoutType } from '@openmrs/esm-framework';
7
+ import { CardHeader, EmptyDataIllustration, ErrorState, launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';
8
+ import { usePatientAppointments } from './patient-appointments.resource';
9
+ import PatientAppointmentsTable from './patient-appointments-table';
10
+ import styles from './patient-appointments-base.scss';
11
+
12
+ import PatientAppointmentContext, { PatientAppointmentContextTypes } from '../hooks/patientAppointmentContext';
13
+
14
+ interface PatientAppointmentsBaseProps {
15
+ patientUuid: string;
16
+ }
17
+
18
+ enum AppointmentTypes {
19
+ UPCOMING = 0,
20
+ TODAY = 1,
21
+ PAST = 2,
22
+ }
23
+
24
+ const PatientAppointmentsBase: React.FC<PatientAppointmentsBaseProps> = ({ patientUuid }) => {
25
+ const { t } = useTranslation();
26
+ const headerTitle = t('appointments', 'Appointments');
27
+ const isTablet = useLayoutType() === 'tablet';
28
+ const patientAppointmentContext = useContext(PatientAppointmentContext);
29
+ const [switchedView, setSwitchedView] = useState(false);
30
+
31
+ const [contentSwitcherValue, setContentSwitcherValue] = useState(0);
32
+ const startDate = dayjs(new Date().toISOString()).subtract(6, 'month').toISOString();
33
+ const {
34
+ data: appointmentsData,
35
+ isError,
36
+ isLoading,
37
+ isValidating,
38
+ } = usePatientAppointments(patientUuid, startDate, new AbortController());
39
+
40
+ const launchAppointmentsForm = () => {
41
+ if (patientAppointmentContext === PatientAppointmentContextTypes.PATIENT_CHART) {
42
+ launchPatientWorkspace('appointments-form-workspace');
43
+ } else {
44
+ launchWorkspace('add-appointment', {
45
+ context: 'creating',
46
+ patientUuid,
47
+ });
48
+ }
49
+ };
50
+
51
+ if (isLoading) return <DataTableSkeleton role="progressbar" compact={!isTablet} zebra />;
52
+ if (isError) {
53
+ return <ErrorState headerTitle={headerTitle} error={isError} />;
54
+ }
55
+ if (Object.keys(appointmentsData)?.length) {
56
+ return (
57
+ <div className={styles.widgetCard}>
58
+ <CardHeader title={headerTitle}>
59
+ {isValidating ? (
60
+ <span>
61
+ <InlineLoading />
62
+ </span>
63
+ ) : null}
64
+ <div className={styles.contentSwitcherWrapper}>
65
+ <ContentSwitcher
66
+ size={isTablet ? 'md' : 'sm'}
67
+ onChange={({ index }) => {
68
+ setContentSwitcherValue(index);
69
+ setSwitchedView(true);
70
+ }}>
71
+ <Switch name={'upcoming'} text={t('upcoming', 'Upcoming')} />
72
+ <Switch name={'today'} text={t('today', 'Today')} />
73
+ <Switch name={'past'} text={t('past', 'Past')} />
74
+ </ContentSwitcher>
75
+ <div className={styles.divider}>|</div>
76
+ <Button
77
+ kind="ghost"
78
+ renderIcon={(props) => <Add size={16} {...props} />}
79
+ iconDescription="Add Appointments"
80
+ onClick={launchAppointmentsForm}>
81
+ {t('add', 'Add')}
82
+ </Button>
83
+ </div>
84
+ </CardHeader>
85
+ {(() => {
86
+ if (contentSwitcherValue === AppointmentTypes.UPCOMING) {
87
+ if (appointmentsData.upcomingAppointments?.length) {
88
+ return (
89
+ <PatientAppointmentsTable
90
+ patientAppointments={appointmentsData?.upcomingAppointments}
91
+ switchedView={switchedView}
92
+ setSwitchedView={setSwitchedView}
93
+ patientUuid={patientUuid}
94
+ />
95
+ );
96
+ }
97
+ return (
98
+ <Layer>
99
+ <Tile className={styles.tile}>
100
+ <EmptyDataIllustration />
101
+ <p className={styles.content}>
102
+ {t(
103
+ 'noUpcomingAppointmentsForPatient',
104
+ 'There are no upcoming appointments to display for this patient',
105
+ )}
106
+ </p>
107
+ </Tile>
108
+ </Layer>
109
+ );
110
+ }
111
+ if (contentSwitcherValue === AppointmentTypes.TODAY) {
112
+ if (appointmentsData.todaysAppointments?.length) {
113
+ return (
114
+ <PatientAppointmentsTable
115
+ patientAppointments={appointmentsData?.todaysAppointments}
116
+ switchedView={switchedView}
117
+ setSwitchedView={setSwitchedView}
118
+ patientUuid={patientUuid}
119
+ />
120
+ );
121
+ }
122
+ return (
123
+ <Layer>
124
+ <Tile className={styles.tile}>
125
+ <EmptyDataIllustration />
126
+ <p className={styles.content}>
127
+ {t(
128
+ 'noCurrentAppointments',
129
+ 'There are no appointments scheduled for today to display for this patient',
130
+ )}
131
+ </p>
132
+ </Tile>
133
+ </Layer>
134
+ );
135
+ }
136
+
137
+ if (contentSwitcherValue === AppointmentTypes.PAST) {
138
+ if (appointmentsData.pastAppointments?.length) {
139
+ return (
140
+ <PatientAppointmentsTable
141
+ patientAppointments={appointmentsData?.pastAppointments}
142
+ switchedView={switchedView}
143
+ setSwitchedView={setSwitchedView}
144
+ patientUuid={patientUuid}
145
+ />
146
+ );
147
+ }
148
+ return (
149
+ <Layer>
150
+ <Tile className={styles.tile}>
151
+ <EmptyDataIllustration />
152
+ <p className={styles.content}>
153
+ {t('noPastAppointments', 'There are no past appointments to display for this patient')}
154
+ </p>
155
+ </Tile>
156
+ </Layer>
157
+ );
158
+ }
159
+ })()}
160
+ </div>
161
+ );
162
+ }
163
+ };
164
+
165
+ export default PatientAppointmentsBase;
@@ -0,0 +1,85 @@
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
+ // TO DO Move this styles to style - guide
7
+ // https://github.com/openmrs/openmrs-esm-core/blob/master/packages/framework/esm-styleguide/src/_vars.scss
8
+ $color-blue-30: #a6c8ff;
9
+ $color-blue-10: #edf5ff;
10
+
11
+ .widgetCard {
12
+ border: 1px solid $ui-03;
13
+ max-width: 60rem;
14
+ margin: auto;
15
+ margin-top: spacing.$spacing-05;
16
+ }
17
+
18
+ .productiveHeading01 {
19
+ @include type.type-style('heading-compact-01');
20
+ }
21
+
22
+ .contentSwitcherWrapper {
23
+ display: flex;
24
+ justify-content: flex-end;
25
+ align-items: center;
26
+ width: 60%;
27
+ }
28
+
29
+ .contentSwitcherWrapper > div > button {
30
+ background-color: $ui-02;
31
+ }
32
+
33
+ .contentSwitcherWrapper > div button:first-child {
34
+ border-top: 1px solid $color-blue-30;
35
+ border-bottom: 1px solid $color-blue-30;
36
+ border-left: 1px solid $color-blue-30;
37
+ border-right: none;
38
+ border-radius: spacing.$spacing-02 0 0px spacing.$spacing-02;
39
+ }
40
+
41
+ .contentSwitcherWrapper > div button:last-child {
42
+ border-top: 1px solid $color-blue-30;
43
+ border-bottom: 1px solid $color-blue-30;
44
+ border-right: 1px solid $color-blue-30;
45
+ border-left: none;
46
+ border-radius: 0px spacing.$spacing-02 spacing.$spacing-02 0px;
47
+ }
48
+
49
+ .contentSwitcherWrapper > div > button[aria-selected='true'],
50
+ .contentSwitcherWrapper > div > button[aria-selected='true']:first-child {
51
+ background-color: $color-blue-10;
52
+ color: $color-blue-60-2;
53
+ border-color: $color-blue-60-2;
54
+ border-right: 1px solid $color-blue-60-2;
55
+ }
56
+
57
+ .contentSwitcherWrapper > div > button[aria-selected='true'],
58
+ .contentSwitcherWrapper > div > button[aria-selected='true']:last-child {
59
+ background-color: $color-blue-10;
60
+ color: $color-blue-60-2;
61
+ border-color: $color-blue-60-2;
62
+ border-left: 1px solid $color-blue-60-2;
63
+ }
64
+
65
+ .contentSwitcherWrapper > div > button[aria-selected='true']:focus {
66
+ box-shadow: none;
67
+ }
68
+
69
+ .divider {
70
+ width: 1px;
71
+ height: spacing.$spacing-05;
72
+ color: colors.$gray-20;
73
+ margin: 0 spacing.$spacing-05;
74
+ }
75
+
76
+ .content {
77
+ @include type.type-style('heading-compact-01');
78
+ color: $text-02;
79
+ margin-top: spacing.$spacing-05;
80
+ margin-bottom: spacing.$spacing-03;
81
+ }
82
+
83
+ .tile {
84
+ text-align: center;
85
+ }
@@ -0,0 +1,91 @@
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 { mockAppointmentsData } from '__mocks__';
6
+ import { mockPatient, patientChartBasePath, renderWithSwr, waitForLoadingToFinish } from 'tools';
7
+ import AppointmentsBase from './patient-appointments-base.component';
8
+
9
+ const testProps = {
10
+ basePath: patientChartBasePath,
11
+ patientUuid: mockPatient.id,
12
+ };
13
+
14
+ const mockOpenmrsFetch = openmrsFetch as jest.Mock;
15
+
16
+ describe('AppointmensOverview', () => {
17
+ it('renders an empty state if appointments data is unavailable', async () => {
18
+ mockOpenmrsFetch.mockReturnValueOnce({ data: [] });
19
+
20
+ renderAppointments();
21
+
22
+ await waitForLoadingToFinish();
23
+
24
+ expect(screen.getByRole('heading', { name: /appointments/i })).toBeInTheDocument();
25
+ expect(screen.getByRole('button', { name: /add/i })).toBeInTheDocument();
26
+ });
27
+
28
+ it('renders an error state if there was a problem fetching appointments data', async () => {
29
+ const error = {
30
+ message: 'Internal server error',
31
+ response: {
32
+ status: 500,
33
+ statusText: 'Internal server error',
34
+ },
35
+ };
36
+
37
+ mockOpenmrsFetch.mockRejectedValueOnce(error);
38
+
39
+ renderAppointments();
40
+
41
+ await waitForLoadingToFinish();
42
+
43
+ expect(screen.getByRole('heading', { name: /appointments/i })).toBeInTheDocument();
44
+ expect(
45
+ screen.getByText(
46
+ 'Sorry, there was a problem displaying this information. You can try to reload this page, or contact the site administrator and quote the error code above.',
47
+ ),
48
+ ).toBeInTheDocument();
49
+ });
50
+
51
+ it(`renders a tabular overview of the patient's appointment schedule if available`, async () => {
52
+ const user = userEvent.setup();
53
+
54
+ mockOpenmrsFetch.mockReturnValueOnce(mockAppointmentsData);
55
+
56
+ renderAppointments();
57
+
58
+ await waitForLoadingToFinish();
59
+
60
+ expect(screen.getByRole('heading', { name: /appointments/i })).toBeInTheDocument();
61
+ expect(screen.getByRole('button', { name: /add/i })).toBeInTheDocument();
62
+
63
+ const upcomingAppointmentsTab = screen.getByRole('tab', { name: /upcoming/i });
64
+ const pastAppointmentsTab = screen.getByRole('tab', { name: /past/i });
65
+
66
+ expect(screen.getByRole('tablist')).toContainElement(upcomingAppointmentsTab);
67
+ expect(screen.getByRole('tablist')).toContainElement(pastAppointmentsTab);
68
+ expect(screen.getByTitle(/Empty data illustration/i)).toBeInTheDocument();
69
+ expect(screen.getByText(/There are no upcoming appointments to display for this patient/i)).toBeInTheDocument();
70
+
71
+ await user.click(pastAppointmentsTab);
72
+ expect(screen.getByRole('table')).toBeInTheDocument();
73
+
74
+ const expectedColumnHeaders = [/date/, /location/, /service/];
75
+ expectedColumnHeaders.forEach((header) => {
76
+ expect(screen.getByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument();
77
+ });
78
+
79
+ expect(screen.getAllByRole('row').length).toEqual(7); // 7 appts in mock data + header row
80
+
81
+ const previousPageButton = screen.getByRole('button', { name: /previous page/i });
82
+ const nextPageButton = screen.getByRole('button', { name: /next page/i });
83
+
84
+ expect(previousPageButton).toBeDisabled();
85
+ expect(nextPageButton).toBeDisabled();
86
+ });
87
+ });
88
+
89
+ function renderAppointments() {
90
+ renderWithSwr(<AppointmentsBase {...testProps} />);
91
+ }
@@ -0,0 +1,66 @@
1
+ import React, { useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
4
+ import { showSnackbar } from '@openmrs/esm-framework';
5
+ import { changeAppointmentStatus, usePatientAppointments } from './patient-appointments.resource';
6
+
7
+ interface PatientCancelAppointmentModalProps {
8
+ closeCancelModal: () => void;
9
+ appointmentUuid: string;
10
+ patientUuid: string;
11
+ }
12
+
13
+ const PatientCancelAppointmentModal: React.FC<PatientCancelAppointmentModalProps> = ({
14
+ closeCancelModal,
15
+ appointmentUuid,
16
+ patientUuid,
17
+ }) => {
18
+ const { t } = useTranslation();
19
+ const { mutate } = usePatientAppointments(patientUuid, new Date().toUTCString(), new AbortController());
20
+ const [isSubmitting, setIsSubmitting] = useState(false);
21
+
22
+ const handleCancel = async () => {
23
+ setIsSubmitting(true);
24
+
25
+ changeAppointmentStatus('Cancelled', appointmentUuid)
26
+ .then(({ status }) => {
27
+ if (status === 200) {
28
+ mutate();
29
+ closeCancelModal();
30
+ showSnackbar({
31
+ isLowContrast: true,
32
+ kind: 'success',
33
+ subtitle: t('appointmentCancelledSuccessfully', 'Appointment cancelled successfully'),
34
+ title: t('appointmentCancelled', 'Appointment cancelled'),
35
+ });
36
+ }
37
+ })
38
+ .catch((err) => {
39
+ showSnackbar({
40
+ title: t('appointmentCancelError', 'Error cancelling appointment'),
41
+ kind: 'error',
42
+ isLowContrast: true,
43
+ subtitle: err?.message,
44
+ });
45
+ });
46
+ };
47
+
48
+ return (
49
+ <div>
50
+ <ModalHeader closeModal={closeCancelModal} title={t('cancelAppointment', 'Cancel appointment')} />
51
+ <ModalBody>
52
+ <p>{t('cancelAppointmentModalConfirmationText', 'Are you sure you want to cancel this appointment?')}</p>
53
+ </ModalBody>
54
+ <ModalFooter>
55
+ <Button kind="secondary" onClick={closeCancelModal}>
56
+ {t('discard', 'Discard')}
57
+ </Button>
58
+ <Button kind="danger" onClick={handleCancel} disabled={isSubmitting}>
59
+ {t('cancelAppointment', 'Cancel appointment')}
60
+ </Button>
61
+ </ModalFooter>
62
+ </div>
63
+ );
64
+ };
65
+
66
+ export default PatientCancelAppointmentModal;
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import PatientAppointmentsBase from './patient-appointments-base.component';
3
+
4
+ interface PatientAppointmentsDetailedSummaryProps {
5
+ patientUuid: string;
6
+ }
7
+ /**
8
+ * This component is wired in as an extension to render the patient appointments view (all appointments for a single patient) within of the context of the patient chart.
9
+ * It uses the PatientAppointmentsBase component to render the actual appointments data.
10
+ */
11
+ const PatientAppointmentsDetailedSummary: React.FC<PatientAppointmentsDetailedSummaryProps> = ({ patientUuid }) => {
12
+ return <PatientAppointmentsBase patientUuid={patientUuid} />;
13
+ };
14
+
15
+ export default PatientAppointmentsDetailedSummary;
@@ -0,0 +1,27 @@
1
+ @use '@carbon/styles/scss/type';
2
+ @use '@carbon/colors';
3
+ @use '@carbon/layout';
4
+
5
+ .patientBanner {
6
+ display: flex;
7
+ }
8
+
9
+ .patientAvatar {
10
+ width: 5rem;
11
+ height: 5rem;
12
+ margin: 1rem;
13
+ border-radius: 1px;
14
+ }
15
+
16
+ .divider {
17
+ border: 0;
18
+ border-top: 0.075rem solid colors.$gray-20;
19
+ width: 100%;
20
+ }
21
+
22
+ // Overriding styles for RTL support
23
+ html[dir='rtl'] {
24
+ .titleContent {
25
+ margin-left: 1rem;
26
+ }
27
+ }
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { ArrowLeft } from '@carbon/react/icons';
4
+ import { Button } from '@carbon/react';
5
+ import { useNavigate } from 'react-router-dom';
6
+ import styles from './patient-appointments-header.scss';
7
+ import { PatientBannerPatientInfo, PatientPhoto, displayName } from '@openmrs/esm-framework';
8
+
9
+ interface PatientAppointmentsHeaderProps {
10
+ patient: fhir.Patient;
11
+ }
12
+
13
+ const PatientAppointmentsHeader: React.FC<PatientAppointmentsHeaderProps> = ({ patient }) => {
14
+ const { t } = useTranslation();
15
+ const navigate = useNavigate();
16
+ const patientName = displayName(patient);
17
+
18
+ return (
19
+ <div>
20
+ <div className={styles.titleContainer}>
21
+ <Button
22
+ kind="ghost"
23
+ onClick={() => navigate(-1)}
24
+ renderIcon={ArrowLeft}
25
+ iconDescription={t('back', 'Back')}
26
+ size="lg">
27
+ <span>{t('back', 'Back')}</span>
28
+ </Button>
29
+ </div>
30
+ <div className={styles.divider}></div>
31
+ <div className={styles.patientBanner}>
32
+ <div className={styles.patientAvatar} role="img">
33
+ <PatientPhoto patientUuid={patient.id} patientName={patientName} />
34
+ </div>
35
+ <PatientBannerPatientInfo patient={patient}></PatientBannerPatientInfo>
36
+ </div>
37
+ <div className={styles.divider}></div>
38
+ </div>
39
+ );
40
+ };
41
+
42
+ export default PatientAppointmentsHeader;