Skip to content

Commit

Permalink
(feat) - O3-4200 Service queues - use visit form in patient chart for… (
Browse files Browse the repository at this point in the history
#1402)

* (feat) - O3-4200 Service queues - use visit form in patient chart for create queue entry workflow

* rename slot names
  • Loading branch information
chibongho authored Dec 7, 2024
1 parent 15c68bb commit 3e16a00
Show file tree
Hide file tree
Showing 48 changed files with 453 additions and 1,842 deletions.
2 changes: 1 addition & 1 deletion __mocks__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export * from './identifiers.mock';
export * from './locations.mock';
export * from './metrics.mock';
export * from './patient.mock';
export * from './patient-visits.mock';
export * from './patient-appointments.mock';
export * from './patient-registration.mock';
export * from './queue-entry.mock';
export * from './queue-rooms.mock';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const mockPatientsVisits = {
recentVisits: [
export const mockPatientAppointments = {
recentAppointments: [
{
uuid: '6baa7963-68ea-497e-b258-6fb82382bd07',
appointmentNumber: '0000',
Expand Down Expand Up @@ -59,7 +59,7 @@ export const mockPatientsVisits = {
recurring: false,
},
],
futureVisits: [
futureAppointments: [
{
uuid: '6baa7963-68ea-497e-b258-6fb82382bd07',
appointmentNumber: '0000',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ const CheckInButton: React.FC<CheckInButtonProps> = ({ appointment, patientUuid
to: checkInButton.customUrl,
templateParams: { patientUuid: appointment.patient.uuid, appointmentUuid: appointment.uuid },
})
: launchWorkspace('start-visit-workspace-form', { patientUuid: patientUuid, showPatientHeader: true })
: launchWorkspace('start-visit-workspace-form', {
patientUuid: patientUuid,
showPatientHeader: true,
openedFrom: 'appointments-check-in',
})
}>
{t('checkIn', 'Check in')}
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,72 @@ import {
StructuredListRow,
StructuredListWrapper,
} from '@carbon/react';
import { formatDate, parseDate } from '@openmrs/esm-framework';
import { usePatientAppointments } from './patient-appointments.resource';
import { formatDate, parseDate, showSnackbar, type Visit } from '@openmrs/esm-framework';
import { changeAppointmentStatus, usePatientAppointments } from './patient-appointments.resource';
import { ErrorState } from '@openmrs/esm-patient-common-lib';
import styles from './patient-upcoming-appointments-card.scss';
import dayjs from 'dayjs';
import { type Appointment } from '../types';
import { useMutateAppointments } from '../form/appointments-form.resource';

interface PatientUpcomingAppointmentsProps {
// See VisitFormExtensionState in esm-patient-chart-app
export interface PatientUpcomingAppointmentsProps {
setOnVisitCreatedOrUpdated(onSubmit: (visit: Visit) => Promise<any>);
visitFormOpenedFrom: string;
patientChartConfig?: {
showUpcomingAppointments: boolean;
};
patientUuid: string;
setUpcomingAppointment: (value: Appointment) => void;
}

/**
* This is an extension that gets slotted into the patient chart start visit form when
* the appropriate config values are enabled.
* @param param0
* @returns
*/
const PatientUpcomingAppointmentsCard: React.FC<PatientUpcomingAppointmentsProps> = ({
patientUuid,
setUpcomingAppointment,
setOnVisitCreatedOrUpdated,
patientChartConfig,
}) => {
const { t } = useTranslation();
const startDate = dayjs(new Date().toISOString()).subtract(6, 'month').toISOString();
const headerTitle = t('upcomingAppointments', 'Upcoming appointments');
const [selectedAppointment, setSelectedAppointment] = useState(null);
const [selectedAppointment, setSelectedAppointment] = useState<Appointment>(null);
const { mutateAppointments } = useMutateAppointments();

const ac = useMemo<AbortController>(() => new AbortController(), []);
useEffect(() => () => ac.abort(), [ac]);
const { data: appointmentsData, error, isLoading } = usePatientAppointments(patientUuid, startDate, ac);

useEffect(() => {
setOnVisitCreatedOrUpdated(() => {
if (selectedAppointment) {
return changeAppointmentStatus('CheckedIn', selectedAppointment.uuid)
.then(() => {
mutateAppointments();
showSnackbar({
isLowContrast: true,
kind: 'success',
subtitle: t('appointmentMarkedChecked', 'Appointment marked as Checked In'),
title: t('appointmentCheckedIn', 'Appointment Checked In'),
});
})
.catch((error) => {
showSnackbar({
title: t('updateError', 'Error updating upcoming appointment'),
kind: 'error',
isLowContrast: false,
subtitle: error?.message,
});
});
} else {
return Promise.resolve();
}
});
}, [selectedAppointment, mutateAppointments, setOnVisitCreatedOrUpdated, t]);

const todaysAppointments = appointmentsData?.todaysAppointments?.length ? appointmentsData?.todaysAppointments : [];
const futureAppointments = appointmentsData?.upcomingAppointments?.length
? appointmentsData?.upcomingAppointments
Expand All @@ -46,9 +87,12 @@ const PatientUpcomingAppointmentsCard: React.FC<PatientUpcomingAppointmentsProps

const handleRadioChange = (appointment: Appointment) => {
setSelectedAppointment(appointment);
setUpcomingAppointment(appointment);
};

if (!patientChartConfig.showUpcomingAppointments) {
return <></>;
}

if (error) {
return <ErrorState headerTitle={headerTitle} error={error} />;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/esm-appointments-app/src/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
{
"name": "patient-upcoming-appointment-widget",
"component": "patientUpcomingAppointmentsWidget",
"slot": "upcoming-appointment-slot"
"slot": "visit-form-top-slot"
},
{
"name": "edit-appointments-form",
Expand Down
3 changes: 3 additions & 0 deletions packages/esm-appointments-app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"appointmentCancelError": "Error cancelling appointment",
"appointmentCancelled": "Appointment Cancelled",
"appointmentCancelledSuccessfully": "Appointment cancelled successfully",
"appointmentCheckedIn": "Appointment Checked In",
"appointmentColor": "Appointment color",
"appointmentConflict": "Appointment conflict",
"appointmentEdited": "Appointment edited",
Expand All @@ -18,6 +19,7 @@
"appointmentEndError": "Error ending appointment",
"appointmentFormError": "Error scheduling appointment",
"appointmentHistory": "Appointment History",
"appointmentMarkedChecked": "Appointment marked as Checked In",
"appointmentMetrics": "Appointment metrics",
"appointmentMetricsLoadError": "",
"appointmentNoteLabel": "Write an additional note",
Expand Down Expand Up @@ -158,6 +160,7 @@
"unscheduledAppointments_lower": "unscheduled appointments",
"upcoming": "Upcoming",
"upcomingAppointments": "Upcoming appointments",
"updateError": "Error updating upcoming appointment",
"view": "View",
"vitals": "Vitals",
"week": "Week",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,68 +193,6 @@ export function useServiceQueueEntries(service: string, locationUuid: string) {
};
}

export async function postQueueEntry(
visitUuid: string,
queueUuid: string,
patientUuid: string,
priority: string,
status: string,
sortWeight: number,
locationUuid: string,
visitQueueNumberAttributeUuid: string,
) {
const abortController = new AbortController();

await Promise.all([generateVisitQueueNumber(locationUuid, visitUuid, queueUuid, visitQueueNumberAttributeUuid)]);

return openmrsFetch(`${restBaseUrl}/visit-queue-entry`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
signal: abortController.signal,
body: {
visit: { uuid: visitUuid },
queueEntry: {
status: {
uuid: status,
},
priority: {
uuid: priority,
},
queue: {
uuid: queueUuid,
},
patient: {
uuid: patientUuid,
},
startedAt: new Date(),
sortWeight: sortWeight,
},
},
});
}

export async function generateVisitQueueNumber(
location: string,
visitUuid: string,
queueUuid: string,
visitQueueNumberAttributeUuid: string,
) {
const abortController = new AbortController();

await openmrsFetch(
`${restBaseUrl}/queue-entry-number?location=${location}&queue=${queueUuid}&visit=${visitUuid}&visitAttributeType=${visitQueueNumberAttributeUuid}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: abortController.signal,
},
);
}

export function serveQueueEntry(servicePointName: string, ticketNumber: string, status: string) {
const abortController = new AbortController();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { type ConfigObject } from '../config-schema';
import { useQueues } from '../hooks/useQueues';
import { updateQueueEntry } from './active-visits-table.resource';
import { useMutateQueueEntries } from '../hooks/useQueueEntries';
import { useQueueLocations } from '../patient-search/hooks/useQueueLocations';
import { useQueueLocations } from '../create-queue-entry/hooks/useQueueLocations';
import styles from './change-status-dialog.scss';

interface ChangeStatusDialogProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jest.mock('./active-visits-table.resource', () => ({
updateQueueEntry: jest.fn(),
}));

jest.mock('../patient-search/hooks/useQueueLocations', () => {
jest.mock('../create-queue-entry/hooks/useQueueLocations', () => {
return {
useQueueLocations: jest.fn().mockReturnValue({
queueLocations: mockLocations.data?.results.map((location) => ({ ...location, id: location.uuid })),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
SelectItem,
} from '@carbon/react';
import { showSnackbar } from '@openmrs/esm-framework';
import { useQueueLocations } from '../patient-search/hooks/useQueueLocations';
import { useQueueLocations } from '../create-queue-entry/hooks/useQueueLocations';
import {
addProviderToQueueRoom,
updateProviderToQueueRoom,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jest.mock('../hooks/useQueues', () => {
};
});

jest.mock('../patient-search/hooks/useQueueLocations', () => ({
jest.mock('../create-queue-entry/hooks/useQueueLocations', () => ({
useQueueLocations: jest.fn().mockReturnValue({
queueLocations: [
{ id: '1GHI12', name: 'Location 1' },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Button, DataTableSkeleton } from '@carbon/react';
import {
ArrowLeftIcon,
ErrorState,
ExtensionSlot,
getPatientName,
PatientBannerContactDetails,
PatientBannerPatientInfo,
PatientBannerToggleContactDetailsButton,
PatientPhoto,
usePatient,
useVisit,
type DefaultWorkspaceProps,
} from '@openmrs/esm-framework';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './create-queue-entry.scss';
import ExistingVisitFormComponent from './existing-visit-form/existing-visit-form.component';

interface PatientSearchProps extends DefaultWorkspaceProps {
selectedPatientUuid: string;
currentServiceQueueUuid?: string;
handleBackToSearchList?: () => void;
}

export const AddPatientToQueueContext = React.createContext({
currentServiceQueueUuid: '',
});

/**
*
* This is the component that appears when clicking on a search result in the "Add patient to queue" workspace,
*/
const CreateQueueEntryWorkspace: React.FC<PatientSearchProps> = ({
closeWorkspace,
promptBeforeClosing,
selectedPatientUuid,
currentServiceQueueUuid,
handleBackToSearchList,
}) => {
const { t } = useTranslation();
const { patient } = usePatient(selectedPatientUuid);
const { activeVisit, isLoading, error } = useVisit(selectedPatientUuid);

const [showContactDetails, setContactDetails] = useState(false);

const patientName = patient && getPatientName(patient);

return patient ? (
<div className={styles.patientSearchContainer}>
<AddPatientToQueueContext.Provider value={{ currentServiceQueueUuid }}>
<div className={styles.patientBannerContainer}>
<div className={styles.patientBanner}>
<div className={styles.patientPhoto}>
<PatientPhoto patientUuid={patient.id} patientName={patientName} />
</div>
<PatientBannerPatientInfo patient={patient} />
<PatientBannerToggleContactDetailsButton
showContactDetails={showContactDetails}
toggleContactDetails={() => setContactDetails(!showContactDetails)}
/>
</div>
{showContactDetails ? (
<PatientBannerContactDetails patientId={patient.id} deceased={patient.deceasedBoolean} />
) : null}
</div>
<div className={styles.backButton}>
<Button
kind="ghost"
renderIcon={(props) => <ArrowLeftIcon size={24} {...props} />}
iconDescription={t('backToSearchResults', 'Back to search results')}
size="sm"
onClick={() => handleBackToSearchList?.()}>
<span>{t('backToSearchResults', 'Back to search results')}</span>
</Button>
</div>
{isLoading ? (
<DataTableSkeleton role="progressbar" />
) : error ? (
<ErrorState headerTitle={t('errorFetchingVisit', 'Error fetching patient visit')} error={error} />
) : activeVisit ? (
<ExistingVisitFormComponent visit={activeVisit} closeWorkspace={closeWorkspace} />
) : (
<ExtensionSlot
name="start-visit-workspace-form-slot"
state={{
patientUuid: selectedPatientUuid,
closeWorkspace,
promptBeforeClosing,
openedFrom: 'service-queues-add-patient',
}}
/>
)}
</AddPatientToQueueContext.Provider>
</div>
) : null;
};

export default CreateQueueEntryWorkspace;
Loading

0 comments on commit 3e16a00

Please sign in to comment.