Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING: O3-2827: Appointments: Refactor (Part I) #971

Merged
merged 17 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions __mocks__/appointments.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ export const mockAppointmentsData = {
],
};

// TODO: still used by the queue-linelist-base-table test?
export const mockMappedAppointmentsData = {
data: [
{
Expand Down Expand Up @@ -374,3 +375,73 @@ export const mockProviders = {
},
],
};

export const mockUseAppointmentServiceData = [
{
appointmentServiceId: 1,
name: 'Outpatient',
description: null,
speciality: {},
startDateTime: new Date().toISOString(),
endTime: '',
maxAppointmentsLimit: null,
durationMins: 15,
location: {},
uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90',
color: '#006400',
initialAppointmentStatus: 'Scheduled',
creatorName: null,
weeklyAvailability: [
{
dayOfWeek: 'MONDAY',
startTime: '07:00:00',
endTime: '20:00:00',
maxAppointmentsLimit: null,
uuid: '7c7c53c8-c104-40cc-9926-50fc6fe4c4c1',
},
{
dayOfWeek: 'TUESDAY',
startTime: '07:00:00',
endTime: '20:00:00',
maxAppointmentsLimit: null,
uuid: '7683b94e-6c48-4132-b402-54837a8c0fb2',
},
{
dayOfWeek: 'SUNDAY',
startTime: '07:00:00',
endTime: '20:00:00',
maxAppointmentsLimit: null,
uuid: '00be8427-0037-4984-8875-6a5a2bc57e8e',
},
{
dayOfWeek: 'FRIDAY',
startTime: '07:00:00',
endTime: '20:00:00',
maxAppointmentsLimit: null,
uuid: 'af6b8d5b-be05-4e24-8601-30573f848bec',
},
{
dayOfWeek: 'THURSDAY',
startTime: '07:00:00',
endTime: '20:00:00',
maxAppointmentsLimit: null,
uuid: 'eb35e91b-6909-41fe-9d09-750b83fb3b9c',
},
{
dayOfWeek: 'SATURDAY',
startTime: '07:00:00',
endTime: '20:00:00',
maxAppointmentsLimit: null,
uuid: '7f6347fd-c514-4fd2-ab79-d7fd760bf82f',
},
{
dayOfWeek: 'WEDNESDAY',
startTime: '07:00:00',
endTime: '20:00:00',
maxAppointmentsLimit: null,
uuid: 'dad83f54-a0a2-4ba9-819b-01e906c89b69',
},
],
serviceTypes: [{ duration: 15, name: 'Chemotherapy', uuid: '53d58ff1-0c45-4e2e-9bd2-9cc826cb46e1' }],
},
];
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@
"extract-translations": "turbo extract-translations -- --config ../../tools/i18next-parser.config.js"
},
"dependencies": {
"@hookform/resolvers": "^3.3.1",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this isn't terribly important, it would be preferrable, I think, to keep all dependencies in devDependencies. These aren't in any way different from how we handle, e.g., dayjs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ibacher ! I thought I was just keeping this consistent with the patient chart, but it looks like they are all devDependencies there, I will correct.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so looking again, I see that these dependencies are listed as dependencies, not devDependencies, in patient-chart. However, when I moved them all to devDependencies here things worked. Do we have a best practice regarding this (to be honest, I don't really understand why these are all devdepencies, is the assumption thst they are all bundled with ESM framework anyway)? @ibacher @denniskigen @brandones

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we are somewhat inconsistent on this because we changed the norm a couple times during the first couple years of O3. I don't think there is a technical difference between devDependencies and dependencies for us, with our current tooling and setup, though at least some monorepo tools install devDependencies at the monorepo level and dependencies at the package level—but I think Yarn 4 just treats them the same.

Oddly, my inclination would be the opposite of what @ibacher suggests here—I am inclined to follow the rule that a dependency that gets built/bundled goes in dependencies. But I am quite open to being convinced otherwise 🤷

"classnames": "^2.3.2",
"react-hook-form": "^7.46.2",
"swr": "^2.0.1",
"xlsx": "^0.18.5"
"xlsx": "^0.18.5",
"zod": "^3.22.2"
},
"devDependencies": {
"@babel/core": "^7.11.6",
"@carbon/react": "~1.37.0",
"@openmrs/esm-framework": "next",
"@openmrs/esm-patient-common-lib": "next",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this dependency? I don't see it used in any of the code marked as coming from the chart.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forget why I needed it, but it was because something wasn't working... potentially something I later removed, I will take it out and see if things still work.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used in the appointment-form.component @ibacher

import { convertTime12to24 } from '@openmrs/esm-patient-common-lib';

"@playwright/test": "1.40.1",
"@swc/core": "^1.2.165",
"@swc/jest": "^0.2.29",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { openmrsFetch } from '@openmrs/esm-framework';
import { amPm } from '../../helpers';
import { AppointmentService } from '../../types';
import { type AppointmentService } from '../../types';

const appointmentServiceInitialValue: AppointmentService = {
appointmentServiceId: 0,
creatorName: '',
description: '',
durationMins: '',
durationMins: 0,
endTime: '',
initialAppointmentStatus: '',
location: { uuid: '', display: '' },
Expand Down
4 changes: 2 additions & 2 deletions packages/esm-appointments-app/src/appointments.component.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import AppointmentTabs from './appointments/appointment-tabs.component';
import AppointmentsHeader from './appointments-header/appointments-header.component';
import AppointmentMetrics from './appointments-metrics/appointments-metrics.component';
import AppointmentsHeader from './header/appointments-header.component';
import AppointmentMetrics from './metrics/appointments-metrics.component';
import Overlay from './overlay.component';

const Appointments: React.FC = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,28 @@ import utc from 'dayjs/plugin/utc';
import { Button, OverflowMenu, OverflowMenuItem } from '@carbon/react';
import { TaskComplete } from '@carbon/react/icons';
import { useTranslation } from 'react-i18next';
import { launchOverlay } from '../../hooks/useOverlay';
import { type MappedAppointment } from '../../types';
import { closeOverlay, launchOverlay } from '../../hooks/useOverlay';
import { type Appointment } from '../../types';
import { showModal } from '@openmrs/esm-framework';
import { useVisits } from '../../hooks/useVisits';
import AppointmentForm from '../forms/create-edit-form/appointments-form.component';
import AppointmentForm from '../../form/appointments-form.component';
import CheckInButton from './checkin-button.component';
import DefaulterTracingForm from '../forms/defaulter-tracing-form/default-tracing-form.component';

dayjs.extend(utc);
dayjs.extend(isToday);

interface AppointmentActionsProps {
visits: Array<any>;
appointment: MappedAppointment;
appointment: Appointment;
scheduleType: string;
mutate: () => void;
}

const AppointmentActions: React.FC<AppointmentActionsProps> = ({ visits, appointment, scheduleType }) => {
const AppointmentActions: React.FC<AppointmentActionsProps> = ({ visits, appointment, mutate }) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We seem to be passing mutate() through three or for different levels, just so we can access it as a prop in AppointmentForm. This seems like a perfect place for using a React context.

The PatientSearchContext is a pretty good example of how this can be used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it was annoying me that I had to pass this all the way around. I can check out out PatientSearchContext.

Another pattern I was seeing was using the useSWR call at multiple levels just to retrieve the mutate function, but that didn't seem correct, and seemed dependent on making sure you were asking for the same/correct key at multiple levels.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the interest of getting this PR merged without continuing to bloat, I will handle this as a separate ticket here: https://openmrs.atlassian.net/browse/O3-2849

Copy link
Contributor

@brandones brandones Feb 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can always create a new hook that does your useSWR call. Then you are sure to be making the same one each time. You shouldn't be passing mutates around. If you don't like the aesthetic of using a fetch hook just to obtain the mutate, then you can also write a custom hook that just obtains the correct mutate function. See useMutatePatientOrders for example. Ignore the comment, it's out of date. PR

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @brandones !

const { t } = useTranslation();
const { mutateVisit } = useVisits();
const patientUuid = appointment.patientUuid;
const visitDate = dayjs(appointment.dateTime);
const patientUuid = appointment.patient.uuid;
const visitDate = dayjs(appointment.startDateTime);
const isFutureAppointment = visitDate.isAfter(dayjs());
const isTodayAppointment = visitDate.isToday();
const hasActiveVisit = visits?.some((visit) => visit?.patient?.uuid === patientUuid && visit?.startDatetime);
Expand All @@ -44,17 +44,12 @@ const AppointmentActions: React.FC<AppointmentActionsProps> = ({ visits, appoint
});
};

const handleOpenDefaulterForm = () => {
launchOverlay('CCC Defaulter tracing form', <DefaulterTracingForm patientUuid={appointment.patientUuid} />);
};

/**
* Renders the appropriate visit status button based on the current appointment state.
* @returns {JSX.Element} The rendered button.
*/
const renderVisitStatus = () => {
const checkedOutText = t('checkedOut', 'Checked out');
const followUpButtonText = t('launchFormUpForm', 'Follow up');

switch (true) {
case hasCheckedOut:
Expand All @@ -70,27 +65,10 @@ const AppointmentActions: React.FC<AppointmentActionsProps> = ({ visits, appoint
</Button>
);
case isTodayAppointment: {
const isAfterNoon = new Date().getHours() > 12;

if (scheduleType === 'Pending' && isAfterNoon) {
return (
<Button onClick={handleOpenDefaulterForm} size="sm" kind="tertiary">
{followUpButtonText}
</Button>
);
}

Comment on lines -73 to -82
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not in a position to say whether this is right or not, but this does seem to indicate a point where we have implementation needs for extensibility that's not currently provided.

return <CheckInButton patientUuid={patientUuid} appointment={appointment} />;
}

default:
if (!isFutureAppointment) {
return (
<Button onClick={handleOpenDefaulterForm} size="sm" kind="tertiary">
{followUpButtonText}
</Button>
);
}
return null;
}
};
Expand All @@ -105,7 +83,12 @@ const AppointmentActions: React.FC<AppointmentActionsProps> = ({ visits, appoint
onClick={() =>
launchOverlay(
t('editAppointments', 'Edit Appointment'),
<AppointmentForm appointment={appointment} context="editing" />,
<AppointmentForm
appointment={appointment}
context="editing"
closeWorkspace={closeOverlay}
mutate={mutate}
/>,
)
}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,59 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import AppointmentActions from './appointments-actions.component';
import { type MappedAppointment } from '../../types';
import { type Appointment } from '../../types';

const appointment: Appointment = {
uuid: '7cd38a6d-377e-491b-8284-b04cf8b8c6d8',
appointmentNumber: '0000',
patient: {
identifier: '100GEJ',
identifiers: [],
name: 'John Wilson',
uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e',
gender: 'M',
age: '35',
birthDate: '1986-04-03T00:00:00.000+0000',
phoneNumber: '0700000000',
},
service: {
appointmentServiceId: 1,
name: 'Outpatient',
description: null,
startTime: '',
endTime: '',
maxAppointmentsLimit: null,
durationMins: null,
location: {
uuid: '8d6c993e-c2cc-11de-8d13-0010c6dffd0f',
},
uuid: 'e2ec9cf0-ec38-4d2b-af6c-59c82fa30b90',
initialAppointmentStatus: 'Scheduled',
creatorName: null,
},
provider: {
uuid: 'f9badd80-ab76-11e2-9e96-0800200c9a66',
person: { uuid: '24252571-dd5a-11e6-9d9c-0242ac150002', display: 'Dr James Cook' },
},
location: { name: 'HIV Clinic', uuid: '2131aff8-2e2a-480a-b7ab-4ac53250262b' },
startDateTime: new Date().toISOString(),
appointmentKind: 'WalkIn',
status: 'Scheduled',
comments: 'Some comments',
additionalInfo: null,
providers: [{ uuid: '24252571-dd5a-11e6-9d9c-0242ac150002', display: 'Dr James Cook' }],
recurring: false,
voided: false,
teleconsultationLink: null,
extensions: [],
};

describe('AppointmentActions', () => {
const defaultProps = {
visits: [],
appointment: {
patientUuid: '123',
dateTime: new Date().toISOString(),
} as MappedAppointment,
appointment: appointment,
scheduleType: 'Pending',
mutate: () => {},
};

beforeAll(() => {
Expand All @@ -30,7 +73,7 @@ describe('AppointmentActions', () => {
it('renders the correct button when the patient has checked out', () => {
const visits = [
{
patient: { uuid: '123' },
patient: { uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' },
startDatetime: new Date().toISOString(),
stopDatetime: new Date().toISOString(),
},
Expand All @@ -44,7 +87,7 @@ describe('AppointmentActions', () => {
it('renders the correct button when the patient has an active visit and today is the appointment date', () => {
const visits = [
{
patient: { uuid: '123' },
patient: { uuid: '8673ee4f-e2ab-4077-ba55-4980f408773e' },
startDatetime: new Date().toISOString(),
stopDatetime: null,
},
Expand All @@ -68,11 +111,4 @@ describe('AppointmentActions', () => {
const button = screen.getByRole('button', { name: /actions/i });
expect(button).toBeInTheDocument();
});

it('renders the correct button when the appointment is in the past or has not been scheduled', () => {
const props = { ...defaultProps, appointment: { ...defaultProps.appointment, dateTime: '2022-01-01' } };
render(<AppointmentActions {...props} />);
const button = screen.getByRole('button', { name: /follow up/i });
expect(button).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { Download } from '@carbon/react/icons';
import { EmptyState } from '../../empty-state/empty-state.component';
import { downloadAppointmentsAsExcel } from '../../helpers/excel';
import { launchOverlay } from '../../hooks/useOverlay';
import { type MappedAppointment } from '../../types';
import { type Appointment } from '../../types';
import { getPageSizes, useSearchResults } from '../utils';
import { type ConfigObject } from '../../config-schema';
import AppointmentDetails from '../details/appointment-details.component';
Expand All @@ -34,10 +34,10 @@ import PatientSearch from '../../patient-search/patient-search.component';
import styles from './appointments-table.scss';

interface AppointmentsTableProps {
appointments: Array<MappedAppointment>;
appointments: Array<Appointment>;
isLoading: boolean;
tableHeading: string;
mutate?: () => void;
mutate: () => void;
visits?: Array<any>;
scheduleType?: string;
}
Expand All @@ -46,6 +46,7 @@ const AppointmentsTable: React.FC<AppointmentsTableProps> = ({
appointments,
isLoading,
tableHeading,
mutate,
visits,
scheduleType,
}) => {
Expand Down Expand Up @@ -82,16 +83,18 @@ const AppointmentsTable: React.FC<AppointmentsTableProps> = ({
<ConfigurableLink
style={{ textDecoration: 'none', maxWidth: '50%' }}
to={customPatientChartUrl}
templateParams={{ patientUuid: appointment.patientUuid }}>
{appointment.name}
templateParams={{ patientUuid: appointment.patient.uuid }}>
{appointment.patient.name}
</ConfigurableLink>
),
nextAppointmentDate: '--',
identifier: appointment.identifier,
dateTime: formatDatetime(new Date(appointment.dateTime)),
serviceType: appointment.serviceType,
identifier: appointment.patient.identifier,
dateTime: formatDatetime(new Date(appointment.startDateTime)),
serviceType: appointment.service.name,
provider: appointment.provider,
actions: <AppointmentActions visits={visits} appointment={appointment} scheduleType={scheduleType} />,
actions: (
<AppointmentActions visits={visits} appointment={appointment} scheduleType={scheduleType} mutate={mutate} />
),
}));

if (isLoading) {
Expand Down Expand Up @@ -130,7 +133,9 @@ const AppointmentsTable: React.FC<AppointmentsTableProps> = ({
onClick={() =>
downloadAppointmentsAsExcel(
appointments,
`${tableHeading} Appointments ${formatDate(new Date(appointments[0]?.dateTime), { year: true })}`,
`${tableHeading} Appointments ${formatDate(new Date(appointments[0]?.startDateTime), {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FIXME for latter, but this should all be translatable.

year: true,
})}`,
)
}>
{t('download', 'Download')}
Expand Down
Loading
Loading