Skip to content

Commit

Permalink
(fix) Standardize appointment component props and fix filter behaviour (
Browse files Browse the repository at this point in the history
#1504)

* (refactor) Standardize appointment component props and improve filter handling

Relates to #1449.

Makes the following refactors to the Appointments app:

- Rename `appointmentServiceType` to `appointmentServiceTypes` in components that use it. Prior to #1449, this prop was a string, but is now an array of strings. The refactor also updates the components to handle the new prop type.
- Rename the `filterCancelled` prop to `excludeCancelledAppointments` in components that use it. The latter is more descriptive of the purpose of the prop.
- Fix `hasActiveFilters` logic to only consider service type filters. There's a tangential issue where the Today's appointments component shows an empty filter state when it shouldn't. It should show an empty state instead if there are no scheduled appointments for today. Cleaning up this logic allows me to isolate that issue so it can be fixed in a separate PR.

* Clean up hooks

* Remove initialSelectedItems from service type multi-select

Makes it so that the multi-select is not pre-selected with the first service type. With this change, no options are pre-selected by default. This more accurately wires up the filter to default to a blank state where all available appointments are shown. Users will have to explicitly choose which service types they want to filter by, rather than having one pre-selected for them.

* Review feedback

* More review feedback
  • Loading branch information
denniskigen authored Feb 27, 2025
1 parent 11229ff commit e62d4f9
Show file tree
Hide file tree
Showing 13 changed files with 113 additions and 93 deletions.
12 changes: 6 additions & 6 deletions packages/esm-appointments-app/src/appointments.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SelectedDateContext from './hooks/selectedDateContext';

const Appointments: React.FC = () => {
const { t } = useTranslation();
const [appointmentServiceType, setAppointmentServiceType] = useState<string[]>([]);
const [appointmentServiceTypes, setAppointmentServiceTypes] = useState<Array<string>>([]);
const [selectedDate, setSelectedDate] = useState(dayjs().startOf('day').format(omrsDateFormat));

const params = useParams();
Expand All @@ -23,19 +23,19 @@ const Appointments: React.FC = () => {

useEffect(() => {
if (params.serviceType) {
setAppointmentServiceType([params.serviceType]);
setAppointmentServiceTypes([params.serviceType]);
}
}, [params.serviceType]);

return (
<SelectedDateContext.Provider value={{ selectedDate, setSelectedDate }}>
<AppointmentsHeader
appointmentServiceType={appointmentServiceType}
onChange={setAppointmentServiceType}
appointmentServiceTypes={appointmentServiceTypes}
onChange={setAppointmentServiceTypes}
title={t('appointments', 'Appointments')}
/>
<AppointmentMetrics appointmentServiceType={appointmentServiceType} />
<AppointmentTabs appointmentServiceType={appointmentServiceType} />
<AppointmentMetrics appointmentServiceTypes={appointmentServiceTypes} />
<AppointmentTabs appointmentServiceTypes={appointmentServiceTypes} />
</SelectedDateContext.Provider>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import UnscheduledAppointments from './unscheduled/unscheduled-appointments.comp
import styles from './appointment-tabs.scss';

interface AppointmentTabsProps {
appointmentServiceType: string[];
appointmentServiceTypes: Array<string>;
}

const AppointmentTabs: React.FC<AppointmentTabsProps> = ({ appointmentServiceType }) => {
const AppointmentTabs: React.FC<AppointmentTabsProps> = ({ appointmentServiceTypes }) => {
const { t } = useTranslation();
const { showUnscheduledAppointmentsTab } = useConfig<ConfigObject>();
const [activeTabIndex, setActiveTabIndex] = useState(0);
Expand All @@ -30,15 +30,15 @@ const AppointmentTabs: React.FC<AppointmentTabsProps> = ({ appointmentServiceTyp
</TabList>
<TabPanels>
<TabPanel className={styles.tabPanel}>
<ScheduledAppointments appointmentServiceType={appointmentServiceType} />
<ScheduledAppointments appointmentServiceTypes={appointmentServiceTypes} />
</TabPanel>
<TabPanel className={styles.tabPanel}>
<UnscheduledAppointments />
</TabPanel>
</TabPanels>
</Tabs>
) : (
<ScheduledAppointments appointmentServiceType={appointmentServiceType} />
<ScheduledAppointments appointmentServiceTypes={appointmentServiceTypes} />
)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('AppointmentTabs', () => {
xit(`renders tabs showing different appointment lists`, async () => {
mockOpenmrsFetch.mockResolvedValue({ ...mockAppointmentsData } as unknown as FetchResponse);

renderWithSwr(<AppointmentTabs appointmentServiceType="" />);
renderWithSwr(<AppointmentTabs appointmentServiceTypes={['service-type-uuid']} />);

await waitForLoadingToFinish();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useEffect, useState } from 'react';
import styles from './appointment-details.scss';
import { usePatientAppointmentHistory } from '../../hooks/usePatientAppointmentHistory';
import { useTranslation } from 'react-i18next';
import { formatDate, formatDatetime, usePatient } from '@openmrs/esm-framework';
import { usePatientAppointmentHistory } from '../../hooks/usePatientAppointmentHistory';
import { getGender } from '../../helpers';
import { type Appointment } from '../../types';
import { useTranslation } from 'react-i18next';
import styles from './appointment-details.scss';

interface AppointmentDetailsProps {
appointment: Appointment;
Expand All @@ -21,6 +21,7 @@ const AppointmentDetails: React.FC<AppointmentDetailsProps> = ({ appointment })
setIsEnabledQuery(true);
}
}, [appointmentsCount, isLoading]);

return (
<div className={styles.appointmentDetailsContainer}>
<p className={styles.title}>{appointment.service.name}</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,44 @@
import React from 'react';
import React, { useMemo } from 'react';
import { filterByServiceType } from '../utils';
import { useAppointmentList } from '../../hooks/useAppointmentList';
import AppointmentsTable from '../common-components/appointments-table.component';

interface AppointmentsListProps {
appointmentServiceType?: string[];
appointmentServiceTypes?: Array<string>;
date: string;
excludeCancelledAppointments?: boolean;
status?: string;
title: string;
date: string;
filterCancelled?: boolean;
}

const AppointmentsList: React.FC<AppointmentsListProps> = ({
appointmentServiceType,
appointmentServiceTypes,
date,
excludeCancelledAppointments = false,
status,
title,
date,
filterCancelled = false,
}) => {
const { appointmentList, isLoading } = useAppointmentList(status, date);

const appointments = filterByServiceType(appointmentList, appointmentServiceType).map((appointment) => ({
id: appointment.uuid,
...appointment,
}));
const appointmentsFilteredByServiceType = filterByServiceType(appointmentList, appointmentServiceTypes).map(
(appointment) => ({
id: appointment.uuid,
...appointment,
}),
);

const activeAppointments = useMemo(() => {
return excludeCancelledAppointments
? appointmentsFilteredByServiceType.filter((appointment) => appointment.status !== 'Cancelled')
: appointmentsFilteredByServiceType;
}, [excludeCancelledAppointments, appointmentsFilteredByServiceType]);

const activeAppointments = filterCancelled
? appointments.filter((appointment) => appointment.status !== 'Cancelled')
: appointments;
return (
<AppointmentsTable
appointments={activeAppointments}
hasActiveFilters={appointmentServiceTypes?.length > 0}
isLoading={isLoading}
tableHeading={title}
hasActiveFilters={appointmentServiceType?.length > 0 || filterCancelled}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import React from 'react';
import { useEarlyAppointmentList } from '../../hooks/useAppointmentList';
import { useTranslation } from 'react-i18next';
import { filterByServiceType } from '../utils';
import { useEarlyAppointmentList } from '../../hooks/useAppointmentList';
import AppointmentsTable from '../common-components/appointments-table.component';
import { useTranslation } from 'react-i18next';

interface EarlyAppointmentsProps {
appointmentServiceType?: string[];
appointmentServiceTypes?: Array<string>;
date: string;
}

/**
* Component to display early appointments
* Note that although we define this extension in routes.jsx, we currently don't wire it into the scheduled-appointments-panels-slot by default because it requests a custom endpoint (see useEarlyAppointments) not provided by the standard Bahmni Appointments module
*/
const EarlyAppointments: React.FC<EarlyAppointmentsProps> = ({ appointmentServiceType, date }) => {
const EarlyAppointments: React.FC<EarlyAppointmentsProps> = ({ appointmentServiceTypes, date }) => {
const { t } = useTranslation();
const { earlyAppointmentList, isLoading } = useEarlyAppointmentList(date);

const appointments = filterByServiceType(earlyAppointmentList, appointmentServiceType).map((appointment, index) => {
const appointments = filterByServiceType(earlyAppointmentList, appointmentServiceTypes).map((appointment, index) => {
return {
id: `${index}`,
...appointment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ import styles from './scheduled-appointments.scss';
dayjs.extend(isSameOrBefore);

interface ScheduledAppointmentsProps {
appointmentServiceType?: string[];
appointmentServiceTypes?: Array<string>;
}

type DateType = 'pastDate' | 'today' | 'futureDate';

const scheduledAppointmentsPanelsSlot = 'scheduled-appointments-panels-slot';

const ScheduledAppointments: React.FC<ScheduledAppointmentsProps> = ({ appointmentServiceType }) => {
const ScheduledAppointments: React.FC<ScheduledAppointmentsProps> = ({ appointmentServiceTypes }) => {
const { t } = useTranslation();
const { selectedDate } = useContext(SelectedDateContext);
const layout = useLayoutType();
Expand Down Expand Up @@ -81,22 +81,22 @@ const ScheduledAppointments: React.FC<ScheduledAppointmentsProps> = ({ appointme
onChange={({ name }) => setCurrentTab(name)}
selectedIndex={panelsToShow.findIndex((panel) => panel.name == currentTab) ?? 0}
selectionMode="manual">
{panelsToShow.map((panel) => {
return <Switch key={`panel-${panel.name}`} name={panel.name} text={t(panel.config.title)} />;
})}
{panelsToShow.map((panel) => (
<Switch key={`panel-${panel.name}`} name={panel.name} text={t(panel.config.title)} />
))}
</ContentSwitcher>

<ExtensionSlot name={scheduledAppointmentsPanelsSlot}>
{(extension) => {
return (
<ExtensionWrapper
extension={extension}
appointmentServiceTypes={appointmentServiceTypes}
currentTab={currentTab}
appointmentServiceType={appointmentServiceType}
date={selectedDate}
dateType={dateType}
showExtensionTab={showExtension}
extension={extension}
hideExtensionTab={hideExtension}
showExtensionTab={showExtension}
/>
);
}}
Expand Down Expand Up @@ -133,15 +133,15 @@ function useAllowedExtensions() {
function ExtensionWrapper({
extension,
currentTab,
appointmentServiceType,
appointmentServiceTypes,
date,
dateType,
showExtensionTab,
hideExtensionTab,
}: {
extension: ConnectedExtension;
currentTab: string;
appointmentServiceType: string[];
appointmentServiceTypes: Array<string>;
date: string;
dateType: DateType;
showExtensionTab: (extension: string) => void;
Expand Down Expand Up @@ -173,7 +173,7 @@ function ExtensionWrapper({
<Extension
state={{
date,
appointmentServiceType,
appointmentServiceTypes,
status: extension.config?.status,
title: extension.config?.title,
}}
Expand Down
6 changes: 3 additions & 3 deletions packages/esm-appointments-app/src/appointments/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ export function useAppointmentSearchResults(data: Appointment[], searchString: s
}, [searchString, data]);
}

export function filterByServiceType(appointmentList: any[], appointmentServiceType: string[]) {
return appointmentServiceType?.length > 0
? appointmentList.filter(({ service }) => appointmentServiceType.includes(service.uuid))
export function filterByServiceType(appointmentList: Array<Appointment>, appointmentServiceTypes: Array<string>) {
return appointmentServiceTypes?.length > 0
? appointmentList.filter(({ service }) => appointmentServiceTypes.includes(service.uuid))
: appointmentList;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useContext, useMemo, useState } from 'react';
import dayjs from 'dayjs';
import { useTranslation } from 'react-i18next';
import { DatePicker, DatePickerInput, MultiSelect } from '@carbon/react';
Expand All @@ -10,11 +10,11 @@ import styles from './appointments-header.scss';

interface AppointmentHeaderProps {
title: string;
appointmentServiceType?: string[];
appointmentServiceTypes?: Array<string>;
onChange?: (evt) => void;
}

const AppointmentsHeader: React.FC<AppointmentHeaderProps> = ({ title, appointmentServiceType, onChange }) => {
const AppointmentsHeader: React.FC<AppointmentHeaderProps> = ({ title, onChange }) => {
const { t } = useTranslation();
const { selectedDate, setSelectedDate } = useContext(SelectedDateContext);
const { serviceTypes } = useAppointmentServices();
Expand Down Expand Up @@ -55,11 +55,10 @@ const AppointmentsHeader: React.FC<AppointmentHeaderProps> = ({ title, appointme
{typeof onChange === 'function' && (
<MultiSelect
id="serviceTypeMultiSelect"
label={t('filterAppointmentsByServiceType', 'Filter appointments by service type')}
items={serviceTypeOptions}
itemToString={(item) => (item ? item.label : '')}
label={t('filterAppointmentsByServiceType', 'Filter appointments by service type')}
onChange={handleMultiSelectChange}
initialSelectedItems={serviceTypeOptions.length > 0 ? [serviceTypeOptions[0].id] : []}
type="inline"
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const HomeAppointments = () => {
<div className={styles.container}>
<AppointmentsList
date={toOmrsIsoString(dayjs().startOf('day').toDate())}
filterCancelled
excludeCancelledAppointments
title={t('todays', "Today's")}
/>
</div>
Expand Down
Loading

0 comments on commit e62d4f9

Please sign in to comment.