@openmrs/esm-appointments-app 11.1.1-pre.9033 → 11.1.1-pre.9051

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.
@@ -5,4 +5,4 @@
5
5
  │ 6973.js (315.022 KiB)
6
6
  
7
7
 
8
- Rspack compiled with 1 warning in 37.63 s
8
+ Rspack compiled with 1 warning in 26.16 s
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.2.0","appointments":">=2.0.0"},"extensions":[{"name":"home-appointments","slot":"homepage-widgets-slot","component":"homeAppointments","order":1},{"name":"clinical-appointments-dashboard-link","slot":"homepage-dashboard-slot","component":"appointmentsDashboardLink","meta":{"name":"appointments","slot":"clinical-appointments-dashboard-slot","title":"Appointments"},"order":2},{"component":"root","name":"clinical-appointments-dashboard","slot":"clinical-appointments-dashboard-slot"},{"name":"appointments-dashboard","slot":"appointments-dashboard-slot","component":"appointmentsDashboard"},{"name":"appointments-calendar-dashboard-link","slot":"calendar-dashboard-slot","component":"appointmentsCalendarDashboardLink"},{"name":"todays-appointments-dashboard","slot":"todays-appointment-slot","component":"homeAppointments"},{"name":"early-appointments-panel","component":"earlyAppointments"},{"name":"patient-appointments-summary-dashboard","component":"patientAppointmentsSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"hideDashboardTitle":true,"path":"appointments","slot":"patient-chart-appointments-dashboard-slot","title":"Appointments"}},{"name":"appointments-details-widget","component":"patientAppointmentsDetailedSummary","slot":"patient-chart-appointments-dashboard-slot","meta":{"columnSpan":1}},{"name":"patient-upcoming-appointment-widget","component":"patientUpcomingAppointmentsWidget","slot":"visit-form-top-slot"},{"name":"home-appointments-tile","slot":"home-metrics-tiles-slot","component":"homeAppointmentsTile"},{"name":"metrics-card-scheduled-appointments","component":"metricsCardScheduledAppointments","slot":"appointments-metrics-slot","order":1},{"name":"metrics-card-highest-volume-service","component":"metricsCardHighestVolumeService","slot":"appointments-metrics-slot","order":2},{"name":"metrics-card-providers-booked","component":"metricsCardProvidersBooked","slot":"appointments-metrics-slot","order":3}],"modals":[{"name":"end-appointment-modal","component":"endAppointmentModal"},{"name":"cancel-appointment-modal","component":"cancelAppointmentModal"},{"name":"batch-change-appointments-statuses-modal","component":"batchChangeAppointmentStatusesModal"}],"workspaceGroups2":[{"name":"appointments-group","scopePattern":"/home/appointments"}],"workspaceWindows2":[{"name":"appointments-window","group":"appointments-group"}],"workspaces2":[{"name":"appointments-form-workspace","component":"appointmentsFormWorkspace","window":"appointments-window"},{"name":"appointments-patient-search-workspace","component":"@openmrs/esm-patient-search-app#patientSearchWorkspace2","window":"appointments-window"},{"name":"appointments-start-visit-workspace","component":"@openmrs/esm-patient-chart-app#exportedVisitForm","window":"appointments-window"}],"version":"11.1.1-pre.9033"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.2.0","appointments":">=2.0.0"},"extensions":[{"name":"home-appointments","slot":"homepage-widgets-slot","component":"homeAppointments","order":1},{"name":"clinical-appointments-dashboard-link","slot":"homepage-dashboard-slot","component":"appointmentsDashboardLink","meta":{"name":"appointments","slot":"clinical-appointments-dashboard-slot","title":"Appointments"},"order":2},{"component":"root","name":"clinical-appointments-dashboard","slot":"clinical-appointments-dashboard-slot"},{"name":"appointments-dashboard","slot":"appointments-dashboard-slot","component":"appointmentsDashboard"},{"name":"appointments-calendar-dashboard-link","slot":"calendar-dashboard-slot","component":"appointmentsCalendarDashboardLink"},{"name":"todays-appointments-dashboard","slot":"todays-appointment-slot","component":"homeAppointments"},{"name":"early-appointments-panel","component":"earlyAppointments"},{"name":"patient-appointments-summary-dashboard","component":"patientAppointmentsSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"hideDashboardTitle":true,"path":"appointments","slot":"patient-chart-appointments-dashboard-slot","title":"Appointments"}},{"name":"appointments-details-widget","component":"patientAppointmentsDetailedSummary","slot":"patient-chart-appointments-dashboard-slot","meta":{"columnSpan":1}},{"name":"patient-upcoming-appointment-widget","component":"patientUpcomingAppointmentsWidget","slot":"visit-form-top-slot"},{"name":"home-appointments-tile","slot":"home-metrics-tiles-slot","component":"homeAppointmentsTile"},{"name":"metrics-card-scheduled-appointments","component":"metricsCardScheduledAppointments","slot":"appointments-metrics-slot","order":1},{"name":"metrics-card-highest-volume-service","component":"metricsCardHighestVolumeService","slot":"appointments-metrics-slot","order":2},{"name":"metrics-card-providers-booked","component":"metricsCardProvidersBooked","slot":"appointments-metrics-slot","order":3}],"modals":[{"name":"end-appointment-modal","component":"endAppointmentModal"},{"name":"cancel-appointment-modal","component":"cancelAppointmentModal"},{"name":"batch-change-appointments-statuses-modal","component":"batchChangeAppointmentStatusesModal"}],"workspaceGroups2":[{"name":"appointments-group","scopePattern":"/home/appointments"}],"workspaceWindows2":[{"name":"appointments-window","group":"appointments-group"}],"workspaces2":[{"name":"appointments-form-workspace","component":"appointmentsFormWorkspace","window":"appointments-window"},{"name":"appointments-patient-search-workspace","component":"@openmrs/esm-patient-search-app#patientSearchWorkspace2","window":"appointments-window"},{"name":"appointments-start-visit-workspace","component":"@openmrs/esm-patient-chart-app#exportedVisitForm","window":"appointments-window"}],"version":"11.1.1-pre.9051"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-appointments-app",
3
- "version": "11.1.1-pre.9033",
3
+ "version": "11.1.1-pre.9051",
4
4
  "description": "Appointments front-end module for O3",
5
5
  "browser": "dist/openmrs-esm-appointments-app.js",
6
6
  "main": "src/index.ts",
@@ -0,0 +1,113 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Tag, InlineLoading } from '@carbon/react';
3
+ import { type Dayjs } from 'dayjs';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { formatAMPM } from '../../helpers/functions';
6
+ import { type Appointment } from '../../types';
7
+ import { useAppointmentsByDate } from '../../hooks/useAppointmentsByDate';
8
+ import {
9
+ getServiceColor,
10
+ STATUS_TAG_TYPES,
11
+ DEFAULT_STATUS_TAG_TYPE,
12
+ CALENDAR_HOURS,
13
+ formatHourLabel,
14
+ } from '../utils/calendar-colors';
15
+ import styles from './daily-calendar-view.scss';
16
+
17
+ const LOCALE_MAP: Record<string, string> = {
18
+ gregory: 'en-US',
19
+ ethiopic: 'am-ET',
20
+ islamic: 'ar-SA',
21
+ persian: 'fa-IR',
22
+ };
23
+
24
+ interface DailyCalendarViewProps {
25
+ calKey: string;
26
+ calendarSelectedDate: Dayjs;
27
+ }
28
+
29
+ const DailyCalendarView: React.FC<DailyCalendarViewProps> = ({ calKey, calendarSelectedDate }) => {
30
+ const { t } = useTranslation();
31
+ const isoDate = calendarSelectedDate.format('YYYY-MM-DD');
32
+ const locale = LOCALE_MAP[calKey] ?? 'en-US';
33
+ const { appointments, isLoading } = useAppointmentsByDate(isoDate);
34
+
35
+ const displayDate = useMemo(() => {
36
+ const d = calendarSelectedDate.toDate();
37
+ return new Intl.DateTimeFormat(locale, {
38
+ year: 'numeric',
39
+ month: 'long',
40
+ day: 'numeric',
41
+ calendar: calKey,
42
+ }).format(d);
43
+ }, [calendarSelectedDate, locale, calKey]);
44
+
45
+ const hourSlots = useMemo(
46
+ () =>
47
+ CALENDAR_HOURS.map((hr) => ({
48
+ hr,
49
+ appts: appointments.filter((a) => {
50
+ if (a.startDateTime == null) return false;
51
+ return new Date(a.startDateTime).getHours() === hr;
52
+ }),
53
+ })).filter((s) => s.appts.length > 0),
54
+ [appointments],
55
+ );
56
+
57
+ if (isLoading) {
58
+ return (
59
+ <div className={styles.container}>
60
+ <InlineLoading description={t('loadingAppointments', 'Loading appointments…')} />
61
+ </div>
62
+ );
63
+ }
64
+
65
+ return (
66
+ <div className={styles.container}>
67
+ <div className={styles.heading}>
68
+ <h2 className={styles.title}>{displayDate}</h2>
69
+ <p className={styles.subtitle}>
70
+ {appointments.length === 0
71
+ ? t('noAppointments', 'No appointments scheduled')
72
+ : t('appointmentCount', '{{count}} appointment(s)', { count: appointments.length })}
73
+ </p>
74
+ </div>
75
+ {hourSlots.map(({ hr, appts }) => (
76
+ <div key={hr} className={styles.hourRow}>
77
+ <div className={styles.hourLabel}>{formatHourLabel(hr)}</div>
78
+ <div className={styles.hourSlot}>
79
+ {appts.map((a) => (
80
+ <DailyCard key={a.uuid} appointment={a} />
81
+ ))}
82
+ </div>
83
+ </div>
84
+ ))}
85
+ </div>
86
+ );
87
+ };
88
+
89
+ const DailyCard: React.FC<{ appointment: Appointment }> = ({ appointment }) => {
90
+ const color = getServiceColor(appointment.service.name);
91
+ const tagType = STATUS_TAG_TYPES[appointment.status] ?? DEFAULT_STATUS_TAG_TYPE;
92
+ const time = useMemo(() => {
93
+ if (appointment.startDateTime == null) return '—';
94
+ return formatAMPM(new Date(appointment.startDateTime));
95
+ }, [appointment.startDateTime]);
96
+
97
+ return (
98
+ <div className={styles.card} style={{ borderLeftColor: color }}>
99
+ <span className={styles.cardTime}>{time}</span>
100
+ <div className={styles.cardDetails}>
101
+ <div className={styles.cardName}>{appointment.patient?.name ?? '—'}</div>
102
+ <div className={styles.cardService} style={{ color }}>
103
+ {appointment.service.name}
104
+ </div>
105
+ </div>
106
+ <Tag type={tagType} size="sm">
107
+ {appointment.status}
108
+ </Tag>
109
+ </div>
110
+ );
111
+ };
112
+
113
+ export default DailyCalendarView;
@@ -0,0 +1,96 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .container {
6
+ margin: layout.$spacing-05;
7
+ border: 1px solid colors.$gray-20;
8
+ background: colors.$white;
9
+ overflow-x: auto;
10
+ }
11
+
12
+ .heading {
13
+ display: flex;
14
+ align-items: baseline;
15
+ gap: layout.$spacing-03;
16
+ padding: layout.$spacing-03 layout.$spacing-05;
17
+ border-bottom: 1px solid colors.$gray-20;
18
+ background: colors.$gray-10;
19
+ }
20
+
21
+ .title {
22
+ @include type.type-style('heading-compact-02');
23
+ color: colors.$gray-100;
24
+ margin: 0;
25
+ }
26
+
27
+ .subtitle {
28
+ @include type.type-style('label-01');
29
+ color: colors.$gray-50;
30
+ margin: 0;
31
+ }
32
+
33
+ .hourRow {
34
+ display: flex;
35
+ border-bottom: 1px solid colors.$gray-20;
36
+
37
+ &:last-child {
38
+ border-bottom: none;
39
+ }
40
+ }
41
+
42
+ .hourLabel {
43
+ @include type.type-style('label-01');
44
+ color: colors.$gray-60;
45
+ font-size: 0.6875rem;
46
+ white-space: nowrap;
47
+ min-width: 6.5rem;
48
+ padding: layout.$spacing-02 layout.$spacing-03 0 layout.$spacing-03;
49
+ text-align: right;
50
+ border-right: 1px solid colors.$gray-20;
51
+ background: colors.$white;
52
+ flex-shrink: 0;
53
+ }
54
+
55
+ .hourSlot {
56
+ flex: 1;
57
+ display: flex;
58
+ flex-direction: column;
59
+ gap: layout.$spacing-02;
60
+ padding: layout.$spacing-02 layout.$spacing-04;
61
+ min-height: 3rem;
62
+ background: colors.$white;
63
+ }
64
+
65
+ .card {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: layout.$spacing-04;
69
+ padding: layout.$spacing-04 layout.$spacing-04;
70
+ margin: layout.$spacing-03 0;
71
+ border-left: 3px solid;
72
+ border-radius: 0 2px 2px 0;
73
+ background: colors.$gray-10;
74
+ }
75
+
76
+ .cardTime {
77
+ @include type.type-style('label-01');
78
+ color: colors.$gray-60;
79
+ min-width: 3.5rem;
80
+ flex-shrink: 0;
81
+ }
82
+
83
+ .cardDetails {
84
+ flex: 1;
85
+ min-width: 0;
86
+ }
87
+
88
+ .cardName {
89
+ @include type.type-style('body-compact-01');
90
+ color: colors.$gray-100;
91
+ }
92
+
93
+ .cardService {
94
+ @include type.type-style('label-01');
95
+ color: colors.$gray-50;
96
+ }
@@ -0,0 +1,152 @@
1
+ import React from 'react';
2
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
3
+ import { screen, waitFor } from '@testing-library/react';
4
+ import dayjs from 'dayjs';
5
+ import { type FetchResponse, openmrsFetch } from '@openmrs/esm-framework';
6
+ import { renderWithSwr } from 'tools';
7
+ import { type Appointment, AppointmentStatus, AppointmentKind } from '../../types';
8
+ import DailyCalendarView from './daily-calendar-view.component';
9
+
10
+ vi.mock('react-i18next', () => ({
11
+ useTranslation: () => ({
12
+ t: (key: string, fallback?: string, opts?: { count?: number }) => {
13
+ if (opts?.count !== undefined) return `${opts.count} appointment(s)`;
14
+ return fallback ?? key;
15
+ },
16
+ }),
17
+ }));
18
+
19
+ const mockOpenmrsFetch = vi.mocked(openmrsFetch);
20
+
21
+ const defaultProps = {
22
+ calKey: 'gregory',
23
+ calendarSelectedDate: dayjs('2026-06-09'),
24
+ };
25
+
26
+ const mockAppointment = (overrides: Partial<Appointment> = {}): Appointment => ({
27
+ uuid: '3b4d4f2a-7c8d-4e1f-9a6b-5c8d2e1f4a7b',
28
+ appointmentNumber: 'APT-0001',
29
+ appointmentKind: AppointmentKind.SCHEDULED,
30
+ comments: '',
31
+ endDateTime: new Date('2026-06-09T09:15:00').getTime(),
32
+ location: { uuid: 'b1a8b05e-3542-4037-bbd3-998ee9c40574', name: 'Inpatient Ward' },
33
+ patient: {
34
+ identifier: '100GEJ',
35
+ name: 'John Wilson',
36
+ uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e',
37
+ },
38
+ provider: { uuid: 'f9badd80-ab76-11e2-9e96-0800200c9a66', display: 'doctor - James Cook' },
39
+ providers: [{ uuid: 'f9badd80-ab76-11e2-9e96-0800200c9a66' }],
40
+ recurring: false,
41
+ service: {
42
+ appointmentServiceId: 1,
43
+ creatorName: 'Test Creator',
44
+ description: 'Outpatient service',
45
+ durationMins: 15,
46
+ endTime: '17:00',
47
+ initialAppointmentStatus: 'Scheduled',
48
+ maxAppointmentsLimit: null,
49
+ name: 'Outpatient',
50
+ startTime: '08:00',
51
+ uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90',
52
+ },
53
+ startDateTime: new Date('2026-06-09T09:00:00').getTime(),
54
+ dateAppointmentScheduled: new Date('2026-06-09T00:00:00.000Z').getTime(),
55
+ status: AppointmentStatus.SCHEDULED,
56
+ voided: false,
57
+ extensions: {},
58
+ teleconsultationLink: null,
59
+ ...overrides,
60
+ });
61
+
62
+ describe('DailyCalendarView', () => {
63
+ beforeEach(() => {
64
+ mockOpenmrsFetch.mockReset();
65
+ });
66
+
67
+ it('shows loading spinner while fetching', () => {
68
+ mockOpenmrsFetch.mockReturnValue(new Promise(() => {}));
69
+ renderWithSwr(<DailyCalendarView {...defaultProps} />);
70
+ expect(screen.getByText(/Loading appointments/i)).toBeInTheDocument();
71
+ });
72
+
73
+ it('shows empty state when no appointments', async () => {
74
+ mockOpenmrsFetch.mockResolvedValue({ data: [] } as FetchResponse);
75
+ renderWithSwr(<DailyCalendarView {...defaultProps} />);
76
+
77
+ await waitFor(() => {
78
+ expect(screen.getByText(/No appointments scheduled/i)).toBeInTheDocument();
79
+ });
80
+ });
81
+
82
+ it('renders the date heading and appointment count', async () => {
83
+ mockOpenmrsFetch.mockResolvedValue({ data: [mockAppointment()] } as FetchResponse);
84
+ renderWithSwr(<DailyCalendarView {...defaultProps} />);
85
+
86
+ await screen.findByText(/June 9, 2026/);
87
+ await screen.findByText('1 appointment(s)');
88
+ });
89
+
90
+ it('renders date using the given calendar key', async () => {
91
+ mockOpenmrsFetch.mockResolvedValue({ data: [mockAppointment()] } as FetchResponse);
92
+ renderWithSwr(<DailyCalendarView {...defaultProps} calKey="ethiopic" />);
93
+
94
+ await waitFor(() => expect(screen.getByRole('heading').textContent).toBeTruthy());
95
+ });
96
+
97
+ it('renders hour labels only for hours with appointments', async () => {
98
+ mockOpenmrsFetch.mockResolvedValue({
99
+ data: [
100
+ mockAppointment({
101
+ uuid: '3b4d4f2a-7c8d-4e1f-9a6b-5c8d2e1f4a7b',
102
+ startDateTime: new Date('2026-06-09T09:00:00').getTime(),
103
+ }),
104
+ mockAppointment({
105
+ uuid: '5c6e8f1b-9a2d-4c3f-8b7a-1d2e3f4a5b6c',
106
+ startDateTime: new Date('2026-06-09T14:00:00').getTime(),
107
+ }),
108
+ ],
109
+ } as FetchResponse);
110
+ renderWithSwr(<DailyCalendarView {...defaultProps} />);
111
+
112
+ await screen.findByText('9 AM');
113
+ await screen.findByText('2 PM');
114
+ await waitFor(() => expect(screen.queryByText('10 AM')).not.toBeInTheDocument());
115
+ });
116
+
117
+ it('renders appointment card with patient name, service, time, and status', async () => {
118
+ mockOpenmrsFetch.mockResolvedValue({
119
+ data: [
120
+ mockAppointment({
121
+ service: { ...mockAppointment().service, name: 'HIV Clinic' },
122
+ startDateTime: new Date('2026-06-09T14:30:00').getTime(),
123
+ }),
124
+ ],
125
+ } as FetchResponse);
126
+ renderWithSwr(<DailyCalendarView {...defaultProps} />);
127
+
128
+ await screen.findByText('John Wilson');
129
+ await screen.findByText('HIV Clinic');
130
+ await screen.findByText('2:30 PM');
131
+ await screen.findByText('Scheduled');
132
+ });
133
+
134
+ it('renders multiple appointments in the same hour slot', async () => {
135
+ mockOpenmrsFetch.mockResolvedValue({
136
+ data: [
137
+ mockAppointment({
138
+ patient: { identifier: '100GEJ', name: 'John Wilson', uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' },
139
+ startDateTime: new Date('2026-06-09T09:00:00').getTime(),
140
+ }),
141
+ mockAppointment({
142
+ patient: { identifier: '100732HE', name: 'Jane Smith', uuid: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' },
143
+ startDateTime: new Date('2026-06-09T09:30:00').getTime(),
144
+ }),
145
+ ],
146
+ } as FetchResponse);
147
+ renderWithSwr(<DailyCalendarView {...defaultProps} />);
148
+
149
+ await screen.findByText('John Wilson');
150
+ await screen.findByText('Jane Smith');
151
+ });
152
+ });