import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import dayjs from 'dayjs'; import { capitalize } from 'lodash-es'; import { useTranslation } from 'react-i18next'; import { useReactToPrint } from 'react-to-print'; import { useSWRConfig } from 'swr'; import { Button, DataTable, DataTableSkeleton, Dropdown, InlineLoading, Layer, OverflowMenu, OverflowMenuItem, Search, Table, TableBody, TableCell, TableContainer, TableExpandedRow, TableExpandHeader, TableExpandRow, TableHead, TableHeader, TableRow, TableToolbarContent, Tag, Tile, } from '@carbon/react'; import { CardHeader, EmptyState, ErrorState, getDrugOrderByUuid, invalidateVisitByUuid, PatientChartPagination, type FulfillerStatus, type Order, type OrderBasketItem, type OrderType, type OrderUrgency, useLaunchWorkspaceRequiringVisit, useOrderBasket, useOrderTypes, usePatientOrders, } from '@openmrs/esm-patient-common-lib'; import { prepMedicationOrderPostData } from '@openmrs/esm-patient-medications-app/src/api/api'; import { prepTestOrderPostData } from '@openmrs/esm-patient-tests-app/src/test-orders/api'; import { prepOrderPostData } from '../order-basket/general-order-type/resources'; import { AddIcon, age, ExtensionSlot, formatDate, getCoreTranslation, getPatientName, launchWorkspace2, OpenmrsDateRangePicker, parseDate, PrinterIcon, useConfig, useLayoutType, usePagination, } from '@openmrs/esm-framework'; import { buildGeneralOrder, buildLabOrder, buildMedicationOrder } from '../utils'; import { ORDER_TYPES, getOrderGrouping, isValidOmrsOrderType } from '../constants/order-types'; import GeneralOrderTable from './general-order-table.component'; import MedicationRecord from './medication-record.component'; import PrintComponent from '../print/print.component'; import styles from './order-details-table.scss'; interface OrderDetailsProps { patientUuid: string; patient: fhir.Patient; showAddButton?: boolean; showPrintButton?: boolean; title?: string; } interface OrderBasketItemActionsProps { orderItem: Order; patient: fhir.Patient; } interface OrderHeaderProps { key: string; header: string; isSortable: boolean; } function getPriorityTagType(urgency: OrderUrgency) { switch (urgency) { case 'ROUTINE': return 'green'; case 'STAT': return 'red'; default: return 'gray'; } } function getStatusTagType(status: FulfillerStatus) { switch (status) { case 'RECEIVED': return 'blue'; case 'IN_PROGRESS': return 'cyan'; case 'ON_HOLD': return 'teal'; case 'EXCEPTION': return 'magenta'; case 'COMPLETED': return 'green'; case 'DECLINED': return 'red'; default: return 'gray'; } } const OrderDetailsTable: React.FC = ({ patientUuid, patient, showAddButton, showPrintButton, title, }) => { const { t } = useTranslation(); const defaultPageSize = 10; const headerTitle = t('orders', 'Orders'); const isTablet = useLayoutType() === 'tablet'; const responsiveSize = isTablet ? 'lg' : 'md'; const _launchOrderBasket = useLaunchWorkspaceRequiringVisit(patientUuid, 'order-basket'); const contentToPrintRef = useRef(null); const { excludePatientIdentifierCodeTypes } = useConfig(); const [isPrinting, setIsPrinting] = useState(false); const { data: orderTypes } = useOrderTypes(); const [selectedOrderTypeUuid, setSelectedOrderTypeUuid] = useState(null); // UI-controlled date range const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([new Date(), new Date()]); // Derived API filter dates (ISO strings) const [startDate, endDate] = dateRange; const selectedFromDate = useMemo(() => (startDate ? dayjs(startDate).format('YYYY-MM-DD') : null), [startDate]); const selectedToDate = useMemo(() => (endDate ? dayjs(endDate).format('YYYY-MM-DD') : null), [endDate]); const selectedOrderType = orderTypes?.find((x) => x.uuid === selectedOrderTypeUuid); const getOrderTypeDisplayText = useCallback( (orderType: OrderType | undefined) => { if (!orderType) { return t('ordersLower', 'orders'); } const label = (orderType.display || orderType.name || '').toLowerCase(); if (label.includes('test')) { return t('testOrders', 'test orders'); } if (label.includes('drug')) { return t('drugOrders', 'drug orders'); } return t('ordersLower', 'orders'); }, [t], ); const emptyStateDisplayText = useMemo(() => { return getOrderTypeDisplayText(selectedOrderType); }, [selectedOrderType, getOrderTypeDisplayText]); const getOrderTypeLabel = useCallback( (orderType: OrderType) => { if (!orderType) { return ''; } if (!orderType.uuid) { return t('allOrders', 'All orders'); } const label = (orderType.display || orderType.name || '').toLowerCase(); if (label.includes('drug')) { const value = t('drugOrders', 'Drug orders'); return value ? value.charAt(0).toUpperCase() + value.slice(1) : value; } if (label.includes('test')) { const value = t('testOrders', 'Test orders'); return value ? value.charAt(0).toUpperCase() + value.slice(1) : value; } return orderType.display || orderType.name || ''; }, [t], ); const { data: allOrders, error, isLoading, isValidating, } = usePatientOrders(patientUuid, 'ACTIVE', selectedOrderTypeUuid, selectedFromDate, selectedToDate); const displayedOrders = useMemo(() => allOrders ?? [], [allOrders]); const launchOrderBasketForNewItem = useCallback( () => _launchOrderBasket(null, { encounterUuid: '' }), [_launchOrderBasket], ); const tableHeaders: Array = [ { key: 'orderNumber', header: t('orderNumber', 'Order number'), isSortable: true, }, { key: 'dateOfOrder', header: t('dateOfOrder', 'Date of order'), isSortable: true, }, { key: 'orderType', header: t('orderType', 'Order type'), isSortable: true, }, { key: 'order', header: t('order', 'Order'), isSortable: true, }, { key: 'priority', header: t('priority', 'Priority'), isSortable: false, }, { key: 'orderedBy', header: t('orderedBy', 'Ordered by'), isSortable: false, }, { key: 'status', header: t('status', 'Status'), isSortable: false, }, ]; const tableRows = useMemo( () => displayedOrders?.map((order) => ({ id: order.uuid, dateActivated: order.dateActivated, orderNumber: order.orderNumber, dateOfOrder:
{formatDate(parseDate(order.dateActivated))}
, orderType: capitalize(order.orderType?.display ?? '-'), order: order.display, priority: ( { // t('ON_SCHEDULED_DATE', 'On scheduled date') // t('ROUTINE', 'Routine') // t('STAT', 'STAT') } {t(order.urgency, capitalize(order.urgency.replaceAll('_', ' ')))} ), orderedBy: order.orderer?.display, status: order.fulfillerStatus ? ( { // t('COMPLETED', 'Completed') // t('DECLINED', 'Declined') // t('DISCONTINUED', 'Discontinued') // t('EXCEPTION', 'Exception') // t('IN_PROGRESS', 'In progress') // t('ON_HOLD', 'On hold') // t('RECEIVED', 'Received') } {t(order.fulfillerStatus, capitalize(order.fulfillerStatus.replaceAll('_', ' ')))} ) : ( '--' ), })) ?? [], [displayedOrders, t], ); const { results: paginatedOrders, goTo, currentPage } = usePagination(tableRows, defaultPageSize); const patientDetails = useMemo(() => { const getGender = (gender: string): string => { switch (gender) { case 'male': return getCoreTranslation('male'); case 'female': return getCoreTranslation('female'); case 'other': return getCoreTranslation('other'); case 'unknown': return getCoreTranslation('unknown'); default: return gender; } }; const identifiers = patient?.identifier?.filter( (identifier) => !excludePatientIdentifierCodeTypes?.uuids?.includes(identifier.type.coding[0].code), ) ?? []; return { name: patient ? getPatientName(patient) : '', age: age(patient?.birthDate), gender: getGender(patient?.gender), location: patient?.address?.[0].city, identifiers: identifiers?.length ? identifiers.map(({ value }) => value) : [], }; }, [patient, excludePatientIdentifierCodeTypes?.uuids]); const onBeforeGetContentResolve = useRef void)>(null); useEffect(() => { if (isPrinting && onBeforeGetContentResolve.current) { onBeforeGetContentResolve.current(); } }, [isPrinting]); const handlePrint = useReactToPrint({ content: () => contentToPrintRef.current, documentTitle: `OpenMRS - ${patientDetails.name} - ${title}`, onBeforeGetContent: () => new Promise((resolve) => { if (patient && title) { onBeforeGetContentResolve.current = resolve; setIsPrinting(true); } }), onAfterPrint: () => { onBeforeGetContentResolve.current = null; setIsPrinting(false); }, }); const orderTypesToDisplay = useMemo( () => [ { display: t('allOrders', 'All orders'), uuid: null, }, ...(orderTypes?.map((orderType) => ({ display: orderType.display, uuid: orderType.uuid, })) ?? []), ], [orderTypes, t], ); const handleDateFilterChange = useCallback( ([startDate, endDate]: [Date | null | undefined, Date | null | undefined]) => { setDateRange([startDate ?? null, endDate ?? null]); }, [], ); const isOmrsOrder = useCallback((orderItem: Order) => isValidOmrsOrderType(orderItem.type), []); return ( <>
getOrderTypeLabel(orderType)} label={t('allOrders', 'All orders')} onChange={(e: { selectedItem: OrderType }) => { const selected = e.selectedItem; setSelectedOrderTypeUuid(selected?.uuid ?? null); }} selectedItem={orderTypesToDisplay.find((x) => x.uuid === selectedOrderTypeUuid)} titleText={t('selectOrderType', 'Select order type') + ':'} type="inline" />
{isLoading ? ( ) : error ? ( ) : orderTypes?.length > 0 ? ( <> {!tableRows?.length ? ( launchOrderBasketForNewItem()} /> ) : (
{isValidating ? ( ) : null}
{showPrintButton && ( )} {showAddButton && ( )}
{({ getExpandedRowProps, getExpandHeaderProps, getHeaderProps, getRowProps, getTableContainerProps, getTableProps, headers, onInputChange, rows, }) => ( <> {!isPrinting && (
) => onInputChange(e)} placeholder={t('searchTable', 'Search table')} />
)} {headers.map((header) => ( {header.header} ))} {!isPrinting && } {rows.map((row) => { const matchingOrder = allOrders?.find((order) => order.uuid === row.id); const isExpandable = matchingOrder?.type === ORDER_TYPES.DRUG_ORDER || matchingOrder?.type === ORDER_TYPES.GENERAL_ORDER; const cells = row.cells.map((cell) => ( {cell.value?.['content'] ?? cell.value} )); const actionCell = !isPrinting && ( {matchingOrder && isOmrsOrder(matchingOrder) ? ( ) : ( )} ); return ( {isExpandable ? ( {cells} {actionCell} ) : ( {cells} {actionCell} )} {isExpandable && (row.isExpanded ? ( {matchingOrder?.type === ORDER_TYPES.DRUG_ORDER ? ( ) : ( )} ) : ( ))} ); })}
{rows.length === 0 ? (

{t('noMatchingOrdersToDisplay', 'No matching orders to display')}

{t('checkFilters', 'Check the filters above')}

) : null} )}
{!isPrinting && (
goTo(page)} />
)}
)} ) : null} ); }; function OrderBasketItemActions({ orderItem, patient }: OrderBasketItemActionsProps) { const { t } = useTranslation(); const isDeclined = orderItem.fulfillerStatus === 'DECLINED'; const { grouping, postDataPrepFn } = useMemo(() => { if (orderItem.type === ORDER_TYPES.DRUG_ORDER) { return { grouping: getOrderGrouping(ORDER_TYPES.DRUG_ORDER), postDataPrepFn: prepMedicationOrderPostData, }; } else if (orderItem.type === ORDER_TYPES.TEST_ORDER) { return { grouping: getOrderGrouping(ORDER_TYPES.TEST_ORDER, orderItem.orderType.uuid), postDataPrepFn: prepTestOrderPostData, }; } else { return { grouping: getOrderGrouping(ORDER_TYPES.GENERAL_ORDER, orderItem.orderType.uuid), postDataPrepFn: prepOrderPostData, }; } }, [orderItem.type, orderItem.orderType.uuid]); const { orders, setOrders } = useOrderBasket(patient, grouping, postDataPrepFn); const alreadyInBasket = orders.some((x) => x.uuid === orderItem.uuid); const { mutate: globalMutate } = useSWRConfig(); const windowProps = useMemo(() => ({ encounterUuid: orderItem.encounter.uuid }), [orderItem.encounter.uuid]); const groupProps = useMemo( () => ({ patient, patientUuid: patient.id, visitContext: orderItem.encounter.visit, mutateVisitContext: invalidateVisitByUuid(globalMutate, orderItem.encounter.visit.uuid), }), [patient, orderItem.encounter.visit, globalMutate], ); const handleCancelOrder = useCallback(() => { if (orderItem.type === ORDER_TYPES.DRUG_ORDER) { getDrugOrderByUuid(orderItem.uuid) .then((res) => { const medicationOrder = res.data; const discontinueItem = buildMedicationOrder(medicationOrder, 'DISCONTINUE'); setOrders([...orders, discontinueItem]); launchWorkspace2('order-basket', {}, windowProps, groupProps); }) .catch((e) => { console.error('Error cancelling drug order: ', e); }); } else if (orderItem.type === ORDER_TYPES.TEST_ORDER) { const labItem = buildLabOrder(orderItem, 'DISCONTINUE'); setOrders([...orders, labItem]); launchWorkspace2('order-basket', {}, windowProps, groupProps); } else { const order = buildGeneralOrder(orderItem, 'DISCONTINUE'); setOrders([...orders, order]); launchWorkspace2('order-basket', {}, windowProps, groupProps); } }, [orderItem, setOrders, orders, windowProps, groupProps]); const handleModifyOrder = useCallback(() => { if (orderItem.type === ORDER_TYPES.DRUG_ORDER) { // make another call to fetch the order, // this time with custom rep to include the drug field // (when we fetch the array containing orderItem, we fetched for // all order types and including the drug field in the custom rep will // yield an error) getDrugOrderByUuid(orderItem.uuid) .then((res) => { const medicationOrder = res.data; launchWorkspace2( 'add-drug-order', { order: buildMedicationOrder(medicationOrder, 'REVISE'), orderToEditOrdererUuid: orderItem.orderer.uuid }, windowProps, groupProps, ); }) .catch((e) => { console.error('Error modifying drug order: ', e); }); } else if (orderItem.type === ORDER_TYPES.TEST_ORDER) { const labItem = buildLabOrder(orderItem, 'REVISE'); launchWorkspace2( 'add-lab-order', { order: labItem, orderTypeUuid: orderItem.orderType.uuid, orderToEditOrdererUuid: orderItem.orderer.uuid }, windowProps, groupProps, ); } else if (orderItem.type === ORDER_TYPES.GENERAL_ORDER) { const generalItem = buildGeneralOrder(orderItem, 'REVISE'); launchWorkspace2( 'orderable-concept-workspace', { order: generalItem, orderTypeUuid: orderItem.orderType.uuid, orderToEditOrdererUuid: orderItem.orderer.uuid }, windowProps, groupProps, ); } }, [orderItem, windowProps, groupProps]); const handleAddOrEditTestResults = useCallback(() => { launchWorkspace2('test-results-form-workspace', { order: orderItem, patient }); }, [orderItem, patient]); // No actions available for declined orders if (isDeclined) { return null; } return ( {orderItem?.type === ORDER_TYPES.TEST_ORDER && ( )} ); } export default OrderDetailsTable;