Skip to content

Commit

Permalink
(feat) O3-2942 service queues - provide a way to edit queue entries i…
Browse files Browse the repository at this point in the history
…n place (#1017)

* (feat) O3-2942 service queues - provide a way to edit queue entries in place

* Apply suggestions from code review

Co-authored-by: Dennis Kigen <[email protected]>

* misc typo fix / renaming

---------

Co-authored-by: Dennis Kigen <[email protected]>
  • Loading branch information
chibongho and denniskigen authored Mar 8, 2024
1 parent cdf20e8 commit d277bf4
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 35 deletions.
10 changes: 9 additions & 1 deletion packages/esm-service-queues-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,21 @@ export const addProviderToRoomModal = getAsyncLifecycle(
);

export const transitionQueueEntryModal = getAsyncLifecycle(
() => import('./queue-table/transitions/transition-queue-entry-modal.component'),
() => import('./queue-table/queue-entry-actions/transition-queue-entry-modal.component'),
{
featureName: 'transfer patient to a different queue',
moduleName,
},
);

export const editQueueEntryModal = getAsyncLifecycle(
() => import('./queue-table/queue-entry-actions/edit-queue-entry-modal.component'),
{
featureName: 'edit queue entry of a patient',
moduleName,
},
);

export const addQueueEntry = getSyncLifecycle(addQueueEntryComponent, options);

export function startupApp() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { Button, OverflowMenu, OverflowMenuItem } from '@carbon/react';
import { Button } from '@carbon/react';
import { showModal } from '@openmrs/esm-framework';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { type QueueTableCellComponentProps, type QueueTableColumn } from '../../types';

export function QueueTableTransitionCell({ queueEntry }: QueueTableCellComponentProps) {
export function QueueTableActionCell({ queueEntry }: QueueTableCellComponentProps) {
const { t } = useTranslation();

return (
<div>
<Button
kind="ghost"
aria-label="Actions"
onClick={() => {
const dispose = showModal('edit-queue-entry-modal', {
closeModal: () => dispose(),
queueEntry,
});
}}>
{t('edit', 'Edit')}
</Button>
<Button
kind="ghost"
aria-label="Actions"
Expand All @@ -24,8 +35,8 @@ export function QueueTableTransitionCell({ queueEntry }: QueueTableCellComponent
);
}

export const queueTableTransitionColumn: QueueTableColumn = {
export const queueTableActionColumn: QueueTableColumn = {
headerI18nKey: '',
CellComponent: QueueTableTransitionCell,
CellComponent: QueueTableActionCell,
getFilterableValue: null,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { type QueueEntry } from '../../types';
import QueueEntryActionModal from './queue-entry-actions-modal.component';
import { updateQueueEntry } from './queue-entry-actions.resource';
import { useQueues } from '../../hooks/useQueues';

interface EditQueueEntryModalProps {
queueEntry: QueueEntry;
closeModal: () => void;
}

const EditQueueEntryModal: React.FC<EditQueueEntryModalProps> = ({ queueEntry, closeModal }) => {
const { t } = useTranslation();
const { queues } = useQueues();

return (
<QueueEntryActionModal
queueEntry={queueEntry}
closeModal={closeModal}
formParams={{
modalTitle: t('editQueueEntry', 'Edit queue entry'),
modalInstruction: t('editQueueEntryInstruction', 'Edit fields of existing queue entry'),
submitButtonText: t('editQueueEntry', 'Edit queue entry'),
submitSuccessTitle: t('queueEntryEdited', 'Queue entry edited'),
submitSuccessText: t('queueEntryEditedSuccessfully', 'Queue entry edited successfully'),
submitFailureTitle: t('queueEntryEditingFailed', 'Error editing queue entry'),
submitAction: (queueEntry, formState) => {
const selectedQueue = queues.find((q) => q.uuid == formState.selectedQueue);
const statuses = selectedQueue?.allowedStatuses;
const priorities = selectedQueue?.allowedPriorities;

return updateQueueEntry(queueEntry.uuid, {
status: statuses.find((s) => s.uuid == formState.selectedStatus),
priority: priorities.find((p) => p.uuid == formState.selectedPriority),
priorityComment: formState.prioritycomment,
});
},
disableSubmit: () => false,
}}
/>
);
};

export default EditQueueEntryModal;
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,59 @@ import {
Stack,
Switch,
} from '@carbon/react';
import { type FetchResponse, showSnackbar } from '@openmrs/esm-framework';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutateQueueEntries } from '../../hooks/useMutateQueueEntries';
import { useQueues } from '../../hooks/useQueues';
import { type QueueEntry } from '../../types';
import styles from './transition-dialogs.scss';
import { showSnackbar } from '@openmrs/esm-framework';
import { useMutateQueueEntries } from '../../hooks/useMutateQueueEntries';
import { transitionQueueEntry } from './transitions.resource';
import styles from './queue-entry-actons-modal.scss';
import { TextArea } from '@carbon/react';

interface TransitionQueueEntryModalProps {
interface QueueEntryActionModalProps {
queueEntry: QueueEntry;
closeModal: () => void;
formParams: FormParams;
}

interface FormState {
selectedQueue: string;
selectedPriority: string;
selectedStatus: string;
prioritycomment: string;
}

interface FormParams {
modalTitle: string;
modalInstruction: string;
submitButtonText: string;
submitSuccessTitle: string;
submitSuccessText: string;
submitFailureTitle: string;
submitAction: (queueEntry: QueueEntry, formState: FormState) => Promise<FetchResponse<any>>;
disableSubmit: (queueEntry, formState) => boolean;
}

const TransitionQueueEntryModal: React.FC<TransitionQueueEntryModalProps> = ({ queueEntry, closeModal }) => {
// Modal with form to provide the same UI for editting or transitioning a queue entry
export const QueueEntryActionModal: React.FC<QueueEntryActionModalProps> = ({ queueEntry, closeModal, formParams }) => {
const { t } = useTranslation();
const { mutateQueueEntries } = useMutateQueueEntries();
const {
modalTitle,
modalInstruction,
submitButtonText,
submitSuccessTitle,
submitSuccessText,
submitFailureTitle,
submitAction,
disableSubmit,
} = formParams;

const [formState, setFormState] = useState<FormState>({
selectedQueue: queueEntry.queue.uuid,
selectedPriority: queueEntry.priority.uuid,
selectedStatus: queueEntry.status.uuid,
prioritycomment: queueEntry.priorityComment ?? '',
});
const { queues } = useQueues();
const [isSubmitting, setIsSubmitting] = useState(false);
Expand All @@ -61,6 +86,7 @@ const TransitionQueueEntryModal: React.FC<TransitionQueueEntryModalProps> = ({ q
selectedQueue: selectedQueueUuid,
selectedStatus: newQueueHasCurrentStatus ? formState.selectedStatus : allowedStatuses[0]?.uuid,
selectedPriority: newQueueHasCurrentPriority ? formState.selectedPriority : allowedPriorities[0]?.uuid,
prioritycomment: formState.prioritycomment,
});
};

Expand All @@ -72,23 +98,22 @@ const TransitionQueueEntryModal: React.FC<TransitionQueueEntryModalProps> = ({ q
setFormState({ ...formState, selectedStatus: selectedStatusUuid });
};

const setPriorityComment = (prioritycomment: string) => {
setFormState({ ...formState, prioritycomment });
};

const submitForm = (e) => {
e.preventDefault();
setIsSubmitting(true);

transitionQueueEntry({
queueEntryToTransition: queueEntry.uuid,
newQueue: formState.selectedQueue,
newStatus: formState.selectedStatus,
newPriority: formState.selectedPriority,
})
submitAction(queueEntry, formState)
.then(({ status }) => {
if (status === 200) {
showSnackbar({
isLowContrast: true,
title: t('queueEntryTransitioned', 'Queue entry transitioned'),
title: submitSuccessTitle,
kind: 'success',
subtitle: t('queueEntryTransitionedSuccessfully', 'Queue entry transitioned successfully'),
subtitle: submitSuccessText,
});
mutateQueueEntries();
closeModal();
Expand All @@ -98,7 +123,7 @@ const TransitionQueueEntryModal: React.FC<TransitionQueueEntryModalProps> = ({ q
})
.catch((error) => {
showSnackbar({
title: t('queueEntryTransitionFailed', 'Error transitioning queue entry'),
title: submitFailureTitle,
kind: 'error',
subtitle: error?.message,
});
Expand All @@ -113,12 +138,12 @@ const TransitionQueueEntryModal: React.FC<TransitionQueueEntryModalProps> = ({ q
return (
<div>
<Form onSubmit={submitForm}>
<ModalHeader closeModal={closeModal} title={t('transitionPatient', 'Transition patient')} />
<ModalHeader closeModal={closeModal} title={modalTitle} />
<ModalBody>
<div className={styles.modalBody}>
<Stack gap={4}>
<h5>{queueEntry.display}</h5>
<p>{t('transitionPatientStatusOrQueue', 'Select a new status or queue for patient to transition to.')}</p>
<p>{modalInstruction}</p>
<section className={styles.section}>
<div className={styles.sectionTitle}>{t('serviceQueue', 'Service queue')}</div>
<Select
Expand Down Expand Up @@ -206,25 +231,28 @@ const TransitionQueueEntryModal: React.FC<TransitionQueueEntryModalProps> = ({ q
</ContentSwitcher>
)}
</section>
<section className={styles.section}>
<div className={styles.sectionTitle}>{t('priorityComment', 'Priority comment')}</div>
<TextArea
value={formState.prioritycomment}
onChange={(e) => setPriorityComment(e.target.value)}
placeholder={t('enterCommentHere', 'Enter comment here')}
/>
</section>
</Stack>
</div>
</ModalBody>
<ModalFooter>
<Button kind="secondary" onClick={closeModal}>
{t('cancel', 'Cancel')}
</Button>
<Button
disabled={
isSubmitting ||
(formState.selectedQueue == queueEntry.queue.uuid && formState.selectedStatus == queueEntry.status.uuid)
}
type="submit">
{t('transitionPatient', 'Transition patient')}
<Button disabled={isSubmitting || disableSubmit(queueEntry, formState)} type="submit">
{submitButtonText}
</Button>
</ModalFooter>
</Form>
</div>
);
};

export default TransitionQueueEntryModal;
export default QueueEntryActionModal;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React from 'react';
import { renderWithSwr } from 'tools';
import TransitionQueueEntryModal from './transition-queue-entry-modal.component';
import userEvent from '@testing-library/user-event';
import EditQueueEntryModal from './edit-queue-entry-modal.component';

const mockedOpenmrsFetch = openmrsFetch as jest.Mock;

Expand Down Expand Up @@ -80,3 +81,68 @@ describe('TransitionQueueEntryModal: ', () => {
expect(showSnackbar).toHaveBeenCalled();
});
});

describe('EditQueueEntryModal: ', () => {
const queueEntry = mockQueueEntryBrian;
const { queue } = queueEntry;
const { allowedStatuses, allowedPriorities } = queue;

const nextQueue = mockQueueSurgery;

it('renders the dialog with the right status and priority options', () => {
renderWithSwr(<EditQueueEntryModal queueEntry={queueEntry} closeModal={() => {}} />);
expect(screen.getByText(queueEntry.patient.display)).toBeInTheDocument();

for (const status of allowedStatuses) {
const expectedStatusDisplay =
queueEntry.status.uuid == status.uuid ? `${status.display} (Current)` : status.display;
expect(screen.getByRole('radio', { name: expectedStatusDisplay })).toBeInTheDocument();
}

for (const pri of allowedPriorities) {
const expectedPriorityDisplay = queueEntry.priority.uuid == pri.uuid ? `${pri.display} (Current)` : pri.display;
expect(screen.getByRole('radio', { name: expectedPriorityDisplay })).toBeInTheDocument();
}
});

it('has a cancel button that closes the modal', async () => {
const closeModal = jest.fn();
const user = userEvent.setup();

renderWithSwr(<EditQueueEntryModal queueEntry={queueEntry} closeModal={closeModal} />);
const cancelButton = screen.getByText('Cancel');
await user.click(cancelButton);
expect(closeModal).toHaveBeenCalled();
});

it('has a enabled submit button when selected queue and status is same as before', () => {
renderWithSwr(<EditQueueEntryModal queueEntry={queueEntry} closeModal={() => {}} />);
const submitButton = screen.getByRole('button', { name: /Edit queue entry/ });
expect(submitButton).toBeEnabled();
});

it('has an working submit button when selected queue and status is different from before', async () => {
mockedOpenmrsFetch.mockResolvedValue({
status: 200,
});
const user = userEvent.setup();
renderWithSwr(<EditQueueEntryModal queueEntry={queueEntry} closeModal={() => {}} />);

// change queue
const queueDropdown = screen.getByRole('combobox');
await queueDropdown.click();
const queueSelection = screen.getByRole('option', { name: nextQueue.display });
await user.selectOptions(queueDropdown, queueSelection);

// change status
const inServiceRadioButton = screen.getByText(mockStatusInService.display);
await inServiceRadioButton.click();

const submitButton = screen.getByRole('button', { name: /Edit queue entry/ });
expect(submitButton).not.toBeDisabled();
await submitButton.click();

expect(mockedOpenmrsFetch).toHaveBeenCalled();
expect(showSnackbar).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
import { openmrsFetch, restBaseUrl, type Location } from '@openmrs/esm-framework';
import { type Concept, type QueueEntry } from '../../types';

// see QueueEntryTransition.java in openmrs-module-queue
interface TransitionQueueEntryParams {
queueEntryToTransition: string;
transitionDate?: string;
newQueue?: string;
newStatus?: string;
newPriority?: string;
newPriorityComment?: string;
}

/**
Expand All @@ -27,3 +30,30 @@ export function transitionQueueEntry(params: TransitionQueueEntryParams, abortCo
body: params,
});
}

// see QueueEntryResource.java#getUpdatableProperties() in openmrs-module-queue
interface UpdateQueueEntryParams {
status?: Concept;
priority?: Concept;
priorityComment?: string;
sortWeight?: number;
startedAt?: string;
endedAt?: string;
loationWaitingFor?: Location;
providerWaitingFor?: Location;
}

export function updateQueueEntry(
queueEntryUuid: string,
params: UpdateQueueEntryParams,
abortController?: AbortController,
) {
return openmrsFetch(`${restBaseUrl}/queue-entry/${queueEntryUuid}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
signal: abortController?.signal,
body: params,
});
}
Loading

0 comments on commit d277bf4

Please sign in to comment.