From ef73f5f237fe5af3f7a2f6c9521fb84bdfa823ce Mon Sep 17 00:00:00 2001 From: Fredrick Kioko <67967749+its-kios09@users.noreply.github.com> Date: Wed, 8 Jan 2025 22:05:13 +0300 Subject: [PATCH] (feat) O3-3676 Implement ability to add patient to a queue from lab app (#111) * (feat) ability to patient to new queue from laboratory * (feat) added jest unit --- .../orders-data-table.component.tsx | 109 +++++++----------- ...nsition-patient-to-new-queue.component.tsx | 35 ++++++ .../transition-patient-to-new-queue.scss | 13 +++ .../transition-patient-to-new-queue.test.tsx | 64 ++++++++++ .../all-lab-requests-tile.component.tsx | 3 +- src/laboratory-resource.ts | 2 +- translations/en.json | 3 + 7 files changed, 162 insertions(+), 67 deletions(-) create mode 100644 src/lab-tabs/actions/transition-patient-to-new-queue/transition-patient-to-new-queue.component.tsx create mode 100644 src/lab-tabs/actions/transition-patient-to-new-queue/transition-patient-to-new-queue.scss create mode 100644 src/lab-tabs/actions/transition-patient-to-new-queue/transition-patient-to-new-queue.test.tsx diff --git a/src/components/orders-table/orders-data-table.component.tsx b/src/components/orders-table/orders-data-table.component.tsx index 9a2f87a6..257f8c0f 100644 --- a/src/components/orders-table/orders-data-table.component.tsx +++ b/src/components/orders-table/orders-data-table.component.tsx @@ -28,6 +28,7 @@ import type { FulfillerStatus, OrdersDataTableProps } from '../../types'; import { OrdersDateRangePicker } from './orders-date-range-picker.component'; import ListOrderDetails from './list-order-details.component'; import styles from './orders-data-table.scss'; +import TransitionLatestQueueEntryButton from '../../lab-tabs/actions/transition-patient-to-new-queue/transition-patient-to-new-queue.component'; const OrdersDataTable: React.FC = (props) => { const { t } = useTranslation(); @@ -63,7 +64,6 @@ const OrdersDataTable: React.FC = (props) => { return acc; }, {}); - // Convert the result to an array of objects with patientId and orders return Object.keys(groupedOrders).map((patientId) => ({ patientId: patientId, orders: groupedOrders[patientId], @@ -72,52 +72,34 @@ const OrdersDataTable: React.FC = (props) => { return []; } } + const groupedOrdersByPatient = groupOrdersById(flattenedLabOrders); const searchResults = useSearchGroupedResults(groupedOrdersByPatient, searchString); const orderStatuses = [ - { - value: null, - display: t('all', 'All'), - }, - { - value: 'NEW', - display: t('newStatus', 'NEW'), - }, - { - value: 'RECEIVED', - display: t('receivedStatus', 'RECEIVED'), - }, - { - value: 'IN_PROGRESS', - display: t('inProgressStatus', 'IN_PROGRESS'), - }, - { - value: 'COMPLETED', - display: t('completedStatus', 'COMPLETED'), - }, - { - value: 'EXCEPTION', - display: t('exceptionStatus', 'EXCEPTION'), - }, - { - value: 'ON_HOLD', - display: t('onHoldStatus', 'ON_HOLD'), - }, - { - value: 'DECLINED', - display: t('declinedStatus', 'DECLINED'), - }, + { value: null, display: t('all', 'All') }, + { value: 'NEW', display: t('newStatus', 'NEW') }, + { value: 'RECEIVED', display: t('receivedStatus', 'RECEIVED') }, + { value: 'IN_PROGRESS', display: t('inProgressStatus', 'IN_PROGRESS') }, + { value: 'COMPLETED', display: t('completedStatus', 'COMPLETED') }, + { value: 'EXCEPTION', display: t('exceptionStatus', 'EXCEPTION') }, + { value: 'ON_HOLD', display: t('onHoldStatus', 'ON_HOLD') }, + { value: 'DECLINED', display: t('declinedStatus', 'DECLINED') }, ]; const columns = useMemo(() => { - return [ + const baseColumns = [ { id: 0, header: t('patient', 'Patient'), key: 'patientName' }, { id: 1, header: t('age', 'Age'), key: 'patientAge' }, - { id: 2, header: t('totalOrders', 'Total Orders'), key: 'totalOrders' }, + { id: 2, header: t('gender', 'Gender'), key: 'patientGender' }, + { id: 3, header: t('totalOrders', 'Total Orders'), key: 'totalOrders' }, ]; - }, [t]); + + const showActionColumn = flattenedLabOrders.some((order) => order.fulfillerStatus === 'COMPLETED'); + + return showActionColumn ? [...baseColumns, { id: 4, header: t('action', 'Action'), key: 'action' }] : baseColumns; + }, [t, flattenedLabOrders]); const pageSizes = [10, 20, 30, 40, 50]; const [currentPageSize, setPageSize] = useState(10); @@ -128,16 +110,21 @@ const OrdersDataTable: React.FC = (props) => { const tableRows = useMemo(() => { return paginatedLabOrders.map((order) => ({ id: order.patientId, - patientName: order.orders[0].patient?.display?.split('-')[1], + patientName: order.orders[0]?.patient?.display?.split('-')[1], orders: order.orders, totalOrders: order.orders?.length, - patientAge: order.orders[0].patient?.person?.age, + patientAge: order.orders[0]?.patient?.person?.age, + patientGender: order.orders[0]?.patient?.person?.gender, + action: order.orders.some((o) => o.fulfillerStatus === 'COMPLETED') ? ( + + ) : null, })); }, [paginatedLabOrders]); if (isLoading) { return ; } + return ( {({ getExpandHeaderProps, getHeaderProps, getRowProps, getTableProps, headers, rows }) => ( @@ -180,27 +167,23 @@ const OrdersDataTable: React.FC = (props) => { - {rows.map((row) => { - return ( - - - {row.cells.map((cell) => ( - {cell.value?.content ?? cell.value} - ))} - - {row.isExpanded ? ( - - item.patientId === row.id)} - /> - - ) : ( - - )} - - ); - })} + {rows.map((row) => ( + + + {row.cells.map((cell) => ( + {cell.value?.content ?? cell.value} + ))} + + {row.isExpanded ? ( + + item.patientId === row.id)} + /> + + ) : null} + + ))} {rows.length === 0 ? ( @@ -225,12 +208,8 @@ const OrdersDataTable: React.FC = (props) => { totalItems={groupedOrdersByPatient?.length} className={styles.pagination} onChange={({ pageSize, page }) => { - if (pageSize !== currentPageSize) { - setPageSize(pageSize); - } - if (page !== currentPage) { - goTo(page); - } + if (pageSize !== currentPageSize) setPageSize(pageSize); + if (page !== currentPage) goTo(page); }} /> )} diff --git a/src/lab-tabs/actions/transition-patient-to-new-queue/transition-patient-to-new-queue.component.tsx b/src/lab-tabs/actions/transition-patient-to-new-queue/transition-patient-to-new-queue.component.tsx new file mode 100644 index 00000000..78564c2e --- /dev/null +++ b/src/lab-tabs/actions/transition-patient-to-new-queue/transition-patient-to-new-queue.component.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import styles from './transition-patient-to-new-queue.scss'; +import { showModal } from '@openmrs/esm-framework'; +import { Button } from '@carbon/react'; +import { AirlineManageGates } from '@carbon/react/icons'; + +interface TransitionLatestQueueEntryButtonProps { + patientUuid: string; +} + +const TransitionLatestQueueEntryButton: React.FC = ({ patientUuid }) => { + const { t } = useTranslation(); + + const launchModal = () => { + const dispose = showModal('transition-patient-to-latest-queue-modal', { + closeModal: () => dispose(), + patientUuid, + }); + }; + + return ( + + ); +}; + +export default TransitionLatestQueueEntryButton; diff --git a/src/lab-tabs/actions/transition-patient-to-new-queue/transition-patient-to-new-queue.scss b/src/lab-tabs/actions/transition-patient-to-new-queue/transition-patient-to-new-queue.scss new file mode 100644 index 00000000..326b443d --- /dev/null +++ b/src/lab-tabs/actions/transition-patient-to-new-queue/transition-patient-to-new-queue.scss @@ -0,0 +1,13 @@ +@use '@carbon/layout'; +@use '@carbon/styles/scss/type'; + +.addPatientToQueue { + --cds-layout-size-height-context: var(--cds-layout-size-height-sm, 2rem); + --cds-layout-size-height: var(--cds-layout-size-height-context); + display: flex; + align-items: center; + justify-content: center; + text-align: center; + padding: 0 layout.$spacing-04; + gap: layout.$spacing-05; +} diff --git a/src/lab-tabs/actions/transition-patient-to-new-queue/transition-patient-to-new-queue.test.tsx b/src/lab-tabs/actions/transition-patient-to-new-queue/transition-patient-to-new-queue.test.tsx new file mode 100644 index 00000000..1ef1aee8 --- /dev/null +++ b/src/lab-tabs/actions/transition-patient-to-new-queue/transition-patient-to-new-queue.test.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { useTranslation } from 'react-i18next'; +import { showModal } from '@openmrs/esm-framework'; +import TransitionLatestQueueEntryButton from './transition-patient-to-new-queue.component'; + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +jest.mock('@openmrs/esm-framework', () => ({ + showModal: jest.fn(), +})); + +jest.mock('@carbon/react', () => ({ + Button: ({ children, onClick, renderIcon }: any) => ( + + ), +})); + +jest.mock('@carbon/react/icons', () => ({ + AirlineManageGates: () => , +})); + +describe('TransitionLatestQueueEntryButton', () => { + const patientUuid = '1234-uuid'; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the button with correct text and icon', () => { + render(); + + const button = screen.getByTestId('transition-button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveTextContent('transition'); + + const icon = screen.getByTestId('airline-icon'); + expect(icon).toBeInTheDocument(); + }); + + it('calls the showModal function when clicked', () => { + render(); + + const button = screen.getByTestId('transition-button'); + fireEvent.click(button); + + expect(showModal).toHaveBeenCalledTimes(1); + expect(showModal).toHaveBeenCalledWith( + 'transition-patient-to-latest-queue-modal', + expect.objectContaining({ + closeModal: expect.any(Function), + patientUuid, + }), + ); + }); +}); diff --git a/src/lab-tiles/all-lab-requests-tile.component.tsx b/src/lab-tiles/all-lab-requests-tile.component.tsx index ea5d5cb6..7011c93c 100644 --- a/src/lab-tiles/all-lab-requests-tile.component.tsx +++ b/src/lab-tiles/all-lab-requests-tile.component.tsx @@ -5,7 +5,8 @@ import LabSummaryTile from '../components/summary-tile/lab-summary-tile.componen const AllLabRequestsTile = () => { const { t } = useTranslation(); - const { labOrders } = useLabOrders(); + + const { labOrders } = useLabOrders('NEW'); return ( (status === 'NEW' ? null : status), [status]); const newOrdersOnly = status === 'NEW'; const customRepresentation = - 'custom:(uuid,orderNumber,patient:(uuid,display,person:(uuid,display,age)),concept:(uuid,display),action,careSetting:(uuid,display,description,careSettingType,display),previousOrder,dateActivated,scheduledDate,dateStopped,autoExpireDate,encounter:(uuid,display),orderer:(uuid,display),orderReason,orderReasonNonCoded,orderType:(uuid,display,name,description,conceptClasses,parent),urgency,instructions,commentToFulfiller,display,fulfillerStatus,fulfillerComment,specimenSource,laterality,clinicalHistory,frequency,numberOfRepeats)'; + 'custom:(uuid,orderNumber,patient:(uuid,display,person:(uuid,display,age,gender)),concept:(uuid,display),action,careSetting:(uuid,display,description,careSettingType,display),previousOrder,dateActivated,scheduledDate,dateStopped,autoExpireDate,encounter:(uuid,display),orderer:(uuid,display),orderReason,orderReasonNonCoded,orderType:(uuid,display,name,description,conceptClasses,parent),urgency,instructions,commentToFulfiller,display,fulfillerStatus,fulfillerComment,specimenSource,laterality,clinicalHistory,frequency,numberOfRepeats)'; let url = `${restBaseUrl}/order?orderTypes=${laboratoryOrderTypeUuid}&v=${customRepresentation}`; url = fulfillerStatus ? url + `&fulfillerStatus=${fulfillerStatus}` : url; url = excludeCanceled ? `${url}&excludeCanceledAndExpired=true&excludeDiscontinueOrders=true` : url; diff --git a/translations/en.json b/translations/en.json index 51936b52..f9ebc9ec 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1,4 +1,5 @@ { + "action": "Action", "addLabResult": "Add lab results", "age": "Age", "all": "All", @@ -16,6 +17,7 @@ "exceptionStatus": "EXCEPTION", "filterOrdersByStatus": "Filter orders by status", "fulfillerComment": "Fulfiller comment", + "gender": "Gender", "In progress": "In progress", "inProgress": "In progress", "inProgressStatus": "IN_PROGRESS", @@ -58,6 +60,7 @@ "testsOrdered": "Tests ordered", "testType": "Test type", "totalOrders": "Total orders", + "transition": "Transition", "urgencyStatus": "Urgency: ", "viewTestResults": "View test results", "worklist": "Worklist",