@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
package/src/index.ts ADDED
@@ -0,0 +1,111 @@
1
+ import {
2
+ defineConfigSchema,
3
+ defineExtensionConfigSchema,
4
+ getAsyncLifecycle,
5
+ getSyncLifecycle,
6
+ registerBreadcrumbs,
7
+ } from '@openmrs/esm-framework';
8
+ import { configSchema } from './config-schema';
9
+ import { createDashboardLink } from './createDashboardLink.component';
10
+ import { createDashboardLink as createPatientChartDashboardLink } from '@openmrs/esm-patient-common-lib';
11
+ import { dashboardMeta, appointmentCalendarDashboardMeta, patientChartDashboardMeta } from './dashboard.meta';
12
+ import {
13
+ cancelledAppointmentsPanelConfigSchema,
14
+ checkedInAppointmentsPanelConfigSchema,
15
+ completedAppointmentsPanelConfigSchema,
16
+ earlyAppointmentsPanelConfigSchema,
17
+ expectedAppointmentsPanelConfigSchema,
18
+ missedAppointmentsPanelConfigSchema,
19
+ } from './scheduled-appointments-config-schema';
20
+ import rootComponent from './root.component';
21
+ import appointmentsDashboardComponent from './appointments.component';
22
+ import homeAppointmentsComponent from './home/home-appointments.component';
23
+ import appointmentsListComponent from './appointments/scheduled/appointments-list.component';
24
+ import earlyAppointmentsComponent from './appointments/scheduled/early-appointments.component';
25
+ import patientAppointmentsDetailedSummaryComponent from './patient-appointments/patient-appointments-detailed-summary.component';
26
+ import patientAppointmentsOverviewComponent from './patient-appointments/patient-appointments-overview.component';
27
+ import patientUpcomingAppointmentsComponent from './patient-appointments/patient-upcoming-appointments-card.component';
28
+ import appointementsForm from './form/appointments-form.component';
29
+ import patientSearch from './patient-search/patient-search.component';
30
+ export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
31
+
32
+ const moduleName = '@openmrs/esm-appointments-app';
33
+
34
+ const options = {
35
+ featureName: 'appointments',
36
+ moduleName,
37
+ };
38
+
39
+ export function startupApp() {
40
+ const appointmentsBasePath = `${window.spaBase}/home/appointments`;
41
+
42
+ defineConfigSchema(moduleName, configSchema);
43
+
44
+ defineExtensionConfigSchema('expected-appointments-panel', expectedAppointmentsPanelConfigSchema);
45
+ defineExtensionConfigSchema('checked-in-appointments-panel', checkedInAppointmentsPanelConfigSchema);
46
+ defineExtensionConfigSchema('completed-appointments-panel', completedAppointmentsPanelConfigSchema);
47
+ defineExtensionConfigSchema('missed-appointments-panel', missedAppointmentsPanelConfigSchema);
48
+ defineExtensionConfigSchema('cancelled-appointments-panel', cancelledAppointmentsPanelConfigSchema);
49
+ defineExtensionConfigSchema('early-appointments-panel', earlyAppointmentsPanelConfigSchema);
50
+
51
+ registerBreadcrumbs([
52
+ {
53
+ title: 'Appointments',
54
+ path: appointmentsBasePath,
55
+ parent: `${window.spaBase}/home`,
56
+ },
57
+ {
58
+ path: `${window.spaBase}/patient-list/:forDate/:serviceName`,
59
+ title: ([_, serviceName]) => `Patient Lists / ${decodeURI(serviceName)}`,
60
+ parent: `${window.spaBase}`,
61
+ },
62
+ ]);
63
+ }
64
+
65
+ export const root = getSyncLifecycle(rootComponent, options);
66
+
67
+ export const appointmentsDashboardLink = getSyncLifecycle(createDashboardLink(dashboardMeta), options);
68
+
69
+ export const appointmentsCalendarDashboardLink = getSyncLifecycle(
70
+ createDashboardLink(appointmentCalendarDashboardMeta),
71
+ options,
72
+ );
73
+
74
+ export const appointmentsDashboard = getSyncLifecycle(appointmentsDashboardComponent, options);
75
+
76
+ export const homeAppointments = getSyncLifecycle(homeAppointmentsComponent, options);
77
+
78
+ export const appointmentsList = getSyncLifecycle(appointmentsListComponent, options);
79
+
80
+ export const earlyAppointments = getSyncLifecycle(earlyAppointmentsComponent, options);
81
+
82
+ export const appointementForm = getSyncLifecycle(appointementsForm, options);
83
+
84
+ export const searchPatient = getSyncLifecycle(patientSearch, options);
85
+
86
+ // t('Appointments', 'Appointments')
87
+ export const patientAppointmentsSummaryDashboardLink = getSyncLifecycle(
88
+ createPatientChartDashboardLink({ ...patientChartDashboardMeta, moduleName }),
89
+ options,
90
+ );
91
+
92
+ export const patientAppointmentsDetailedSummary = getSyncLifecycle(
93
+ patientAppointmentsDetailedSummaryComponent,
94
+ options,
95
+ );
96
+
97
+ export const patientAppointmentsOverview = getSyncLifecycle(patientAppointmentsOverviewComponent, options);
98
+
99
+ export const patientUpcomingAppointmentsWidget = getSyncLifecycle(patientUpcomingAppointmentsComponent, options);
100
+
101
+ export const patientAppointmentsCancelConfirmationDialog = getAsyncLifecycle(
102
+ () => import('./patient-appointments/patient-appointments-cancel-modal.component'),
103
+ options,
104
+ );
105
+
106
+ export const appointmentsFormWorkspace = getAsyncLifecycle(() => import('./form/appointments-form.component'), options);
107
+
108
+ export const endAppointmentModal = getAsyncLifecycle(
109
+ () => import('./appointments/common-components/end-appointment-modal.component'),
110
+ options,
111
+ );
@@ -0,0 +1,71 @@
1
+ import React, { useContext } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { ErrorState, formatDate, parseDate } from '@openmrs/esm-framework';
4
+ import { useClinicalMetrics, useAllAppointmentsByDate, useScheduledAppointment } from '../hooks/useClinicalMetrics';
5
+ import { useAppointmentList } from '../hooks/useAppointmentList';
6
+ import MetricsCard from './metrics-card.component';
7
+ import MetricsHeader from './metrics-header.component';
8
+ import styles from './appointments-metrics.scss';
9
+ import SelectedDateContext from '../hooks/selectedDateContext';
10
+
11
+ interface AppointmentMetricsProps {
12
+ appointmentServiceType: string;
13
+ }
14
+
15
+ const AppointmentsMetrics: React.FC<AppointmentMetricsProps> = ({ appointmentServiceType }) => {
16
+ const { t } = useTranslation();
17
+
18
+ const { highestServiceLoad, error } = useClinicalMetrics();
19
+ const { totalProviders } = useAllAppointmentsByDate();
20
+ const { totalScheduledAppointments } = useScheduledAppointment(appointmentServiceType);
21
+
22
+ const { selectedDate } = useContext(SelectedDateContext);
23
+ const formattedStartDate = formatDate(parseDate(selectedDate), { mode: 'standard', time: false });
24
+
25
+ // TODO we will need rework these after we discuss the logic we want to use
26
+ const { appointmentList: arrivedAppointments } = useAppointmentList('CheckedIn');
27
+ const { appointmentList: pendingAppointments } = useAppointmentList('Scheduled');
28
+
29
+ const filteredArrivedAppointments = appointmentServiceType
30
+ ? arrivedAppointments.filter(({ service }) => service.uuid === appointmentServiceType)
31
+ : arrivedAppointments;
32
+ const filteredPendingAppointments = appointmentServiceType
33
+ ? pendingAppointments.filter(({ service }) => service.uuid === appointmentServiceType)
34
+ : pendingAppointments;
35
+
36
+ if (error) {
37
+ return (
38
+ <div className={styles.errorContainer}>
39
+ <ErrorState headerTitle={t('appointmentMetricsLoadError', 'Metrics load error')} error={error} />
40
+ </div>
41
+ );
42
+ }
43
+
44
+ return (
45
+ <>
46
+ <MetricsHeader />
47
+ <section className={styles.cardContainer}>
48
+ <MetricsCard
49
+ label={t('patients', 'Patients')}
50
+ value={totalScheduledAppointments}
51
+ headerLabel={t('scheduledAppointments', 'Scheduled appointments')}
52
+ count={{ pendingAppointments: filteredPendingAppointments, arrivedAppointments: filteredArrivedAppointments }}
53
+ />
54
+ <MetricsCard
55
+ label={
56
+ highestServiceLoad?.count !== 0 ? t(highestServiceLoad?.serviceName) : t('serviceName', 'Service name')
57
+ }
58
+ value={highestServiceLoad?.count ?? '--'}
59
+ headerLabel={t('highestServiceVolume', 'Highest volume service: {{time}}', { time: formattedStartDate })}
60
+ />
61
+ <MetricsCard
62
+ label={t('providers', 'Providers')}
63
+ value={totalProviders}
64
+ headerLabel={t('providersBooked', 'Providers booked: {{time}}', { time: formattedStartDate })}
65
+ />
66
+ </section>
67
+ </>
68
+ );
69
+ };
70
+
71
+ export default AppointmentsMetrics;
@@ -0,0 +1,15 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .cardContainer {
6
+ background-color: colors.$white;
7
+ display: grid;
8
+ grid-template-columns: repeat(auto-fit, minmax(18.75rem, 1fr));
9
+ gap: layout.$spacing-05;
10
+ margin: layout.$spacing-05;
11
+ }
12
+
13
+ .errorContainer {
14
+ margin: layout.$spacing-05;
15
+ }
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { openmrsFetch } from '@openmrs/esm-framework';
4
+ import { mockAppointmentMetrics, mockProvidersCount, mockStartTime } from '__mocks__';
5
+ import AppointmentsMetrics from './appointments-metrics.component';
6
+
7
+ const mockedOpenmrsFetch = openmrsFetch as jest.Mock;
8
+
9
+ jest.mock('../hooks/useClinicalMetrics', () => {
10
+ const originalModule = jest.requireActual('../hooks/useClinicalMetrics');
11
+
12
+ return {
13
+ ...originalModule,
14
+ useClinicalMetrics: jest.fn().mockImplementation(() => ({
15
+ highestServiceLoad: mockAppointmentMetrics.highestServiceLoad,
16
+ isLoading: mockAppointmentMetrics.isLoading,
17
+ error: mockAppointmentMetrics.error,
18
+ })),
19
+ useAllAppointmentsByDate: jest.fn().mockImplementation(() => ({
20
+ totalProviders: mockProvidersCount.totalProviders,
21
+ isLoading: mockProvidersCount.isLoading,
22
+ error: mockProvidersCount.error,
23
+ })),
24
+ useScheduledAppointment: jest.fn().mockImplementation(() => ({
25
+ totalScheduledAppointments: mockAppointmentMetrics.totalAppointments,
26
+ })),
27
+ useAppointmentDate: jest.fn().mockImplementation(() => ({
28
+ startDate: mockStartTime.startTime,
29
+ })),
30
+ };
31
+ });
32
+
33
+ describe('Appointment metrics', () => {
34
+ it('renders metrics from the appointments list', async () => {
35
+ mockedOpenmrsFetch.mockResolvedValue({ data: [] });
36
+
37
+ renderAppointmentMetrics();
38
+
39
+ await screen.findByText(/appointment metrics/i);
40
+ expect(screen.getByText(/scheduled appointments/i)).toBeInTheDocument();
41
+ expect(screen.getByText(/patients/i)).toBeInTheDocument();
42
+ expect(screen.getByText(/16/i)).toBeInTheDocument();
43
+ expect(screen.getByText(/4/i)).toBeInTheDocument();
44
+ });
45
+ });
46
+
47
+ function renderAppointmentMetrics() {
48
+ render(<AppointmentsMetrics appointmentServiceType="consultation-service-uuid" />);
49
+ }
@@ -0,0 +1,76 @@
1
+ import React, { useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import dayjs from 'dayjs';
4
+ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
5
+ dayjs.extend(isSameOrBefore);
6
+ import isEmpty from 'lodash-es/isEmpty';
7
+ import { ConfigurableLink } from '@openmrs/esm-framework';
8
+ import { ArrowRight } from '@carbon/react/icons';
9
+ import { basePath, spaHomePage } from '../constants';
10
+ import styles from './metrics-card.scss';
11
+
12
+ interface MetricsCardProps {
13
+ label: string;
14
+ value: number;
15
+ headerLabel: string;
16
+ children?: React.ReactNode;
17
+ view?: string;
18
+ count?: { pendingAppointments: Array<any>; arrivedAppointments: Array<any> };
19
+ appointmentDate?: string;
20
+ }
21
+
22
+ const MetricsCard: React.FC<MetricsCardProps> = ({
23
+ label,
24
+ value,
25
+ headerLabel,
26
+ children,
27
+ view,
28
+ count,
29
+ appointmentDate,
30
+ }) => {
31
+ const { t } = useTranslation();
32
+ const isDateInPast = useMemo(() => !dayjs(appointmentDate).isBefore(dayjs(), 'date'), [appointmentDate]);
33
+
34
+ const metricsLink = {
35
+ patients: 'appointments-list/scheduled',
36
+ highVolume: 'appointments-list/high-volume-service',
37
+ providers: 'appointments-list/providers-link',
38
+ };
39
+
40
+ return (
41
+ <article className={styles.container}>
42
+ <div className={styles.tileContainer}>
43
+ <div className={styles.tileHeader}>
44
+ <div className={styles.headerLabelContainer}>
45
+ <label className={styles.headerLabel}>{headerLabel}</label>
46
+ {children}
47
+ </div>
48
+ {view && (
49
+ <div className={styles.link}>
50
+ <ConfigurableLink className={styles.link} to={`${spaHomePage}${basePath}/${metricsLink[view]}`}>
51
+ <span style={{ fontSize: '0.825rem', marginRight: '0.325rem' }}>{t('view', 'View')}</span>{' '}
52
+ <ArrowRight size={16} className={styles.viewListBtn} />
53
+ </ConfigurableLink>
54
+ </div>
55
+ )}
56
+ </div>
57
+ <div className={styles.metricsGrid}>
58
+ <div>
59
+ <label className={styles.totalsLabel}>{label}</label>
60
+ <p className={styles.totalsValue}>{value}</p>
61
+ </div>
62
+ {!isEmpty(count) && (
63
+ <div className={styles.countGrid}>
64
+ <span>{t('checkedIn', 'Checked in')}</span>
65
+ <span>{isDateInPast ? t('notArrived', 'Not arrived') : t('missed', 'Missed')}</span>
66
+ <p style={{ color: '#319227' }}>{count.arrivedAppointments?.length}</p>
67
+ <p style={{ color: '#da1e28' }}>{count.pendingAppointments?.length}</p>
68
+ </div>
69
+ )}
70
+ </div>
71
+ </div>
72
+ </article>
73
+ );
74
+ };
75
+
76
+ export default MetricsCard;
@@ -0,0 +1,77 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @use '@carbon/colors';
4
+ @import '~@openmrs/esm-styleguide/src/vars';
5
+
6
+ .container {
7
+ flex-grow: 1;
8
+ }
9
+
10
+ .tileContainer {
11
+ border: 1px solid $ui-03;
12
+ height: 7.875rem;
13
+ padding: spacing.$spacing-05;
14
+ display: flex;
15
+ justify-content: space-between;
16
+ flex-direction: column;
17
+ }
18
+
19
+ .tileHeader {
20
+ display: flex;
21
+ justify-content: space-between;
22
+ align-items: baseline;
23
+ margin-bottom: spacing.$spacing-03;
24
+ }
25
+
26
+ .headerLabel {
27
+ @include type.type-style('heading-compact-01');
28
+ color: $text-02;
29
+ }
30
+
31
+ .totalsLabel {
32
+ @include type.type-style('label-01');
33
+ color: $text-02;
34
+ }
35
+
36
+ .totalsValue {
37
+ @include type.type-style('heading-04');
38
+ color: $ui-05;
39
+ }
40
+
41
+ .headerLabelContainer {
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: space-between;
45
+ width: 100%;
46
+ }
47
+
48
+ .link {
49
+ text-decoration: none;
50
+ display: flex;
51
+ align-items: center;
52
+ color: $interactive-01;
53
+ }
54
+
55
+ .metricsGrid {
56
+ display: grid;
57
+ grid-template-columns: 1fr 1fr;
58
+ }
59
+
60
+ .countGrid {
61
+ display: grid;
62
+ grid-template-columns: 1fr 1fr;
63
+ justify-self: flex-end;
64
+ column-gap: spacing.$spacing-03;
65
+ row-gap: spacing.$spacing-03;
66
+ margin: spacing.$spacing-03;
67
+
68
+ & > span {
69
+ font-size: 0.625rem !important;
70
+ margin: 0;
71
+ color: colors.$gray-70;
72
+ }
73
+
74
+ & > p {
75
+ margin: 0;
76
+ }
77
+ }
@@ -0,0 +1,62 @@
1
+ import React, { useContext } from 'react';
2
+ import dayjs from 'dayjs';
3
+ import isToday from 'dayjs/plugin/isToday';
4
+ import { launchWorkspace } from '@openmrs/esm-framework';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Calendar, Hospital } from '@carbon/react/icons';
7
+ import { Button } from '@carbon/react';
8
+ import { ExtensionSlot, isDesktop, navigate, useLayoutType } from '@openmrs/esm-framework';
9
+ import { spaHomePage } from '../constants';
10
+ import styles from './metrics-header.scss';
11
+ import SelectedDateContext from '../hooks/selectedDateContext';
12
+
13
+ dayjs.extend(isToday);
14
+
15
+ const MetricsHeader: React.FC = () => {
16
+ const { t } = useTranslation();
17
+ const { selectedDate } = useContext(SelectedDateContext);
18
+ const layout = useLayoutType();
19
+ const responsiveSize = isDesktop(layout) ? 'sm' : 'md';
20
+
21
+ const launchCreateAppointmentForm = (patientUuid) => {
22
+ const props = {
23
+ patientUuid: patientUuid,
24
+ context: 'creating',
25
+ mutate: () => {}, // TODO get this to mutate properly
26
+ };
27
+
28
+ launchWorkspace('create-appointment', { ...props });
29
+ };
30
+
31
+ return (
32
+ <div className={styles.metricsContainer}>
33
+ <span className={styles.metricsTitle}>{t('appointmentMetrics', 'Appointment metrics')}</span>
34
+ <div className={styles.metricsContent}>
35
+ <Button
36
+ kind="tertiary"
37
+ renderIcon={Calendar}
38
+ size={responsiveSize}
39
+ onClick={() =>
40
+ navigate({ to: `${spaHomePage}/appointments/calendar/${dayjs(selectedDate).format('YYYY-MM-DD')}` })
41
+ }>
42
+ {t('appointmentsCalendar', 'Appointments Calendar')}
43
+ </Button>
44
+ <ExtensionSlot
45
+ name="patient-search-button-slot"
46
+ state={{
47
+ selectPatientAction: launchCreateAppointmentForm,
48
+ buttonText: t('createNewAppointment', 'Create new appointment'),
49
+ overlayHeader: t('createNewAppointment', 'Create new appointment'),
50
+ buttonProps: {
51
+ kind: 'primary',
52
+ renderIcon: (props) => <Hospital size={32} {...props} />,
53
+ size: responsiveSize,
54
+ },
55
+ }}
56
+ />
57
+ </div>
58
+ </div>
59
+ );
60
+ };
61
+
62
+ export default MetricsHeader;
@@ -0,0 +1,33 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .metricsContainer {
6
+ display: flex;
7
+ justify-content: space-between;
8
+ background-color: $ui-02;
9
+ height: layout.$spacing-10;
10
+ align-items: center;
11
+ padding: 0 layout.$spacing-05;
12
+ }
13
+
14
+ .metricsTitle {
15
+ @include type.type-style('heading-03');
16
+ color: $ui-05;
17
+ }
18
+
19
+ .link {
20
+ text-decoration: none;
21
+ display: flex;
22
+ align-items: center;
23
+ }
24
+
25
+ .viewListBtn {
26
+ margin: 0 0 0 layout.$spacing-03;
27
+ }
28
+
29
+ .metricsContent {
30
+ display: flex;
31
+ align-items: center;
32
+ gap: layout.$spacing-05;
33
+ }
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import classNames from 'classnames';
3
+ import {
4
+ StructuredListHead,
5
+ StructuredListCell,
6
+ StructuredListRow,
7
+ StructuredListBody,
8
+ StructuredListWrapper,
9
+ } from '@carbon/react';
10
+ import { useTranslation } from 'react-i18next';
11
+ import { formatDatetime, parseDate } from '@openmrs/esm-framework';
12
+ import { type FormattedEncounter } from './past-visit.component';
13
+ import styles from './past-visit.scss';
14
+
15
+ interface EncounterListProps {
16
+ encounters: Array<FormattedEncounter>;
17
+ }
18
+
19
+ const EncounterList: React.FC<EncounterListProps> = ({ encounters }) => {
20
+ const { t } = useTranslation();
21
+
22
+ const structuredListBodyRowGenerator = () => {
23
+ return encounters.map((encounter, i) => (
24
+ <StructuredListRow label key={`row-${i}`}>
25
+ <StructuredListCell>{formatDatetime(parseDate(encounter.datetime), { mode: 'wide' })}</StructuredListCell>
26
+ <StructuredListCell className={styles.textColor}>{encounter.encounterType}</StructuredListCell>
27
+ <StructuredListCell>{encounter.provider}</StructuredListCell>
28
+ </StructuredListRow>
29
+ ));
30
+ };
31
+
32
+ if (encounters?.length) {
33
+ return (
34
+ <div className={styles.encounterListContainer}>
35
+ <StructuredListWrapper>
36
+ <StructuredListHead>
37
+ <StructuredListRow head>
38
+ <StructuredListCell head>{t('date&Time', 'Date & time')}</StructuredListCell>
39
+ <StructuredListCell head>{t('encounterType', 'Encounter type')}</StructuredListCell>
40
+ <StructuredListCell head>{t('provider', 'Provider')}</StructuredListCell>
41
+ </StructuredListRow>
42
+ </StructuredListHead>
43
+ <StructuredListBody>{structuredListBodyRowGenerator()}</StructuredListBody>
44
+ </StructuredListWrapper>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ return (
50
+ <p className={classNames(styles.bodyLong01, styles.text02)}>{t('noEncountersFound', 'No encounters found')}</p>
51
+ );
52
+ };
53
+
54
+ export default EncounterList;
@@ -0,0 +1,106 @@
1
+ import React, { useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { StructuredListSkeleton, Tab, Tabs } from '@carbon/react';
5
+ import { formatDate, type OpenmrsResource, parseDate, useLayoutType } from '@openmrs/esm-framework';
6
+ import { type Observation } from '../types';
7
+ import { usePastVisits } from './past-visit.resource';
8
+ import EncounterList from './encounter-list.component';
9
+ import styles from './past-visit.scss';
10
+
11
+ interface PastVisitProps {
12
+ patientUuid: string;
13
+ }
14
+
15
+ export interface FormattedEncounter {
16
+ id: string;
17
+ datetime: string;
18
+ encounterType: string;
19
+ form: OpenmrsResource;
20
+ obs: Array<Observation>;
21
+ provider: string;
22
+ visitType: string;
23
+ visitUuid: string;
24
+ }
25
+
26
+ const PastVisit: React.FC<PastVisitProps> = ({ patientUuid }) => {
27
+ const { t } = useTranslation();
28
+ const { data: pastVisits, isError, isLoading } = usePastVisits(patientUuid);
29
+ const [selectedTabIndex, setSelectedTabIndex] = useState(0);
30
+ const isTablet = useLayoutType() === 'tablet';
31
+
32
+ if (isLoading) {
33
+ return <StructuredListSkeleton role="progressbar" />;
34
+ }
35
+
36
+ if (pastVisits?.length) {
37
+ const encounters = mapEncounters(pastVisits[0]);
38
+
39
+ const tabsClasses = classNames(styles.verticalTabs, {
40
+ [styles.tabletTabs]: isTablet,
41
+ [styles.desktopTabs]: !isTablet,
42
+ });
43
+
44
+ const tabClass = (index) =>
45
+ classNames(styles.tab, styles.bodyLong01, {
46
+ [styles.selectedTab]: selectedTabIndex === index,
47
+ });
48
+
49
+ return (
50
+ <div className={styles.wrapper}>
51
+ <div className={styles.visitType}>
52
+ <span> {pastVisits?.length ? pastVisits[0]?.visitType.display : '--'}</span>
53
+ <p className={styles.date}>
54
+ {pastVisits?.length ? formatDate(parseDate(pastVisits[0]?.startDatetime)) : '--'}
55
+ </p>
56
+ </div>
57
+ <div className={styles.visitContainer}>
58
+ <Tabs className={tabsClasses}>
59
+ <Tab
60
+ className={tabClass[0]}
61
+ id="vitals-tab"
62
+ onClick={() => setSelectedTabIndex(0)}
63
+ label={t('vitals', 'Vitals')}></Tab>
64
+
65
+ <Tab
66
+ className={tabClass[1]}
67
+ id="notes-tab"
68
+ onClick={() => setSelectedTabIndex(1)}
69
+ label={t('notes', 'Notes')}></Tab>
70
+
71
+ <Tab
72
+ className={tabClass[2]}
73
+ id="medications-tab"
74
+ onClick={() => setSelectedTabIndex(2)}
75
+ label={t('medications', 'Medications')}></Tab>
76
+
77
+ <Tab
78
+ className={tabClass[3]}
79
+ id="encounters-tab"
80
+ onClick={() => setSelectedTabIndex(3)}
81
+ label={t('encounters', 'Encounters')}>
82
+ <EncounterList encounters={encounters} />
83
+ </Tab>
84
+ </Tabs>
85
+ </div>
86
+ </div>
87
+ );
88
+ }
89
+ return <p className={styles.bodyLong01}>{t('noPreviousVisitFound', 'No previous visit found')}</p>;
90
+ };
91
+
92
+ export function mapEncounters(visit) {
93
+ return visit?.encounters?.map((encounter) => ({
94
+ id: encounter?.uuid,
95
+ datetime: encounter?.encounterDatetime,
96
+ encounterType: encounter?.encounterType?.display,
97
+ form: encounter?.form,
98
+ obs: encounter?.obs,
99
+ provider:
100
+ encounter?.encounterProviders?.length > 0 ? encounter.encounterProviders[0].provider?.person?.display : '--',
101
+ visitUuid: visit?.visitType.uuid,
102
+ visitType: visit?.visitType?.name,
103
+ }));
104
+ }
105
+
106
+ export default PastVisit;
@@ -0,0 +1,25 @@
1
+ import useSWR from 'swr';
2
+ import { openmrsFetch, restBaseUrl, type Visit } from '@openmrs/esm-framework';
3
+
4
+ export function usePastVisits(patientUuid: string) {
5
+ const customRepresentation =
6
+ 'custom:(uuid,encounters:(uuid,encounterDatetime,' +
7
+ 'form:(uuid,name),location:ref,' +
8
+ 'encounterType:ref,encounterProviders:(uuid,display,' +
9
+ 'provider:(uuid,display))),patient:(uuid,uuid),' +
10
+ 'visitType:(uuid,name,display),attributes:(uuid,display,value),location:(uuid,name,display),startDatetime,' +
11
+ 'stopDatetime)';
12
+
13
+ const apiUrl = `${restBaseUrl}/visit?patient=${patientUuid}&v=${customRepresentation}`;
14
+ const { data, error, isLoading, isValidating } = useSWR<{ data: { results: Array<Visit> } }, Error>(
15
+ patientUuid ? apiUrl : null,
16
+ openmrsFetch,
17
+ );
18
+
19
+ return {
20
+ data: data ? data.data.results : null,
21
+ isError: error,
22
+ isLoading,
23
+ isValidating,
24
+ };
25
+ }