@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,15 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import Appointments from './appointments.component';
4
+
5
+ describe('Appointments', () => {
6
+ it('should render correctly', () => {
7
+ render(<Appointments />);
8
+
9
+ expect(screen.getByTitle(/patient queue illustration/i)).toBeInTheDocument();
10
+ expect(screen.getByText(/^appointments$/i)).toBeInTheDocument();
11
+ expect(screen.getByText(/home/i)).toBeInTheDocument();
12
+ expect(screen.getByPlaceholderText(/DD-MMM-YYYY/i)).toBeInTheDocument();
13
+ expect(screen.getByRole('button', { name: /appointments calendar/i })).toBeInTheDocument();
14
+ });
15
+ });
@@ -0,0 +1,24 @@
1
+ @use '@carbon/styles/scss/type';
2
+ @use '@carbon/styles/scss/spacing';
3
+ @use '@carbon/colors';
4
+ @import '~@openmrs/esm-styleguide/src/vars';
5
+
6
+ .wrapper {
7
+ position: relative;
8
+ }
9
+ .monthlyCalendar {
10
+ display: grid;
11
+ grid-template-columns: repeat(7, minmax(0px, 1fr));
12
+ grid-template-rows: repeat(6, minmax(0px, 1fr));
13
+ }
14
+
15
+ .calendarViewContainer {
16
+ margin: spacing.$spacing-05;
17
+ }
18
+ .backgroundColor {
19
+ margin: 1px 0 0;
20
+ transition: width 0.24s ease-in-out;
21
+ position: relative;
22
+ min-height: calc(100vh - 80px);
23
+ background-color: colors.$white;
24
+ }
@@ -0,0 +1,36 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import dayjs from 'dayjs';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { useAppointmentsCalendar } from '../hooks/useAppointmentsCalendar';
5
+ import AppointmentsHeader from '../header/appointments-header.component';
6
+ import CalendarHeader from './header/calendar-header.component';
7
+ import MonthlyCalendarView from './monthly/monthly-calendar-view.component';
8
+ import SelectedDateContext from '../hooks/selectedDateContext';
9
+ import { useParams } from 'react-router-dom';
10
+ import { omrsDateFormat } from '../constants';
11
+
12
+ const AppointmentsCalendarView: React.FC = () => {
13
+ const { t } = useTranslation();
14
+ const [selectedDate, setSelectedDate] = useState<string>(dayjs().startOf('day').format(omrsDateFormat));
15
+ const { calendarEvents } = useAppointmentsCalendar(dayjs(selectedDate).toISOString(), 'monthly');
16
+
17
+ let params = useParams();
18
+
19
+ useEffect(() => {
20
+ if (params.date) {
21
+ setSelectedDate(dayjs(params.date).startOf('day').format(omrsDateFormat));
22
+ }
23
+ }, [params.date]);
24
+
25
+ return (
26
+ <SelectedDateContext.Provider value={{ selectedDate, setSelectedDate }}>
27
+ <div data-testid="appointments-calendar">
28
+ <AppointmentsHeader title={t('calendar', 'Calendar')} />
29
+ <CalendarHeader />
30
+ <MonthlyCalendarView events={calendarEvents} />
31
+ </div>
32
+ </SelectedDateContext.Provider>
33
+ );
34
+ };
35
+
36
+ export default AppointmentsCalendarView;
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import AppointmentsCalendarView from './appointments-calendar-view.component';
4
+
5
+ describe('Appointment calendar view', () => {
6
+ it('renders appointments in calendar view from appointments list', async () => {
7
+ renderAppointmentsCalendarListView();
8
+
9
+ const expectedTableRows = [
10
+ /John Wilson 30-Aug-2021 03:35 03:35 Dr James Cook Outpatient Walk in appointments/,
11
+ /Neil Amstrong 10-Sept-2021 03:50 03:50 Dr James Cook Outpatient Some additional notes/,
12
+ ];
13
+
14
+ expectedTableRows.forEach((row) => {
15
+ expect(screen.queryByRole('row', { name: new RegExp(row, 'i') })).not.toBeInTheDocument();
16
+ });
17
+ });
18
+ });
19
+
20
+ function renderAppointmentsCalendarListView() {
21
+ render(<AppointmentsCalendarView />);
22
+ }
@@ -0,0 +1,34 @@
1
+ import React, { useContext } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button } from '@carbon/react';
4
+ import { ArrowLeft } from '@carbon/react/icons';
5
+ import { navigate } from '@openmrs/esm-framework';
6
+ import { spaHomePage } from '../../constants';
7
+ import styles from './calendar-header.scss';
8
+ import SelectedDateContext from '../../hooks/selectedDateContext';
9
+ import dayjs from 'dayjs';
10
+
11
+ const CalendarHeader: React.FC = () => {
12
+ const { t } = useTranslation();
13
+ const { selectedDate } = useContext(SelectedDateContext);
14
+ const backButtonOnClick = () => {
15
+ navigate({ to: `${spaHomePage}/appointments/${dayjs(selectedDate).format('YYYY-MM-DD')}` });
16
+ };
17
+
18
+ return (
19
+ <div className={styles.calendarHeaderContainer}>
20
+ <div className={styles.titleContainer}>
21
+ <Button
22
+ kind="ghost"
23
+ onClick={backButtonOnClick}
24
+ renderIcon={ArrowLeft}
25
+ iconDescription={t('back', 'Back')}
26
+ size="lg">
27
+ <span>{t('back', 'Back')}</span>
28
+ </Button>
29
+ </div>
30
+ </div>
31
+ );
32
+ };
33
+
34
+ export default CalendarHeader;
@@ -0,0 +1,32 @@
1
+ @use '@carbon/styles/scss/type';
2
+ @use '@carbon/colors';
3
+ @use '@carbon/styles/scss/spacing';
4
+
5
+ .titleContainer {
6
+ display: flex;
7
+ justify-content: space-between;
8
+ align-items: center;
9
+ margin-left: 1rem;
10
+
11
+ & > p {
12
+ @include type.type-style('heading-02');
13
+ }
14
+
15
+ & > button {
16
+ color: colors.$blue-60;
17
+ @include type.type-style('label-02');
18
+ }
19
+ }
20
+
21
+ .titleContent {
22
+ display: flex;
23
+ justify-content: space-between;
24
+ align-items: center;
25
+ }
26
+
27
+ // Overriding styles for RTL support
28
+ html[dir='rtl'] {
29
+ .titleContent {
30
+ margin-left: 1rem;
31
+ }
32
+ }
@@ -0,0 +1,16 @@
1
+ import dayjs from 'dayjs';
2
+ import React from 'react';
3
+ import styles from './days-of-week.scss';
4
+
5
+ interface DaysOfWeekProp {
6
+ dayOfWeek: string;
7
+ }
8
+ const DaysOfWeekCard: React.FC<DaysOfWeekProp> = ({ dayOfWeek }) => {
9
+ const isToday = dayjs(new Date()).format('ddd').toUpperCase() === dayOfWeek;
10
+ return (
11
+ <div tabIndex={0} role="button" className={styles.tileContainer}>
12
+ <span className={isToday ? styles.bold : ''}>{dayOfWeek}</span>
13
+ </div>
14
+ );
15
+ };
16
+ export default DaysOfWeekCard;
@@ -0,0 +1,33 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+ @import '~@openmrs/esm-styleguide/src/vars';
5
+
6
+ .tileContainer {
7
+ height: layout.$spacing-10;
8
+ display: flex;
9
+ justify-content: center;
10
+ width: 100%;
11
+ align-items: center;
12
+ border: 1px solid colors.$gray-20;
13
+
14
+ & > span {
15
+ font-size: layout.$spacing-05;
16
+ color: colors.$gray-70;
17
+ }
18
+ }
19
+
20
+ .bold {
21
+ font-weight: bold;
22
+ }
23
+
24
+ .tileHeader {
25
+ display: flex;
26
+ justify-content: space-between;
27
+ padding-bottom: layout.$spacing-05;
28
+ }
29
+
30
+ .headerLabel {
31
+ @include type.type-style('heading-compact-01');
32
+ color: $text-02;
33
+ }
@@ -0,0 +1,34 @@
1
+ import React, { useContext } from 'react';
2
+ import dayjs from 'dayjs';
3
+ import isBetween from 'dayjs/plugin/isBetween';
4
+ import { type DailyAppointmentsCountByService } from '../../types';
5
+ import { monthDays } from '../../helpers';
6
+ import MonthlyViewWorkload from './monthly-workload-view.component';
7
+ import MonthlyHeader from './monthly-header.module';
8
+ import styles from '../appointments-calendar-view-view.scss';
9
+ import SelectedDateContext from '../../hooks/selectedDateContext';
10
+
11
+ dayjs.extend(isBetween);
12
+
13
+ interface MonthlyCalendarViewProps {
14
+ events: Array<DailyAppointmentsCountByService>;
15
+ }
16
+
17
+ const MonthlyCalendarView: React.FC<MonthlyCalendarViewProps> = ({ events }) => {
18
+ const { selectedDate } = useContext(SelectedDateContext);
19
+
20
+ return (
21
+ <div className={styles.calendarViewContainer}>
22
+ <MonthlyHeader />
23
+ <div className={styles.wrapper}>
24
+ <div className={styles.monthlyCalendar}>
25
+ {monthDays(dayjs(selectedDate)).map((dateTime, i) => (
26
+ <MonthlyViewWorkload key={i} dateTime={dateTime} events={events} />
27
+ ))}
28
+ </div>
29
+ </div>
30
+ </div>
31
+ );
32
+ };
33
+
34
+ export default MonthlyCalendarView;
@@ -0,0 +1,14 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+
4
+ .container {
5
+ display: flex;
6
+ justify-content: space-between;
7
+ align-items: center;
8
+ width: 100%;
9
+ height: layout.$spacing-10;
10
+ }
11
+ .workLoadCard {
12
+ display: flex;
13
+ color: colors.$black;
14
+ }
@@ -0,0 +1,40 @@
1
+ import React, { useContext } from 'react';
2
+ import dayjs from 'dayjs';
3
+ import styles from './monthly-header.module.scss';
4
+ import { Button } from '@carbon/react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import DaysOfWeekCard from './days-of-week.component';
7
+ import SelectedDateContext from '../../hooks/selectedDateContext';
8
+ import { omrsDateFormat } from '../../constants';
9
+
10
+ const monthFormat = 'MMMM, YYYY';
11
+ const daysInWeek = ['SUN', 'MON', 'TUE', 'WED', 'THUR', 'FRI', 'SAT'];
12
+ function MonthlyHeader() {
13
+ const { t } = useTranslation();
14
+ const { selectedDate, setSelectedDate } = useContext(SelectedDateContext);
15
+
16
+ return (
17
+ <>
18
+ <div className={styles.container}>
19
+ <Button
20
+ size="sm"
21
+ onClick={() => setSelectedDate(dayjs(selectedDate).subtract(1, 'month').format(omrsDateFormat))}
22
+ kind="tertiary">
23
+ {t('prev', 'Prev')}
24
+ </Button>
25
+ <span>{dayjs(selectedDate).format(monthFormat)}</span>
26
+
27
+ <Button
28
+ size="sm"
29
+ onClick={() => setSelectedDate(dayjs(selectedDate).add(1, 'month').format(omrsDateFormat))}
30
+ kind="tertiary">
31
+ {t('next', 'Next')}
32
+ </Button>
33
+ </div>
34
+ <div className={styles.workLoadCard}>
35
+ {daysInWeek?.map((day, i) => <DaysOfWeekCard key={`${day}-${i}`} dayOfWeek={day} />)}
36
+ </div>
37
+ </>
38
+ );
39
+ }
40
+ export default MonthlyHeader;
@@ -0,0 +1,188 @@
1
+ @use '@carbon/styles/scss/type';
2
+ @use '@carbon/colors';
3
+ @use '@carbon/layout';
4
+ @import '~@openmrs/esm-styleguide/src/vars';
5
+
6
+ .monthly-cell {
7
+ border-left: 1px solid colors.$gray-20;
8
+ border-bottom: 1px solid colors.$gray-20;
9
+ background: colors.$white;
10
+ color: colors.$black;
11
+ cursor: pointer;
12
+ padding: layout.$spacing-01;
13
+ text-align: right;
14
+ @include type.type-style('body-compact-02');
15
+
16
+ &:nth-child(-n + 7) {
17
+ border-top: 1px solid colors.$gray-20;
18
+ }
19
+
20
+ &:nth-child(7n) {
21
+ border-right: 1px solid colors.$gray-20;
22
+ }
23
+
24
+ &-disabled {
25
+ border-left: 1px solid colors.$gray-20;
26
+ border-bottom: 1px solid colors.$gray-20;
27
+ }
28
+
29
+ &-disabled:last-child {
30
+ border-right: 1px solid colors.$gray-20;
31
+ }
32
+ }
33
+
34
+ .identifiers {
35
+ @include type.type-style('body-compact-02');
36
+ color: $ui-04;
37
+ display: list-item;
38
+
39
+ span:not(:first-child) {
40
+ margin: 0rem 0.75rem;
41
+ }
42
+ }
43
+
44
+ .identifierTag {
45
+ display: flex;
46
+ align-items: center;
47
+ }
48
+
49
+ .weekly-cell {
50
+ position: relative;
51
+ border-right: 1px solid colors.$gray-20;
52
+ border-bottom: 1px solid colors.$gray-20;
53
+ min-height: 120px;
54
+ color: colors.$white;
55
+
56
+ .week-time {
57
+ display: none;
58
+ }
59
+
60
+ &:nth-child(-n + 8) {
61
+ border-top: 1px solid colors.$gray-20;
62
+ }
63
+
64
+ &:first-child {
65
+ border-top: none;
66
+ }
67
+
68
+ &:nth-child(8n + 1) {
69
+ border-bottom: none;
70
+
71
+ .week-time {
72
+ white-space: nowrap;
73
+ display: inline;
74
+ color: colors.$white;
75
+ position: absolute;
76
+ top: -11px;
77
+ height: layout.$spacing-06;
78
+ width: layout.$spacing-07;
79
+ }
80
+ }
81
+ }
82
+
83
+ .currentData {
84
+ display: flex;
85
+ flex-direction: column;
86
+ align-items: start;
87
+ margin: 0.1rem 0.1rem 0.5rem 0.1rem;
88
+
89
+ & > span {
90
+ @include type.type-style('label-02');
91
+ }
92
+ }
93
+
94
+ .serviceArea {
95
+ width: 100%;
96
+ padding: 0.125rem;
97
+ margin-right: 0.5rem;
98
+ display: grid;
99
+ grid-template-columns: 4fr 1fr;
100
+ justify-content: flex-start;
101
+ color: #020f1b;
102
+ cursor: pointer;
103
+ @include type.type-style('label-01');
104
+
105
+ & > span:first-child {
106
+ text-align: left;
107
+ text-overflow: ellipsis;
108
+ overflow: hidden;
109
+ white-space: nowrap;
110
+ }
111
+
112
+ & > span:nth-child(2) {
113
+ text-align: right;
114
+ }
115
+ }
116
+
117
+ .serviceArea:hover {
118
+ background-color: colors.$gray-10;
119
+ }
120
+
121
+ .totals {
122
+ padding: 0.125rem;
123
+ display: grid;
124
+ grid-template-columns: 4fr 1fr;
125
+ justify-content: flex-start;
126
+ background-color: colors.$gray-10;
127
+
128
+ & > div {
129
+ display: flex;
130
+ flex-direction: row;
131
+ align-items: center;
132
+ justify-content: flex-start;
133
+ cursor: pointer;
134
+ font-weight: lighter;
135
+ }
136
+
137
+ & > span:nth-child(2) {
138
+ color: #020f1b;
139
+ font-weight: bold;
140
+ text-align: right;
141
+ @include type.type-style('heading-compact-01');
142
+ }
143
+ }
144
+
145
+ .smallDesktop {
146
+ height: 100px;
147
+ }
148
+
149
+ .largeDesktop {
150
+ height: 150px;
151
+ }
152
+
153
+ .red {
154
+ background-color: colors.$red-70;
155
+ color: colors.$white;
156
+ font-weight: bold;
157
+ }
158
+
159
+ .purple {
160
+ background-color: colors.$yellow-70;
161
+ color: colors.$white;
162
+ font-weight: bold;
163
+ }
164
+
165
+ .blue {
166
+ background-color: colors.$blue-70;
167
+ color: colors.$white;
168
+ font-weight: bold;
169
+ }
170
+
171
+ .green {
172
+ background-color: colors.$green-70;
173
+ color: colors.$white;
174
+ font-weight: bold;
175
+ }
176
+
177
+ .showMoreItems {
178
+ margin-top: 0.1rem;
179
+ padding: 0;
180
+ @include type.type-style('heading-compact-01');
181
+ text-decoration: underline;
182
+ color: colors.$blue-50;
183
+ background-color: colors.$white;
184
+ font-weight: lighter;
185
+ border: none;
186
+ outline: none;
187
+ cursor: pointer;
188
+ }
@@ -0,0 +1,42 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Popover, PopoverContent } from '@carbon/react';
3
+ import styles from './monthly-view-workload.scss';
4
+ import MonthlyWorkloadView, { type MonthlyWorkloadViewProps } from './monthly-workload-view.component';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ interface MonthlyWorkloadViewExpandedProps extends MonthlyWorkloadViewProps {
8
+ count: number;
9
+ }
10
+
11
+ const MonthlyWorkloadViewExpanded: React.FC<MonthlyWorkloadViewExpandedProps> = ({ count, events, dateTime }) => {
12
+ const { t } = useTranslation();
13
+ const [isOpen, setIsOpen] = React.useState(false);
14
+ const popoverRef = useRef(null);
15
+
16
+ const handleClickOutside = (event) => {
17
+ if (popoverRef.current && !popoverRef.current.contains(event.target)) {
18
+ setIsOpen(false);
19
+ }
20
+ };
21
+
22
+ useEffect(() => {
23
+ document.addEventListener('click', handleClickOutside);
24
+
25
+ return () => {
26
+ document.removeEventListener('click', handleClickOutside);
27
+ };
28
+ }, []);
29
+
30
+ return (
31
+ <Popover open={isOpen} align="top" ref={popoverRef}>
32
+ <button className={styles.showMoreItems} onClick={() => setIsOpen((prev) => !prev)}>
33
+ {t('countMore', '{{count}} more', { count })}
34
+ </button>
35
+ <PopoverContent>
36
+ <MonthlyWorkloadView events={events} dateTime={dateTime} showAllServices={true} />
37
+ </PopoverContent>
38
+ </Popover>
39
+ );
40
+ };
41
+
42
+ export default MonthlyWorkloadViewExpanded;
@@ -0,0 +1,109 @@
1
+ import React, { useContext, useMemo } from 'react';
2
+ import classNames from 'classnames';
3
+ import dayjs, { type Dayjs } from 'dayjs';
4
+ import { navigate, useLayoutType } from '@openmrs/esm-framework';
5
+ import { isSameMonth } from '../../helpers';
6
+ import { spaHomePage } from '../../constants';
7
+ import styles from './monthly-view-workload.scss';
8
+ import { type DailyAppointmentsCountByService } from '../../types';
9
+ import SelectedDateContext from '../../hooks/selectedDateContext';
10
+ import { User } from '@carbon/react/icons';
11
+ import MonthlyWorkloadViewExpanded from './monthly-workload-view-expanded.component';
12
+
13
+ export interface MonthlyWorkloadViewProps {
14
+ events: Array<DailyAppointmentsCountByService>;
15
+ dateTime: Dayjs;
16
+ showAllServices?: boolean;
17
+ }
18
+
19
+ const MonthlyWorkloadView: React.FC<MonthlyWorkloadViewProps> = ({ dateTime, events, showAllServices = false }) => {
20
+ const layout = useLayoutType();
21
+ const { selectedDate } = useContext(SelectedDateContext);
22
+
23
+ const currentData = useMemo(
24
+ () =>
25
+ events?.find(
26
+ (event) => dayjs(event.appointmentDate)?.format('YYYY-MM-DD') === dayjs(dateTime)?.format('YYYY-MM-DD'),
27
+ ),
28
+ [events],
29
+ );
30
+
31
+ const visibleServices = useMemo(() => {
32
+ if (currentData?.services) {
33
+ if (showAllServices) return currentData.services;
34
+ return currentData.services.slice(0, layout === 'small-desktop' ? 2 : 4);
35
+ }
36
+ return [];
37
+ }, [currentData, showAllServices, layout, currentData]);
38
+
39
+ const hasHiddenServices = useMemo(() => {
40
+ if (currentData?.services) {
41
+ if (showAllServices) return false;
42
+ return layout === 'small-desktop' ? currentData.services.length > 2 : currentData.services.length > 4;
43
+ }
44
+ return false;
45
+ }, [layout, currentData, currentData]);
46
+
47
+ const navigateToAppointmentsByDate = (serviceUuid: string) => {
48
+ navigate({ to: `${spaHomePage}/appointments/${dayjs(dateTime).format('YYYY-MM-DD')}/${serviceUuid}` });
49
+ };
50
+
51
+ return (
52
+ <div
53
+ onClick={() => navigateToAppointmentsByDate('')}
54
+ className={classNames(
55
+ styles[isSameMonth(dateTime, dayjs(selectedDate)) ? 'monthly-cell' : 'monthly-cell-disabled'],
56
+ showAllServices
57
+ ? {}
58
+ : {
59
+ [styles.smallDesktop]: layout === 'small-desktop',
60
+ [styles.largeDesktop]: layout !== 'small-desktop',
61
+ },
62
+ )}>
63
+ {isSameMonth(dateTime, dayjs(selectedDate)) && (
64
+ <p>
65
+ <div className={classNames(styles.totals)}>
66
+ {currentData?.services ? (
67
+ <div role="button" tabIndex={0}>
68
+ <User size={16} />
69
+ <span>{currentData?.services.reduce((sum, { count = 0 }) => sum + count, 0)}</span>
70
+ </div>
71
+ ) : (
72
+ <div />
73
+ )}
74
+ <b className={styles.calendarDate}>{dateTime.format('D')}</b>
75
+ </div>
76
+ {currentData?.services && (
77
+ <div className={styles.currentData}>
78
+ {visibleServices.map(({ serviceName, serviceUuid, count }, i) => (
79
+ <div
80
+ key={`${serviceUuid}-${count}-${i}`}
81
+ role="button"
82
+ tabIndex={0}
83
+ onClick={(e) => {
84
+ e.stopPropagation();
85
+ navigateToAppointmentsByDate(serviceUuid);
86
+ }}
87
+ className={styles.serviceArea}>
88
+ <span>{serviceName}</span>
89
+ <span>{count}</span>
90
+ </div>
91
+ ))}
92
+ {hasHiddenServices ? (
93
+ <MonthlyWorkloadViewExpanded
94
+ count={currentData.services.length - (layout === 'small-desktop' ? 2 : 4)}
95
+ events={events}
96
+ dateTime={dateTime}
97
+ />
98
+ ) : (
99
+ ''
100
+ )}
101
+ </div>
102
+ )}
103
+ </p>
104
+ )}
105
+ </div>
106
+ );
107
+ };
108
+
109
+ export default MonthlyWorkloadView;