From 8526d83b8d7ab8ca2c2d1c8c1ed917ea72c4fe37 Mon Sep 17 00:00:00 2001 From: Vineet Sharma Date: Fri, 15 Nov 2024 09:00:45 +0530 Subject: [PATCH 01/19] Add config schema and create generic order basket tile --- .../src/orders/index.ts | 1 + .../src/orders/prepFuncs.ts | 19 ++ .../src/orders/useOrders.ts | 4 + .../src/config-schema.ts | 28 +++ .../order-types/generic-order-panel.scss | 0 .../generic-order-type.component.tsx | 197 ++++++++++++++++++ .../order-basket-item-tile.component.tsx | 100 +++++++++ .../order-types/order-basket-item-tile.scss | 84 ++++++++ .../src/order-basket/order-types/resources.ts | 2 + 9 files changed, 435 insertions(+) create mode 100644 packages/esm-patient-common-lib/src/orders/prepFuncs.ts create mode 100644 packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-panel.scss create mode 100644 packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-type.component.tsx create mode 100644 packages/esm-patient-orders-app/src/order-basket/order-types/order-basket-item-tile.component.tsx create mode 100644 packages/esm-patient-orders-app/src/order-basket/order-types/order-basket-item-tile.scss create mode 100644 packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts diff --git a/packages/esm-patient-common-lib/src/orders/index.ts b/packages/esm-patient-common-lib/src/orders/index.ts index e349281908..c461e0513c 100644 --- a/packages/esm-patient-common-lib/src/orders/index.ts +++ b/packages/esm-patient-common-lib/src/orders/index.ts @@ -2,3 +2,4 @@ export * from './useOrderBasket'; export * from './postOrders'; export * from './useOrders'; export * from './types'; +export * from './prepFuncs'; diff --git a/packages/esm-patient-common-lib/src/orders/prepFuncs.ts b/packages/esm-patient-common-lib/src/orders/prepFuncs.ts new file mode 100644 index 0000000000..58bb35b461 --- /dev/null +++ b/packages/esm-patient-common-lib/src/orders/prepFuncs.ts @@ -0,0 +1,19 @@ +import type { PostDataPrepFunction, OrderBasketItem, OrderPost } from './types'; + +export function prepOrderPostData(orderJavaClassName: string): PostDataPrepFunction { + if (!orderJavaClassName) { + return genericPostDataPrepFunction; + } + switch (orderJavaClassName) { + case 'org.openmrs.Order': + return genericPostDataPrepFunction; + } +} + +export const genericPostDataPrepFunction: PostDataPrepFunction = (order, patientUuid, encounterUuid) => { + return { + ...order, + patient: patientUuid, + encounter: encounterUuid, + }; +}; diff --git a/packages/esm-patient-common-lib/src/orders/useOrders.ts b/packages/esm-patient-common-lib/src/orders/useOrders.ts index f60abb7866..f257abee0e 100644 --- a/packages/esm-patient-common-lib/src/orders/useOrders.ts +++ b/packages/esm-patient-common-lib/src/orders/useOrders.ts @@ -71,6 +71,10 @@ export function useOrderTypes() { }; } +export function useOrderType(orderTypeUuid: string) { + return useSWR(`${restBaseUrl}/ordertype/${orderTypeUuid}`); +} + export function getDrugOrderByUuid(orderUuid: string) { return openmrsFetch(`${restBaseUrl}/order/${orderUuid}?v=${drugCustomRepresentation}`); } diff --git a/packages/esm-patient-orders-app/src/config-schema.ts b/packages/esm-patient-orders-app/src/config-schema.ts index e5f3305672..7cd22d9bde 100644 --- a/packages/esm-patient-orders-app/src/config-schema.ts +++ b/packages/esm-patient-orders-app/src/config-schema.ts @@ -1,4 +1,5 @@ import { Type } from '@openmrs/esm-framework'; +import _default from 'react-hook-form/dist/logic/appendErrors'; export const configSchema = { orderEncounterType: { @@ -12,9 +13,36 @@ export const configSchema = { 'Determines whether or not to display a Print button in the Orders details table. If set to true, a Print button gets shown in both the orders table headers. When clicked, this button enables the user to print out the contents of the table', _default: false, }, + orderTypes: { + _type: Type.Array, + _default: [], + _elements: { + orderTypeUuid: { + _type: Type.String, + _description: 'Order type UUID to be displayed on the order basket', + }, + conceptClass: { + _type: Type.String, + _description: 'Concept with the given class name will be ordered', + }, + orderableConcepts: { + _type: Type.Array, + _description: + 'UUIDs of concepts that represent orderable concepts. Either the `conceptClass` should be given, or the orderableConcepts', + _elements: { + _type: Type.UUID, + }, + }, + }, + }, }; export interface ConfigObject { orderEncounterType: string; showPrintButton: boolean; + orderTypes: Array<{ + orderTypeUuid: string; + conceptClass: string; + orderableConcepts: string; + }>; } diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-panel.scss b/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-panel.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-type.component.tsx b/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-type.component.tsx new file mode 100644 index 0000000000..d2ad46b6f5 --- /dev/null +++ b/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-type.component.tsx @@ -0,0 +1,197 @@ +import React, { type ComponentProps, useCallback, useEffect, useMemo, useState } from 'react'; +import { Button, Tile } from '@carbon/react'; +import classNames from 'classnames'; +import styles from './generic-order-panel.scss'; +import { AddIcon, ChevronDownIcon, ChevronUpIcon, closeWorkspace, useLayoutType } from '@openmrs/esm-framework'; +import { useTranslation } from 'react-i18next'; +import { + type DrugOrderBasketItem, + launchPatientWorkspace, + prepOrderPostData, + useOrderBasket, + useOrderType, +} from '@openmrs/esm-patient-common-lib'; +import OrderBasketItemTile from './order-basket-item-tile.component'; + +interface GenericOrderTypeProps { + orderTypeUuid: string; +} + +const GenericOrderType: React.FC = ({ orderTypeUuid }) => { + const { t } = useTranslation(); + const isTablet = useLayoutType() === 'tablet'; + const { data, isLoading, error } = useOrderType(orderTypeUuid); + const prepOrderPostFunc = useMemo(() => prepOrderPostData(orderTypeUuid), [orderTypeUuid]); + const { orders, setOrders } = useOrderBasket('medications', prepOrderPostFunc); + const [isExpanded, setIsExpanded] = useState(orders.length > 0); + const { + incompleteOrderBasketItems, + newOrderBasketItems, + renewedOrderBasketItems, + revisedOrderBasketItems, + discontinuedOrderBasketItems, + } = useMemo(() => { + const incompleteOrderBasketItems: Array = []; + const newOrderBasketItems: Array = []; + const renewedOrderBasketItems: Array = []; + const revisedOrderBasketItems: Array = []; + const discontinuedOrderBasketItems: Array = []; + + orders.forEach((order) => { + if (order?.isOrderIncomplete) { + incompleteOrderBasketItems.push(order); + } else if (order.action === 'NEW') { + newOrderBasketItems.push(order); + } else if (order.action === 'RENEW') { + renewedOrderBasketItems.push(order); + } else if (order.action === 'REVISE') { + revisedOrderBasketItems.push(order); + } else if (order.action === 'DISCONTINUE') { + discontinuedOrderBasketItems.push(order); + } + }); + + return { + incompleteOrderBasketItems, + newOrderBasketItems, + renewedOrderBasketItems, + revisedOrderBasketItems, + discontinuedOrderBasketItems, + }; + }, [orders]); + + const openConceptSearch = () => { + closeWorkspace('order-basket', { + ignoreChanges: true, + onWorkspaceClose: () => launchPatientWorkspace('add-drug-order'), + }); + }; + + const openOrderForm = (order: DrugOrderBasketItem) => { + closeWorkspace('order-basket', { + ignoreChanges: true, + onWorkspaceClose: () => launchPatientWorkspace('add-drug-order', { order }), + }); + }; + + const removeOrder = useCallback( + (order: DrugOrderBasketItem) => { + const newOrders = [...orders]; + newOrders.splice(orders.indexOf(order), 1); + setOrders(newOrders); + }, + [orders, setOrders], + ); + + useEffect(() => { + setIsExpanded(orders.length > 0); + }, [orders]); + + return ( + +
+
+ {/* */} + {/* Add Icon */} +

{`${t('drugOrders', 'Drug orders')} (${orders.length})`}

+
+
+ + +
+
+ {isExpanded && ( + <> + {incompleteOrderBasketItems.length > 0 && ( + <> + {incompleteOrderBasketItems.map((order, index) => ( + openOrderForm(order)} + onRemoveClick={() => removeOrder(order)} + /> + ))} + + )} + {newOrderBasketItems.length > 0 && ( + <> + {newOrderBasketItems.map((order, index) => ( + openOrderForm(order)} + onRemoveClick={() => removeOrder(order)} + /> + ))} + + )} + + {renewedOrderBasketItems.length > 0 && ( + <> + {renewedOrderBasketItems.map((item, index) => ( + openOrderForm(item)} + onRemoveClick={() => removeOrder(item)} + /> + ))} + + )} + + {revisedOrderBasketItems.length > 0 && ( + <> + {revisedOrderBasketItems.map((item, index) => ( + openOrderForm(item)} + onRemoveClick={() => removeOrder(item)} + /> + ))} + + )} + + {discontinuedOrderBasketItems.length > 0 && ( + <> + {discontinuedOrderBasketItems.map((item, index) => ( + openOrderForm(item)} + onRemoveClick={() => removeOrder(item)} + /> + ))} + + )} + + )} +
+ ); +}; + +export default GenericOrderType; diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/order-basket-item-tile.component.tsx b/packages/esm-patient-orders-app/src/order-basket/order-types/order-basket-item-tile.component.tsx new file mode 100644 index 0000000000..28f6f46413 --- /dev/null +++ b/packages/esm-patient-orders-app/src/order-basket/order-types/order-basket-item-tile.component.tsx @@ -0,0 +1,100 @@ +import { TrashCanIcon, useLayoutType, WarningIcon } from '@openmrs/esm-framework'; +import { type OrderBasketItem } from '@openmrs/esm-patient-common-lib'; +import React, { type ComponentProps, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import styles from './order-basket-item-tile.scss'; +import { Button } from '@carbon/react'; +import { Tile } from '@carbon/react'; +import { ClickableTile } from '@carbon/react'; +import classNames from 'classnames'; + +export interface OrderBasketItemTileProps { + orderBasketItem: OrderBasketItem; + onItemClick: () => void; + onRemoveClick: () => void; +} + +const OrderBasketItemTile: React.FC = ({ orderBasketItem, onItemClick, onRemoveClick }) => { + const { t } = useTranslation(); + const isTablet = useLayoutType() === 'tablet'; + + // This here is really dirty, but required. + // If the ref's value is false, we won't react to the ClickableTile's handleClick function. + // Why is this necessary? + // The "Remove" button is nested inside the ClickableTile. If the button's clicked, the tile also raises the + // handleClick event later. Not sure if this is a bug, but this shouldn't be possible in our flows. + // Hence, we manually prevent the handleClick callback from being invoked as soon as the button is pressed once. + const shouldOnClickBeCalled = useRef(true); + + const labTile = ( +
+
+ +
+ {/* {orderBasketItem.testType?.label} */} + + {!!orderBasketItem.orderError && ( + <> +
+ + +   + {t('error', 'Error').toUpperCase()}   + {orderBasketItem.orderError.responseBody?.error?.message ?? orderBasketItem.orderError.message} + + + )} +
+
+
+ ); + return orderBasketItem.action === 'DISCONTINUE' ? ( + {labTile} + ) : ( + shouldOnClickBeCalled.current && onItemClick()} + > + {labTile} + + ); +}; + +function OrderActionLabel({ orderBasketItem }: { orderBasketItem: OrderBasketItem }) { + const { t } = useTranslation(); + + if (orderBasketItem.isOrderIncomplete) { + return {t('orderActionIncomplete', 'Incomplete')}; + } + + switch (orderBasketItem.action) { + case 'NEW': + return {t('orderActionNew', 'New')}; + case 'RENEW': + return {t('orderActionRenew', 'Renew')}; + case 'REVISE': + return {t('orderActionRevise', 'Modify')}; + case 'DISCONTINUE': + return {t('orderActionDiscontinue', 'Discontinue')}; + default: + return <>; + } +} + +export default OrderBasketItemTile; diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/order-basket-item-tile.scss b/packages/esm-patient-orders-app/src/order-basket/order-types/order-basket-item-tile.scss new file mode 100644 index 0000000000..2fc8dde661 --- /dev/null +++ b/packages/esm-patient-orders-app/src/order-basket/order-types/order-basket-item-tile.scss @@ -0,0 +1,84 @@ +@use '@carbon/layout'; +@use '@carbon/type'; +@use '@openmrs/esm-styleguide/src/vars' as *; + +.clickableTileDesktop { + padding: layout.$spacing-03 layout.$spacing-04; + border-bottom: 1px solid $ui-03; +} + +.clickableTileTablet { + padding: layout.$spacing-04 layout.$spacing-05; + background-color: $ui-02; + border-bottom: 1px solid $ui-03; +} + +.orderBasketItemTile { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.label { + display: inline-block; + @include type.type-style('label-01'); + margin-bottom: layout.$spacing-03; +} + +.orderActionNewLabel { + @extend .label; + color: black; + background-color: #c4f4cc; + padding: 0 layout.$spacing-02; +} + +.orderActionIncompleteLabel { + @extend .label; + color: white; + background-color: $danger; + padding: 0 layout.$spacing-02; +} + +.orderActionRenewLabel { + @extend .label; + color: $support-02; +} + +.orderActionRevisedLabel { + @extend .label; + color: #943d00; +} + +.orderActionDiscontinueLabel { + @extend .label; + color: $danger; +} + +.orderErrorText { + color: $danger; + display: flex; + align-items: center; +} + +.name { + @include type.type-style('heading-compact-01'); + color: black; +} + +.removeButton { + svg { + fill: $danger !important; + } +} + +.label01 { + @include type.type-style('label-01'); +} + +.clipTextWithEllipsis { + font-size: layout.$spacing-05; + line-height: 1.5; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts b/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts new file mode 100644 index 0000000000..74afa6fb35 --- /dev/null +++ b/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts @@ -0,0 +1,2 @@ +import { restBaseUrl } from '@openmrs/esm-framework'; +import useSWR from 'swr'; From 96b5652b02fb8ac53850f14097f852dd67c16da9 Mon Sep 17 00:00:00 2001 From: Vineet Sharma Date: Fri, 15 Nov 2024 09:01:07 +0530 Subject: [PATCH 02/19] Update translations --- packages/esm-patient-orders-app/translations/en.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/esm-patient-orders-app/translations/en.json b/packages/esm-patient-orders-app/translations/en.json index fccec1197c..468f5731f7 100644 --- a/packages/esm-patient-orders-app/translations/en.json +++ b/packages/esm-patient-orders-app/translations/en.json @@ -17,6 +17,7 @@ "discontinued": "Discontinued", "dosage": "Dosage", "dose": "Dose", + "drugOrders": "Drug orders", "editResults": "Edit results", "endDate": "End date", "enterTestResults": "Enter test results", @@ -36,6 +37,11 @@ "normalRange": "Normal range", "onDate": "on", "order": "Order", + "orderActionDiscontinue": "Discontinue", + "orderActionIncomplete": "Incomplete", + "orderActionNew": "New", + "orderActionRenew": "Renew", + "orderActionRevise": "Modify", "orderBasket": "Order basket", "orderBasketWorkspaceTitle": "Order basket", "orderCancellation": "Order cancellation", @@ -59,6 +65,7 @@ "reasonForCancellation": "Reason for cancellation", "reasonForCancellationRequired": "Reason for cancellation is required", "refills": "Refills", + "removeFromBasket": "Remove from basket", "result": "Result", "saveAndClose": "Save and close", "saveDrugOrderFailed": "Error ordering {{orderName}}", From 060de6e7a71d2e3239623c5794780d4cd2e58e2a Mon Sep 17 00:00:00 2001 From: Vineet Sharma Date: Tue, 19 Nov 2024 17:42:45 +0530 Subject: [PATCH 03/19] Searching orderable concepts working --- .../src/orders/useOrders.ts | 15 +- .../src/config-schema.ts | 11 +- packages/esm-patient-orders-app/src/index.ts | 6 + .../order-basket/order-basket.workspace.tsx | 10 + .../order-types/generic-order-panel.scss | 81 ++++++ .../generic-order-type.component.tsx | 18 +- .../orderable-concept-search.scss | 21 ++ .../orderable-concept-search.workspace.tsx | 193 ++++++++++++++ .../search-results.component.tsx | 250 ++++++++++++++++++ .../search-results.scss | 152 +++++++++++ .../src/order-basket/order-types/resources.ts | 90 ++++++- .../esm-patient-orders-app/src/routes.json | 6 + 12 files changed, 842 insertions(+), 11 deletions(-) create mode 100644 packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/orderable-concept-search.scss create mode 100644 packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/orderable-concept-search.workspace.tsx create mode 100644 packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/search-results.component.tsx create mode 100644 packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/search-results.scss diff --git a/packages/esm-patient-common-lib/src/orders/useOrders.ts b/packages/esm-patient-common-lib/src/orders/useOrders.ts index f257abee0e..48ead49c25 100644 --- a/packages/esm-patient-common-lib/src/orders/useOrders.ts +++ b/packages/esm-patient-common-lib/src/orders/useOrders.ts @@ -71,8 +71,21 @@ export function useOrderTypes() { }; } +interface OrderTypeResponse { + uuid: string; + display: string; + name: string; + javaClassName: 'org.openmrs.Order'; + retired: false; + description: string; + conceptClasses: Array<{ + uuid: string; + display: string; + }>; +} + export function useOrderType(orderTypeUuid: string) { - return useSWR(`${restBaseUrl}/ordertype/${orderTypeUuid}`); + return useSWR>(`${restBaseUrl}/ordertype/${orderTypeUuid}`); } export function getDrugOrderByUuid(orderUuid: string) { diff --git a/packages/esm-patient-orders-app/src/config-schema.ts b/packages/esm-patient-orders-app/src/config-schema.ts index 7cd22d9bde..7ddc4494d7 100644 --- a/packages/esm-patient-orders-app/src/config-schema.ts +++ b/packages/esm-patient-orders-app/src/config-schema.ts @@ -1,5 +1,4 @@ import { Type } from '@openmrs/esm-framework'; -import _default from 'react-hook-form/dist/logic/appendErrors'; export const configSchema = { orderEncounterType: { @@ -15,7 +14,13 @@ export const configSchema = { }, orderTypes: { _type: Type.Array, - _default: [], + _default: [ + { + orderTypeUuid: '425ae793-e776-4f84-8be1-2f322744644d', + conceptClass: '', + orderableConcepts: ['06393843-1790-43cd-acba-cd497300c734'], + }, + ], _elements: { orderTypeUuid: { _type: Type.String, @@ -43,6 +48,6 @@ export interface ConfigObject { orderTypes: Array<{ orderTypeUuid: string; conceptClass: string; - orderableConcepts: string; + orderableConcepts: Array; }>; } diff --git a/packages/esm-patient-orders-app/src/index.ts b/packages/esm-patient-orders-app/src/index.ts index c28d16d4cf..ff00c5ba64 100644 --- a/packages/esm-patient-orders-app/src/index.ts +++ b/packages/esm-patient-orders-app/src/index.ts @@ -46,3 +46,9 @@ export const ordersDashboardLink = ); export const ordersDashboard = getSyncLifecycle(OrdersSummary, options); + +// t('searchConcepts','Search concepts') +export const orderableConceptSearch = getAsyncLifecycle( + () => import('./order-basket/order-types/orderable-concept-search/orderable-concept-search.workspace'), + options, +); diff --git a/packages/esm-patient-orders-app/src/order-basket/order-basket.workspace.tsx b/packages/esm-patient-orders-app/src/order-basket/order-basket.workspace.tsx index 2f65c41d8b..c025022617 100644 --- a/packages/esm-patient-orders-app/src/order-basket/order-basket.workspace.tsx +++ b/packages/esm-patient-orders-app/src/order-basket/order-basket.workspace.tsx @@ -14,6 +14,7 @@ import { import { type ConfigObject } from '../config-schema'; import { useMutatePatientOrders, useOrderEncounter } from '../api/api'; import styles from './order-basket.scss'; +import GenericOrderType from './order-types/generic-order-type.component'; const OrderBasket: React.FC = ({ patientUuid, @@ -119,6 +120,15 @@ const OrderBasket: React.FC = ({ })} name="order-basket-slot" /> + {config?.orderTypes?.length && + config?.orderTypes?.map((orderType) => ( + + ))}
diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-panel.scss b/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-panel.scss index e69de29bb2..b2b4849bde 100644 --- a/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-panel.scss +++ b/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-panel.scss @@ -0,0 +1,81 @@ +@use '@carbon/colors'; +@use '@carbon/layout'; +@use '@carbon/type'; +@use '@openmrs/esm-styleguide/src/vars' as *; + +.desktopTile { + border-left: layout.$spacing-02 solid colors.$cyan-20; + background-color: $ui-02; + border-top: 1px solid colors.$cyan-20; + border-right: none; + padding: 0; +} + +.tabletTile { + @extend .desktopTile; + border-top: 1px solid #3778c1; +} + +.collapsedTile { + border-bottom: none; + min-height: 0; + + .orderBasketHeader { + margin-bottom: 0; + } +} + +.tabletTile .collapsedTile { + border-bottom: 1px solid $grey-2; +} + +.container { + background-color: $ui-02; + display: flex; + justify-content: space-between; + align-items: center; + text-align: left; +} + +.desktopTile .container { + h4 { + @include type.type-style('heading-compact-02'); + color: $text-02; + } +} + +.tabletTile .container { + padding: layout.$spacing-03; + + h4 { + @include type.type-style('heading-03'); + color: $ui-05; + } +} + +.heading { + margin-left: layout.$spacing-03; +} + +.iconAndLabel { + display: flex; + align-items: center; + margin: layout.$spacing-03; +} + +.buttonContainer { + display: flex; + align-items: center; +} + +.addButton { + svg { + fill: currentColor !important; + } +} + +.chevron { + svg { + fill: black; + } +} diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-type.component.tsx b/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-type.component.tsx index d2ad46b6f5..6aa39b8be5 100644 --- a/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-type.component.tsx +++ b/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-type.component.tsx @@ -15,14 +15,17 @@ import OrderBasketItemTile from './order-basket-item-tile.component'; interface GenericOrderTypeProps { orderTypeUuid: string; + conceptClass: string; + orderableConcepts: Array; } -const GenericOrderType: React.FC = ({ orderTypeUuid }) => { +const GenericOrderType: React.FC = ({ orderTypeUuid, conceptClass, orderableConcepts }) => { const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; const { data, isLoading, error } = useOrderType(orderTypeUuid); + const orderType = data?.data; const prepOrderPostFunc = useMemo(() => prepOrderPostData(orderTypeUuid), [orderTypeUuid]); - const { orders, setOrders } = useOrderBasket('medications', prepOrderPostFunc); + const { orders, setOrders } = useOrderBasket(orderTypeUuid, prepOrderPostFunc); const [isExpanded, setIsExpanded] = useState(orders.length > 0); const { incompleteOrderBasketItems, @@ -63,7 +66,12 @@ const GenericOrderType: React.FC = ({ orderTypeUuid }) => const openConceptSearch = () => { closeWorkspace('order-basket', { ignoreChanges: true, - onWorkspaceClose: () => launchPatientWorkspace('add-drug-order'), + onWorkspaceClose: () => + launchPatientWorkspace('orderable-concept-workspace', { + orderTypeUuid, + conceptClass, + orderableConcepts, + }), }); }; @@ -94,8 +102,8 @@ const GenericOrderType: React.FC = ({ orderTypeUuid }) =>
{/* */} - {/* Add Icon */} -

{`${t('drugOrders', 'Drug orders')} (${orders.length})`}

+ {/* TODO: Add Icon */} +

{`${orderType?.display} (${orders.length})`}

+
+ )} + + + ); +}; + +interface ConceptSearchProps { + closeWorkspace: DefaultWorkspaceProps['closeWorkspace']; + openOrderForm: (search: OrderBasketItem) => void; + orderTypeUuid: string; + conceptClass: string; + orderableConcepts: Array; +} + +function ConceptSearch({ + closeWorkspace, + orderTypeUuid, + openOrderForm, + conceptClass, + orderableConcepts, +}: ConceptSearchProps) { + const { t } = useTranslation(); + const isTablet = useLayoutType() === 'tablet'; + const [searchTerm, setSearchTerm] = useState(''); + // const { debounceDelayInMs } = useConfig(); + const debouncedSearchTerm = useDebounce(searchTerm); + const searchInputRef = useRef(null); + + const cancelDrugOrder = useCallback(() => { + closeWorkspace({ + onWorkspaceClose: () => launchPatientWorkspace('order-basket'), + }); + }, [closeWorkspace]); + + const focusAndClearSearchInput = () => { + setSearchTerm(''); + searchInputRef.current?.focus(); + }; + + const handleSearchTermChange = (event: React.ChangeEvent) => + setSearchTerm(event.target.value ?? ''); + + return ( +
+ + + + {}} + conceptClass={conceptClass} + orderableConcepts={orderableConcepts} + /> + {isTablet && ( +
+

{t('or', 'or')}

+ +
+ )} +
+ ); +} + +export default OrderableConceptSearchWorkspace; diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/search-results.component.tsx b/packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/search-results.component.tsx new file mode 100644 index 0000000000..9149d9d990 --- /dev/null +++ b/packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/search-results.component.tsx @@ -0,0 +1,250 @@ +import React, { type ComponentProps, useCallback } from 'react'; +import { launchPatientWorkspace, type OrderBasketItem } from '@openmrs/esm-patient-common-lib'; +import { useTranslation } from 'react-i18next'; +import { + ArrowRightIcon, + type DefaultWorkspaceProps, + ShoppingCartArrowDownIcon, + useLayoutType, + useSession, +} from '@openmrs/esm-framework'; +import { ShoppingCartArrowUp } from '@carbon/react/icons'; +import { useMemo } from 'react'; +import classNames from 'classnames'; +import { Tile } from '@carbon/react'; +import { Button } from '@carbon/react'; +import { SkeletonText } from '@carbon/react'; +import { ButtonSkeleton } from '@carbon/react'; +import styles from './search-results.scss'; +import { type ConceptType, useGenericOrderBasket, useOrderableConcepts } from '../resources'; + +interface OrderableConceptSearchResultsProps { + searchTerm: string; + openOrderForm: (order: OrderBasketItem) => void; + focusAndClearSearchInput: () => void; + cancelOrder: () => void; + conceptClass: string; + orderableConcepts: Array; + orderTypeUuid: string; + closeWorkspace: DefaultWorkspaceProps['closeWorkspace']; +} + +const OrderableConceptSearchResults: React.FC = ({ + searchTerm, + openOrderForm, + focusAndClearSearchInput, + cancelOrder, + conceptClass, + orderableConcepts, + orderTypeUuid, + closeWorkspace, +}) => { + const { t } = useTranslation(); + const isTablet = useLayoutType() === 'tablet'; + const { concepts, isLoading, error } = useOrderableConcepts(conceptClass, orderableConcepts); + + const filteredTestTypes = useMemo(() => { + if (!searchTerm) { + return concepts; + } + + if (searchTerm && searchTerm.trim() !== '') { + return concepts?.filter((testType) => + testType.synonyms.some((name) => name.toLowerCase().includes(searchTerm.toLowerCase())), + ); + } + }, [searchTerm, concepts]); + + if (isLoading) { + return ; + } + + if (error) { + return ( + +
+

+ {t('errorFetchingTestTypes', 'Error fetching results for "{{searchTerm}}"', { + searchTerm, + })} +

+

+ {t('trySearchingAgain', 'Please try searching again')} +

+
+
+ ); + } + + if (filteredTestTypes?.length) { + return ( + <> +
+ {searchTerm && ( +
+ + {t('searchResultsMatchesForTerm', '{{count}} results for "{{searchTerm}}"', { + count: filteredTestTypes?.length, + searchTerm, + })} + + +
+ )} +
+ {filteredTestTypes.map((testType) => ( + + ))} +
+
+ {isTablet && ( +
+

{t('or', 'or')}

+ +
+ )} + + ); + } + + return ( + +
+

+ {t('noResultsForTestTypeSearch', 'No results to display for "{{searchTerm}}"', { + searchTerm, + })} +

+

+ {t('tryTo', 'Try to')}{' '} + + {t('searchAgain', 'search again')} + {' '} + {t('usingADifferentTerm', 'using a different term')} +

+
+
+ ); +}; + +const TestTypeSearchSkeleton = () => { + const isTablet = useLayoutType() === 'tablet'; + const tileClassName = classNames({ + [styles.tabletSearchResultTile]: isTablet, + [styles.desktopSearchResultTile]: !isTablet, + [styles.skeletonTile]: true, + }); + const buttonSize = isTablet ? 'md' : 'sm'; + + return ( +
+
+ + +
+ {[...Array(4)].map((_, index) => ( + + + + ))} +
+ ); +}; + +interface TestTypeSearchResultItemProps { + testType: ConceptType; + openOrderForm: (searchResult: OrderBasketItem) => void; + orderTypeUuid: string; + closeWorkspace: DefaultWorkspaceProps['closeWorkspace']; +} + +const TestTypeSearchResultItem: React.FC = ({ + testType, + openOrderForm, + orderTypeUuid, + closeWorkspace, +}) => { + const { t } = useTranslation(); + const isTablet = useLayoutType() === 'tablet'; + const session = useSession(); + const { orders, setOrders } = useGenericOrderBasket(orderTypeUuid); + + // const testTypeAlreadyInBasket = useMemo( + // () => orders?.some((order) => order.testType.conceptUuid === testType.conceptUuid), + // [orders, testType], + // ); + + // const createLabOrder = useCallback( + // (testType: TestType) => { + // return createEmptyLabOrder(testType, session.currentProvider?.uuid); + // }, + // [session.currentProvider.uuid], + // ); + + // const addToBasket = useCallback(() => { + // const labOrder = createLabOrder(testType); + // labOrder.isOrderIncomplete = true; + // setOrders([...orders, labOrder]); + // closeWorkspace({ + // ignoreChanges: true, + // onWorkspaceClose: () => launchPatientWorkspace('order-basket'), + // }); + // }, [orders, setOrders, createLabOrder, testType, closeWorkspace]); + + // const removeFromBasket = useCallback(() => { + // setOrders(orders.filter((order) => order.testType.conceptUuid !== testType.conceptUuid)); + // }, [orders, setOrders, testType.conceptUuid]); + + return ( + +
+

+ {testType.label}{' '} +

+
+
+ {/* {testTypeAlreadyInBasket ? ( + + ) : ( + + )} */} + +
+
+ ); +}; + +export default OrderableConceptSearchResults; diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/search-results.scss b/packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/search-results.scss new file mode 100644 index 0000000000..358d5c8998 --- /dev/null +++ b/packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/search-results.scss @@ -0,0 +1,152 @@ +@use '@carbon/colors'; +@use '@carbon/layout'; +@use '@carbon/type'; +@use '@openmrs/esm-styleguide/src/vars' as *; + +/** For TestTypeSearchResults */ +.container { + margin: layout.$spacing-03 layout.$spacing-05 0; +} + +.separator { + @include type.type-style('body-01'); + color: colors.$gray-90; + width: 12rem; + margin: layout.$spacing-05 auto layout.$spacing-03; + overflow: hidden; + text-align: center; + + &::before, + &::after { + background-color: colors.$gray-40; + content: ''; + display: inline-block; + height: 1px; + position: relative; + vertical-align: middle; + width: 50%; + } + + &::before { + right: layout.$spacing-05; + margin-left: -50%; + } + + &::after { + left: layout.$spacing-05; + margin-right: -50%; + } +} +.separatorContainer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-top: layout.$spacing-05; +} + +.searchResultsCount { + @include type.type-style('body-compact-01'); + color: $text-02; +} + +.orderBasketSearchResultsHeader { + display: flex; + justify-content: space-between; + margin-bottom: layout.$spacing-03; + align-items: center; +} + +/** For TestTypeSearchResultItem */ +.searchResultTile { + padding: 0; + display: flex; + flex-direction: column; + justify-content: space-between; + border: 1px solid $grey-2; + &:not(:last-of-type) { + margin-bottom: layout.$spacing-03; + } +} + +.tabletSearchResultTile { + &:not(:last-of-type) { + margin-bottom: layout.$spacing-04; + } +} + +.searchResultTileContent { + padding: layout.$spacing-03 layout.$spacing-05; +} + +.searchResultActions { + border-top: 1px solid $grey-2; + padding: 0 layout.$spacing-05; + display: flex; + justify-content: flex-end; + + svg { + fill: currentColor !important; + } +} + +.searchResultSkeletonWrapper { + margin: layout.$spacing-03 layout.$spacing-05 layout.$spacing-03; + + :global(.cds--skeleton__text) { + margin: 0; + } + + .searchResultCntSkeleton { + margin-right: layout.$spacing-07; + } + + .skeletonTile { + display: flex; + align-items: center; + } +} + +.emptyState { + margin: layout.$spacing-05; + display: flex; + justify-content: center; + align-items: center; + padding: layout.$spacing-07; + border: 1px solid $ui-03; + text-align: center; + background-color: $ui-01; +} + +:global(.omrs-breakpoint-lt-small-desktop) .emptyState { + background-color: $ui-02; +} + +.link { + color: $interactive-01; + text-decoration: underline; + cursor: pointer; +} + +.resultsContainer::-webkit-scrollbar { + width: layout.$spacing-03; +} + +.resultsContainer::-webkit-scrollbar-thumb { + background: $ui-04; + border-radius: layout.$spacing-02; +} + +.heading { + @include type.type-style('heading-01'); + color: colors.$gray-70; + margin-bottom: layout.$spacing-03; +} + +.bodyShort01 { + @include type.type-style('body-compact-01'); +} + +.text02 { + color: $text-02; +} diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts b/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts index 74afa6fb35..dce7582935 100644 --- a/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts +++ b/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts @@ -1,2 +1,88 @@ -import { restBaseUrl } from '@openmrs/esm-framework'; -import useSWR from 'swr'; +import { type Concept, type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; +import { type OrderBasketItem, prepOrderPostData, useOrderBasket } from '@openmrs/esm-patient-common-lib'; +import { useEffect, useMemo } from 'react'; +import useSWRImmutable from 'swr/immutable'; + +type ConceptResult = FetchResponse; +type ConceptResults = FetchResponse<{ results: Array }>; + +export function useGenericOrderBasket(orderTypeUuid: string) { + const prepOrderPostFunc = useMemo(() => prepOrderPostData(orderTypeUuid), [orderTypeUuid]); + return useOrderBasket(orderTypeUuid, prepOrderPostFunc); +} + +function openmrsFetchMultiple(urls: Array) { + // SWR has an RFC for `useSWRList`: + // https://github.com/vercel/swr/discussions/1988 + // If that ever is implemented we should switch to using that. + return Promise.all(urls.map((url) => openmrsFetch<{ results: Array }>(url))); +} + +function useOrderableConceptSWR(conceptClass: string, orderableConcepts?: Array) { + const { data, isLoading, error } = useSWRImmutable( + () => + orderableConcepts?.length + ? orderableConcepts.map( + (c) => + `${restBaseUrl}/concept/${c}?v=custom:(display,names:(display),uuid,setMembers:(display,uuid,names:(display),setMembers:(display,uuid,names:(display))))`, + ) + : `${restBaseUrl}/concept?class=${conceptClass}&v=custom:(display,names:(display),uuid,setMembers:(display,uuid,names:(display),setMembers:(display,uuid,names:(display))))`, + (orderableConcepts ? openmrsFetchMultiple : openmrsFetch) as any, + { + shouldRetryOnError(err) { + return err instanceof Response; + }, + }, + ); + + const results = useMemo(() => { + if (isLoading || error) return null; + return orderableConcepts + ? (data as Array)?.flatMap((d) => d.data.setMembers) + : (data as ConceptResults)?.data.results ?? ([] as Concept[]); + }, [data, isLoading, error, orderableConcepts]); + + return { + data: results, + isLoading, + error, + }; +} + +export interface ConceptType { + label: string; + conceptUuid: string; + synonyms: Array; +} + +export function useOrderableConcepts(conceptClass: string, orderableConcepts: Array) { + const { data, isLoading, error } = useOrderableConceptSWR( + conceptClass, + orderableConcepts.length ? orderableConcepts : null, + ); + + useEffect(() => { + if (error) { + reportError(error); + } + }, [error]); + + const concepts = useMemo( + () => + data + ?.map((concept) => ({ + label: concept.display, + conceptUuid: concept.uuid, + synonyms: concept.names?.flatMap((name) => name.display) ?? [], + })) + ?.sort((testConcept1, testConcept2) => testConcept1.label.localeCompare(testConcept2.label)) + ?.filter((item, pos, array) => !pos || array[pos - 1].conceptUuid !== item.conceptUuid), + [data], + ); + + return { + concepts, + isLoading: isLoading, + error: error, + }; +} diff --git a/packages/esm-patient-orders-app/src/routes.json b/packages/esm-patient-orders-app/src/routes.json index 6e29ba9674..8c5c1c1145 100644 --- a/packages/esm-patient-orders-app/src/routes.json +++ b/packages/esm-patient-orders-app/src/routes.json @@ -51,6 +51,12 @@ "component": "testResultsFormWorkspace", "type": "lab-results", "canHide": false + }, + { + "name": "orderable-concept-workspace", + "title": "searchConcepts", + "component": "orderableConceptSearch", + "type": "order" } ] } From be94ad7c61bbcdbda1c16030cc014656eac83e99 Mon Sep 17 00:00:00 2001 From: Vineet Sharma Date: Wed, 20 Nov 2024 11:34:43 +0530 Subject: [PATCH 04/19] Display concepts in the search order workspace --- .../src/orders/types/drug-order.ts} | 35 ++++++++++- .../src/orders/types/index.ts | 9 +++ .../src/orders/{types.ts => types/order.ts} | 51 ++-------------- .../src/orders/types/test-order.ts | 13 ++++ .../src/orders/useOrders.ts | 4 +- .../add-drug-order.workspace.tsx | 2 +- .../drug-order-form.component.tsx | 20 +++--- .../drug-search/drug-search.component.tsx | 3 +- .../drug-search/drug-search.resource.tsx | 3 +- .../src/add-drug-order/drug-search/helpers.ts | 2 +- .../order-basket-search-results.component.tsx | 8 ++- .../src/api/api.ts | 7 ++- .../src/api/order-config.ts | 2 +- .../medications-details-table.component.tsx | 2 +- .../drug-order-basket-panel.extension.tsx | 3 +- .../drug-order-basket-panel.test.tsx | 2 +- .../order-basket-item-tile.component.tsx | 2 +- .../src/config-schema.ts | 21 ++++--- .../order-basket/order-basket.workspace.tsx | 2 +- .../generic-order-type.component.tsx | 21 +++++-- .../orderable-concept-search.workspace.tsx | 32 ++++++---- .../search-results.component.tsx | 43 +++++-------- .../src/order-basket/order-types/resources.ts | 61 ++++++++++++++++--- .../translations/en.json | 18 +++++- 24 files changed, 225 insertions(+), 141 deletions(-) rename packages/{esm-patient-medications-app/src/types.ts => esm-patient-common-lib/src/orders/types/drug-order.ts} (68%) create mode 100644 packages/esm-patient-common-lib/src/orders/types/index.ts rename packages/esm-patient-common-lib/src/orders/{types.ts => types/order.ts} (78%) create mode 100644 packages/esm-patient-common-lib/src/orders/types/test-order.ts diff --git a/packages/esm-patient-medications-app/src/types.ts b/packages/esm-patient-common-lib/src/orders/types/drug-order.ts similarity index 68% rename from packages/esm-patient-medications-app/src/types.ts rename to packages/esm-patient-common-lib/src/orders/types/drug-order.ts index 3a8713d0fd..ec222ce16e 100644 --- a/packages/esm-patient-medications-app/src/types.ts +++ b/packages/esm-patient-common-lib/src/orders/types/drug-order.ts @@ -1,4 +1,13 @@ -import { type Drug, type OrderBasketItem } from '@openmrs/esm-patient-common-lib'; +import type { OpenmrsResource } from '@openmrs/esm-framework'; +import type { OrderBasketItem } from './order'; + +export interface Drug { + uuid: string; + strength: string; + concept: OpenmrsResource; + dosageForm: OpenmrsResource; + display: string; +} export interface DrugOrderBasketItem extends OrderBasketItem { drug: Drug; @@ -74,3 +83,27 @@ export interface CommonMedicationValueCoded extends CommonMedicationProps { valueCoded: string; names?: string[]; } + +export interface DrugOrderBasketItem extends OrderBasketItem { + drug: Drug; + unit: DosingUnit; + commonMedicationName: string; + dosage: number; + frequency: MedicationFrequency; + route: MedicationRoute; + quantityUnits: QuantityUnit; + patientInstructions: string; + asNeeded: boolean; + asNeededCondition: string; + // TODO: This is unused + startDate: Date | string; + durationUnit: DurationUnit; + duration: number | null; + pillsDispensed: number; + numRefills: number; + indication: string; + isFreeTextDosage: boolean; + freeTextDosage: string; + previousOrder?: string; + template?: OrderTemplate; +} diff --git a/packages/esm-patient-common-lib/src/orders/types/index.ts b/packages/esm-patient-common-lib/src/orders/types/index.ts new file mode 100644 index 0000000000..db8287d80e --- /dev/null +++ b/packages/esm-patient-common-lib/src/orders/types/index.ts @@ -0,0 +1,9 @@ +import type { DrugOrderBasketItem } from './drug-order'; +import type { OrderBasketItem } from './order'; +import type { LabOrderBasketItem } from './test-order'; + +export * from './order'; +export * from './drug-order'; +export * from './test-order'; + +export type GenericOrderBasketItem = OrderBasketItem | DrugOrderBasketItem | LabOrderBasketItem; diff --git a/packages/esm-patient-common-lib/src/orders/types.ts b/packages/esm-patient-common-lib/src/orders/types/order.ts similarity index 78% rename from packages/esm-patient-common-lib/src/orders/types.ts rename to packages/esm-patient-common-lib/src/orders/types/order.ts index 85ffabbf83..c526a28294 100644 --- a/packages/esm-patient-common-lib/src/orders/types.ts +++ b/packages/esm-patient-common-lib/src/orders/types/order.ts @@ -1,4 +1,5 @@ -import { type OpenmrsResource } from '@openmrs/esm-framework'; +import type { OpenmrsResource } from '@openmrs/esm-framework'; +import type { Drug } from './drug-order'; export type OrderAction = 'NEW' | 'REVISE' | 'DISCONTINUE' | 'RENEW'; @@ -131,7 +132,7 @@ export interface Order { quantityUnits: OpenmrsResource; route: OpenmrsResource; scheduleDate: null; - urgency: 'ROUTINE' | 'STAT' | 'ON_SCHEDULED_DATE'; + urgency: OrderUrgency; // additional properties accessionNumber: string; @@ -167,54 +168,10 @@ export interface OrderType { description: string; } -export interface Drug { - uuid: string; - strength: string; - concept: OpenmrsResource; - dosageForm: OpenmrsResource; - display: string; -} +export type FulfillerStatus = 'EXCEPTION' | 'RECEIVED' | 'COMPLETED' | 'IN_PROGRESS' | 'ON_HOLD' | 'DECLINED'; export type PostDataPrepFunction = ( order: OrderBasketItem, patientUuid: string, encounterUuid: string | null, ) => OrderPost; - -// Adopted from @openmrs/esm-patient-medications-app package. We should consider maintaining a single shared types file -export interface DrugOrderBasketItem extends OrderBasketItem { - drug: Drug; - unit: any; - commonMedicationName: string; - dosage: number; - frequency: any; - route: any; - quantityUnits: any; - patientInstructions: string; - asNeeded: boolean; - asNeededCondition: string; - startDate: Date | string; - durationUnit: any; - duration: number | null; - pillsDispensed: number; - numRefills: number; - indication: string; - isFreeTextDosage: boolean; - freeTextDosage: string; - previousOrder?: string; - template?: any; -} - -export interface LabOrderBasketItem extends OrderBasketItem { - testType?: { - label: string; - conceptUuid: string; - }; - urgency?: OrderUrgency; - instructions?: string; - previousOrder?: string; - orderReason?: string; - orderNumber?: string; -} - -export type FulfillerStatus = 'EXCEPTION' | 'RECEIVED' | 'COMPLETED' | 'IN_PROGRESS' | 'ON_HOLD' | 'DECLINED'; diff --git a/packages/esm-patient-common-lib/src/orders/types/test-order.ts b/packages/esm-patient-common-lib/src/orders/types/test-order.ts new file mode 100644 index 0000000000..0ae855169c --- /dev/null +++ b/packages/esm-patient-common-lib/src/orders/types/test-order.ts @@ -0,0 +1,13 @@ +import type { OrderBasketItem, OrderUrgency } from './order'; + +export interface LabOrderBasketItem extends OrderBasketItem { + testType?: { + label: string; + conceptUuid: string; + }; + urgency?: OrderUrgency; + instructions?: string; + previousOrder?: string; + orderReason?: string; + orderNumber?: string; +} diff --git a/packages/esm-patient-common-lib/src/orders/useOrders.ts b/packages/esm-patient-common-lib/src/orders/useOrders.ts index 48ead49c25..b6fd7eecf0 100644 --- a/packages/esm-patient-common-lib/src/orders/useOrders.ts +++ b/packages/esm-patient-common-lib/src/orders/useOrders.ts @@ -71,11 +71,13 @@ export function useOrderTypes() { }; } +export type OrderTypeJavaClassName = 'org.openmrs.Order' | 'org.openmrs.TestOrder' | 'org.openmrs.DrugOrder'; + interface OrderTypeResponse { uuid: string; display: string; name: string; - javaClassName: 'org.openmrs.Order'; + javaClassName: OrderTypeJavaClassName; retired: false; description: string; conceptClasses: Array<{ diff --git a/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.workspace.tsx b/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.workspace.tsx index 9b31dc15e4..7544c0f937 100644 --- a/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.workspace.tsx +++ b/packages/esm-patient-medications-app/src/add-drug-order/add-drug-order.workspace.tsx @@ -6,10 +6,10 @@ import { type DefaultPatientWorkspaceProps, launchPatientWorkspace, useOrderBasket, + type DrugOrderBasketItem, } from '@openmrs/esm-patient-common-lib'; import { careSettingUuid, prepMedicationOrderPostData } from '../api/api'; import { ordersEqual } from './drug-search/helpers'; -import type { DrugOrderBasketItem } from '../types'; import { DrugOrderForm } from './drug-order-form.component'; import DrugSearch from './drug-search/drug-search.component'; import styles from './add-drug-order.scss'; diff --git a/packages/esm-patient-medications-app/src/add-drug-order/drug-order-form.component.tsx b/packages/esm-patient-medications-app/src/add-drug-order/drug-order-form.component.tsx index 3226682fb6..994322befd 100644 --- a/packages/esm-patient-medications-app/src/add-drug-order/drug-order-form.component.tsx +++ b/packages/esm-patient-medications-app/src/add-drug-order/drug-order-form.component.tsx @@ -40,18 +40,18 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useOrderConfig } from '../api/order-config'; import { type ConfigObject } from '../config-schema'; -import type { - CommonMedicationValueCoded, - DosingUnit, - DrugOrderBasketItem, - DurationUnit, - MedicationFrequency, - MedicationRoute, - QuantityUnit, -} from '../types'; import { useRequireOutpatientQuantity } from '../api'; import styles from './drug-order-form.scss'; -import { usePatientChartStore } from '@openmrs/esm-patient-common-lib'; +import { + usePatientChartStore, + type CommonMedicationValueCoded, + type DosingUnit, + type DrugOrderBasketItem, + type DurationUnit, + type MedicationFrequency, + type MedicationRoute, + type QuantityUnit, +} from '@openmrs/esm-patient-common-lib'; export interface DrugOrderFormProps { initialOrderBasketItem: DrugOrderBasketItem; diff --git a/packages/esm-patient-medications-app/src/add-drug-order/drug-search/drug-search.component.tsx b/packages/esm-patient-medications-app/src/add-drug-order/drug-search/drug-search.component.tsx index 59fdf95a8c..edcb9ad0cb 100644 --- a/packages/esm-patient-medications-app/src/add-drug-order/drug-search/drug-search.component.tsx +++ b/packages/esm-patient-medications-app/src/add-drug-order/drug-search/drug-search.component.tsx @@ -2,9 +2,8 @@ import React, { useCallback, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Search } from '@carbon/react'; import { useConfig, useDebounce, ResponsiveWrapper, closeWorkspace, useLayoutType } from '@openmrs/esm-framework'; -import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; +import { type DrugOrderBasketItem, launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; import { type ConfigObject } from '../../config-schema'; -import { type DrugOrderBasketItem } from '../../types'; import OrderBasketSearchResults from './order-basket-search-results.component'; import styles from './order-basket-search.scss'; diff --git a/packages/esm-patient-medications-app/src/add-drug-order/drug-search/drug-search.resource.tsx b/packages/esm-patient-medications-app/src/add-drug-order/drug-search/drug-search.resource.tsx index 9e0f470419..98cfe6e47e 100644 --- a/packages/esm-patient-medications-app/src/add-drug-order/drug-search/drug-search.resource.tsx +++ b/packages/esm-patient-medications-app/src/add-drug-order/drug-search/drug-search.resource.tsx @@ -1,8 +1,7 @@ import { useMemo } from 'react'; import useSWRImmutable from 'swr/immutable'; import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; -import type { Drug } from '@openmrs/esm-patient-common-lib'; -import { type DrugOrderBasketItem, type DrugOrderTemplate, type OrderTemplate } from '../../types'; +import type { Drug, DrugOrderBasketItem, DrugOrderTemplate, OrderTemplate } from '@openmrs/esm-patient-common-lib'; export interface DrugSearchResult { uuid: string; diff --git a/packages/esm-patient-medications-app/src/add-drug-order/drug-search/helpers.ts b/packages/esm-patient-medications-app/src/add-drug-order/drug-search/helpers.ts index f995f5d938..ae8ef4fe29 100644 --- a/packages/esm-patient-medications-app/src/add-drug-order/drug-search/helpers.ts +++ b/packages/esm-patient-medications-app/src/add-drug-order/drug-search/helpers.ts @@ -1,4 +1,4 @@ -import { type DrugOrderBasketItem } from '../../types'; +import { type DrugOrderBasketItem } from '@openmrs/esm-patient-common-lib'; type DrugsOrOrders = Pick; diff --git a/packages/esm-patient-medications-app/src/add-drug-order/drug-search/order-basket-search-results.component.tsx b/packages/esm-patient-medications-app/src/add-drug-order/drug-search/order-basket-search-results.component.tsx index 09dade754d..0902ccec88 100644 --- a/packages/esm-patient-medications-app/src/add-drug-order/drug-search/order-basket-search-results.component.tsx +++ b/packages/esm-patient-medications-app/src/add-drug-order/drug-search/order-basket-search-results.component.tsx @@ -3,7 +3,12 @@ import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { Button, ButtonSkeleton, SkeletonText, Tile } from '@carbon/react'; import { ShoppingCartArrowUp } from '@carbon/react/icons'; -import { launchPatientWorkspace, useOrderBasket, usePatientChartStore } from '@openmrs/esm-patient-common-lib'; +import { + type DrugOrderBasketItem, + launchPatientWorkspace, + useOrderBasket, + usePatientChartStore, +} from '@openmrs/esm-patient-common-lib'; import { ArrowRightIcon, closeWorkspace, @@ -22,7 +27,6 @@ import { useDrugSearch, useDrugTemplate, } from './drug-search.resource'; -import type { DrugOrderBasketItem } from '../../types'; import styles from './order-basket-search-results.scss'; export interface OrderBasketSearchResultsProps { diff --git a/packages/esm-patient-medications-app/src/api/api.ts b/packages/esm-patient-medications-app/src/api/api.ts index adae25c28a..369dfd80e3 100644 --- a/packages/esm-patient-medications-app/src/api/api.ts +++ b/packages/esm-patient-medications-app/src/api/api.ts @@ -2,9 +2,12 @@ import { useCallback, useMemo } from 'react'; import useSWR, { mutate } from 'swr'; import useSWRImmutable from 'swr/immutable'; import { openmrsFetch, restBaseUrl, useConfig, type FetchResponse } from '@openmrs/esm-framework'; -import { type OrderPost, type PatientOrderFetchResponse } from '@openmrs/esm-patient-common-lib'; +import { + type OrderPost, + type PatientOrderFetchResponse, + type DrugOrderBasketItem, +} from '@openmrs/esm-patient-common-lib'; import { type ConfigObject } from '../config-schema'; -import { type DrugOrderBasketItem } from '../types'; export const careSettingUuid = '6f0c9a92-6f24-11e3-af88-005056821db0'; diff --git a/packages/esm-patient-medications-app/src/api/order-config.ts b/packages/esm-patient-medications-app/src/api/order-config.ts index eda6d0ae11..35235b703d 100644 --- a/packages/esm-patient-medications-app/src/api/order-config.ts +++ b/packages/esm-patient-medications-app/src/api/order-config.ts @@ -7,7 +7,7 @@ import { type MedicationFrequency, type MedicationRoute, type QuantityUnit, -} from '../types'; +} from '@openmrs/esm-patient-common-lib'; export interface ConceptName { uuid: string; diff --git a/packages/esm-patient-medications-app/src/components/medications-details-table.component.tsx b/packages/esm-patient-medications-app/src/components/medications-details-table.component.tsx index d2bf770cc5..c1f5dfc9fe 100644 --- a/packages/esm-patient-medications-app/src/components/medications-details-table.component.tsx +++ b/packages/esm-patient-medications-app/src/components/medications-details-table.component.tsx @@ -40,7 +40,7 @@ import { import { useTranslation } from 'react-i18next'; import { useReactToPrint } from 'react-to-print'; import { type AddDrugOrderWorkspaceAdditionalProps } from '../add-drug-order/add-drug-order.workspace'; -import { type DrugOrderBasketItem } from '../types'; +import { type DrugOrderBasketItem } from '@openmrs/esm-patient-common-lib'; import { type ConfigObject } from '../config-schema'; import PrintComponent from '../print/print.component'; import styles from './medications-details-table.scss'; diff --git a/packages/esm-patient-medications-app/src/drug-order-basket-panel/drug-order-basket-panel.extension.tsx b/packages/esm-patient-medications-app/src/drug-order-basket-panel/drug-order-basket-panel.extension.tsx index 0dfdc69b23..098dab4cad 100644 --- a/packages/esm-patient-medications-app/src/drug-order-basket-panel/drug-order-basket-panel.extension.tsx +++ b/packages/esm-patient-medications-app/src/drug-order-basket-panel/drug-order-basket-panel.extension.tsx @@ -3,9 +3,8 @@ import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; import { Button, Tile } from '@carbon/react'; import { AddIcon, ChevronDownIcon, ChevronUpIcon, closeWorkspace, useLayoutType } from '@openmrs/esm-framework'; -import { launchPatientWorkspace, useOrderBasket } from '@openmrs/esm-patient-common-lib'; +import { launchPatientWorkspace, useOrderBasket, type DrugOrderBasketItem } from '@openmrs/esm-patient-common-lib'; import { prepMedicationOrderPostData } from '../api/api'; -import type { DrugOrderBasketItem } from '../types'; import OrderBasketItemTile from './order-basket-item-tile.component'; import RxIcon from './rx-icon.component'; import styles from './drug-order-basket-panel.scss'; diff --git a/packages/esm-patient-medications-app/src/drug-order-basket-panel/drug-order-basket-panel.test.tsx b/packages/esm-patient-medications-app/src/drug-order-basket-panel/drug-order-basket-panel.test.tsx index e26f8821f2..9fdb7ac121 100644 --- a/packages/esm-patient-medications-app/src/drug-order-basket-panel/drug-order-basket-panel.test.tsx +++ b/packages/esm-patient-medications-app/src/drug-order-basket-panel/drug-order-basket-panel.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { getByTextWithMarkup } from 'tools'; -import { type DrugOrderBasketItem } from '../types'; +import { type DrugOrderBasketItem } from '@openmrs/esm-patient-common-lib'; import { mockDrugSearchResultApiData, mockPatientDrugOrdersApiData } from '__mocks__'; import { getTemplateOrderBasketItem } from '../add-drug-order/drug-search/drug-search.resource'; import DrugOrderBasketPanel from './drug-order-basket-panel.extension'; diff --git a/packages/esm-patient-medications-app/src/drug-order-basket-panel/order-basket-item-tile.component.tsx b/packages/esm-patient-medications-app/src/drug-order-basket-panel/order-basket-item-tile.component.tsx index aa958d2dc1..5933cb97a2 100644 --- a/packages/esm-patient-medications-app/src/drug-order-basket-panel/order-basket-item-tile.component.tsx +++ b/packages/esm-patient-medications-app/src/drug-order-basket-panel/order-basket-item-tile.component.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { Button, ClickableTile, Tile } from '@carbon/react'; import { TrashCanIcon, useLayoutType, WarningIcon } from '@openmrs/esm-framework'; -import { type DrugOrderBasketItem } from '../types'; +import { type DrugOrderBasketItem } from '@openmrs/esm-patient-common-lib'; import styles from './order-basket-item-tile.scss'; export interface OrderBasketItemTileProps { diff --git a/packages/esm-patient-orders-app/src/config-schema.ts b/packages/esm-patient-orders-app/src/config-schema.ts index 7ddc4494d7..47fb1dcf5a 100644 --- a/packages/esm-patient-orders-app/src/config-schema.ts +++ b/packages/esm-patient-orders-app/src/config-schema.ts @@ -6,6 +6,12 @@ export const configSchema = { _description: 'The encounter type of the encounter encapsulating orders', _default: '39da3525-afe4-45ff-8977-c53b7b359158', }, + debounceDelayInMs: { + _type: Type.Number, + _description: + 'Number of milliseconds to delay the search operation in the drug search input by after the user starts typing. The useDebounce hook delays the search by 300ms by default', + _default: 300, + }, showPrintButton: { _type: Type.Boolean, _description: @@ -16,9 +22,12 @@ export const configSchema = { _type: Type.Array, _default: [ { - orderTypeUuid: '425ae793-e776-4f84-8be1-2f322744644d', - conceptClass: '', - orderableConcepts: ['06393843-1790-43cd-acba-cd497300c734'], + orderTypeUuid: '67a92e56-0f88-11ea-8d71-362b9e155667', + orderableConcepts: [], + }, + { + orderTypeUuid: '67a9328e-0f88-11ea-8d71-362b9e155667', + orderableConcepts: [], }, ], _elements: { @@ -26,10 +35,6 @@ export const configSchema = { _type: Type.String, _description: 'Order type UUID to be displayed on the order basket', }, - conceptClass: { - _type: Type.String, - _description: 'Concept with the given class name will be ordered', - }, orderableConcepts: { _type: Type.Array, _description: @@ -47,7 +52,7 @@ export interface ConfigObject { showPrintButton: boolean; orderTypes: Array<{ orderTypeUuid: string; - conceptClass: string; orderableConcepts: Array; }>; + debounceDelayInMs: number; } diff --git a/packages/esm-patient-orders-app/src/order-basket/order-basket.workspace.tsx b/packages/esm-patient-orders-app/src/order-basket/order-basket.workspace.tsx index c025022617..8dd7dcf32e 100644 --- a/packages/esm-patient-orders-app/src/order-basket/order-basket.workspace.tsx +++ b/packages/esm-patient-orders-app/src/order-basket/order-basket.workspace.tsx @@ -125,8 +125,8 @@ const OrderBasket: React.FC = ({ ))}
diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-type.component.tsx b/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-type.component.tsx index 6aa39b8be5..aef2bb7ee5 100644 --- a/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-type.component.tsx +++ b/packages/esm-patient-orders-app/src/order-basket/order-types/generic-order-type.component.tsx @@ -2,7 +2,13 @@ import React, { type ComponentProps, useCallback, useEffect, useMemo, useState } import { Button, Tile } from '@carbon/react'; import classNames from 'classnames'; import styles from './generic-order-panel.scss'; -import { AddIcon, ChevronDownIcon, ChevronUpIcon, closeWorkspace, useLayoutType } from '@openmrs/esm-framework'; +import { + AddIcon, + ChevronDownIcon, + ChevronUpIcon, + type DefaultWorkspaceProps, + useLayoutType, +} from '@openmrs/esm-framework'; import { useTranslation } from 'react-i18next'; import { type DrugOrderBasketItem, @@ -15,14 +21,15 @@ import OrderBasketItemTile from './order-basket-item-tile.component'; interface GenericOrderTypeProps { orderTypeUuid: string; - conceptClass: string; orderableConcepts: Array; + closeWorkspace: DefaultWorkspaceProps['closeWorkspace']; } -const GenericOrderType: React.FC = ({ orderTypeUuid, conceptClass, orderableConcepts }) => { +const GenericOrderType: React.FC = ({ orderTypeUuid, orderableConcepts, closeWorkspace }) => { const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; const { data, isLoading, error } = useOrderType(orderTypeUuid); + const conceptClass = data?.data?.conceptClasses?.[0]?.uuid; const orderType = data?.data; const prepOrderPostFunc = useMemo(() => prepOrderPostData(orderTypeUuid), [orderTypeUuid]); const { orders, setOrders } = useOrderBasket(orderTypeUuid, prepOrderPostFunc); @@ -64,7 +71,7 @@ const GenericOrderType: React.FC = ({ orderTypeUuid, conc }, [orders]); const openConceptSearch = () => { - closeWorkspace('order-basket', { + closeWorkspace({ ignoreChanges: true, onWorkspaceClose: () => launchPatientWorkspace('orderable-concept-workspace', { @@ -76,7 +83,7 @@ const GenericOrderType: React.FC = ({ orderTypeUuid, conc }; const openOrderForm = (order: DrugOrderBasketItem) => { - closeWorkspace('order-basket', { + closeWorkspace({ ignoreChanges: true, onWorkspaceClose: () => launchPatientWorkspace('add-drug-order', { order }), }); @@ -95,6 +102,10 @@ const GenericOrderType: React.FC = ({ orderTypeUuid, conc setIsExpanded(orders.length > 0); }, [orders]); + if (isLoading) { + return null; + } + return ( ; } @@ -49,7 +50,7 @@ const OrderableConceptSearchWorkspace: React.FC prepOrderPostData(orderTypeUuid), [orderTypeUuid]); const { orders, setOrders } = useOrderBasket(orderTypeUuid, prepOrderPostFunc); - const [currentOrder, setCurrentOrder] = useState(initialOrder); + // const [currentOrder, setCurrentOrder] = useState(initialOrder); const session = useSession(); const cancelDrugOrder = useCallback(() => { @@ -61,16 +62,16 @@ const OrderableConceptSearchWorkspace: React.FC { const existingOrder = orders.find((order) => ordersEqual(order, searchResult)); - if (existingOrder) { - setCurrentOrder(existingOrder); - } else { - setCurrentOrder(searchResult); - } + // if (existingOrder) { + // setCurrentOrder(existingOrder); + // } else { + // setCurrentOrder(searchResult); + // } }, [orders], ); - const saveDrugOrder = useCallback( + const saveOrderForm = useCallback( (finalizedOrder: OrderBasketItem) => { finalizedOrder.careSetting = careSettingUuid; finalizedOrder.orderer = session.currentProvider.uuid; @@ -135,10 +136,11 @@ function ConceptSearch({ orderableConcepts, }: ConceptSearchProps) { const { t } = useTranslation(); + const { data } = useOrderType(orderTypeUuid); const isTablet = useLayoutType() === 'tablet'; const [searchTerm, setSearchTerm] = useState(''); - // const { debounceDelayInMs } = useConfig(); - const debouncedSearchTerm = useDebounce(searchTerm); + const { debounceDelayInMs } = useConfig(); + const debouncedSearchTerm = useDebounce(searchTerm, debounceDelayInMs ?? 300); const searchInputRef = useRef(null); const cancelDrugOrder = useCallback(() => { @@ -161,8 +163,12 @@ function ConceptSearch({ { const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; - const { concepts, isLoading, error } = useOrderableConcepts(conceptClass, orderableConcepts); - - const filteredTestTypes = useMemo(() => { - if (!searchTerm) { - return concepts; - } - - if (searchTerm && searchTerm.trim() !== '') { - return concepts?.filter((testType) => - testType.synonyms.some((name) => name.toLowerCase().includes(searchTerm.toLowerCase())), - ); - } - }, [searchTerm, concepts]); + const { concepts, isLoading, error } = useOrderableConcepts(searchTerm, conceptClass, orderableConcepts); if (isLoading) { return ; @@ -76,7 +64,7 @@ const OrderableConceptSearchResults: React.FC
@@ -84,7 +72,7 @@ const OrderableConceptSearchResults: React.FC {t('searchResultsMatchesForTerm', '{{count}} results for "{{searchTerm}}"', { - count: filteredTestTypes?.length, + count: concepts?.length, searchTerm, })} @@ -94,11 +82,11 @@ const OrderableConceptSearchResults: React.FC )}
- {filteredTestTypes.map((testType) => ( + {concepts.map((concept) => ( @@ -162,14 +150,14 @@ const TestTypeSearchSkeleton = () => { }; interface TestTypeSearchResultItemProps { - testType: ConceptType; + concept: ConceptType; openOrderForm: (searchResult: OrderBasketItem) => void; orderTypeUuid: string; closeWorkspace: DefaultWorkspaceProps['closeWorkspace']; } const TestTypeSearchResultItem: React.FC = ({ - testType, + concept, openOrderForm, orderTypeUuid, closeWorkspace, @@ -178,11 +166,12 @@ const TestTypeSearchResultItem: React.FC = ({ const isTablet = useLayoutType() === 'tablet'; const session = useSession(); const { orders, setOrders } = useGenericOrderBasket(orderTypeUuid); + const { data, isLoading } = useOrderType(orderTypeUuid); - // const testTypeAlreadyInBasket = useMemo( - // () => orders?.some((order) => order.testType.conceptUuid === testType.conceptUuid), - // [orders, testType], - // ); + const orderAlreadyInBasket = useMemo( + () => orders?.some((order) => matchOrder(data?.data?.javaClassName, order, concept)), + [orders, data, concept], + ); // const createLabOrder = useCallback( // (testType: TestType) => { @@ -212,7 +201,7 @@ const TestTypeSearchResultItem: React.FC = ({ >

- {testType.label}{' '} + {concept.label}{' '}

diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts b/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts index dce7582935..51aa4e5fae 100644 --- a/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts +++ b/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts @@ -1,8 +1,30 @@ -import { type Concept, type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; -import { type OrderBasketItem, prepOrderPostData, useOrderBasket } from '@openmrs/esm-patient-common-lib'; +import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; +import { + type LabOrderBasketItem, + type OrderBasketItem, + type OrderTypeJavaClassName, + prepOrderPostData, + useOrderBasket, +} from '@openmrs/esm-patient-common-lib'; import { useEffect, useMemo } from 'react'; import useSWRImmutable from 'swr/immutable'; +export interface Concept { + uuid: string; + name: { + display: string; + }; + names: Array<{ + display: string; + }>; + conceptClass: { + uuid: string; + }; + answers: Array; + setMembers: Array; + display: string; +} + type ConceptResult = FetchResponse; type ConceptResults = FetchResponse<{ results: Array }>; @@ -18,15 +40,15 @@ function openmrsFetchMultiple(urls: Array) { return Promise.all(urls.map((url) => openmrsFetch<{ results: Array }>(url))); } -function useOrderableConceptSWR(conceptClass: string, orderableConcepts?: Array) { - const { data, isLoading, error } = useSWRImmutable( +function useOrderableConceptSWR(searchTerm: string, conceptClass: string, orderableConcepts?: Array) { + const { data, isLoading, error } = useSWRImmutable | ConceptResults>( () => orderableConcepts?.length ? orderableConcepts.map( (c) => `${restBaseUrl}/concept/${c}?v=custom:(display,names:(display),uuid,setMembers:(display,uuid,names:(display),setMembers:(display,uuid,names:(display))))`, ) - : `${restBaseUrl}/concept?class=${conceptClass}&v=custom:(display,names:(display),uuid,setMembers:(display,uuid,names:(display),setMembers:(display,uuid,names:(display))))`, + : `${restBaseUrl}/concept?class=${conceptClass}&name=${searchTerm}&searchType=fuzzy&v=custom:(display,names:(display),uuid,setMembers:(display,uuid,names:(display),setMembers:(display,uuid,names:(display))))`, (orderableConcepts ? openmrsFetchMultiple : openmrsFetch) as any, { shouldRetryOnError(err) { @@ -37,10 +59,19 @@ function useOrderableConceptSWR(conceptClass: string, orderableConcepts?: Array< const results = useMemo(() => { if (isLoading || error) return null; - return orderableConcepts - ? (data as Array)?.flatMap((d) => d.data.setMembers) - : (data as ConceptResults)?.data.results ?? ([] as Concept[]); - }, [data, isLoading, error, orderableConcepts]); + + if (orderableConcepts) { + const concepts = (data as Array)?.flatMap((d) => d.data.setMembers); + if (searchTerm) { + return concepts?.filter((concept) => + concept.names.some((name) => name.display.toLowerCase().includes(searchTerm.toLowerCase())), + ); + } + return concepts; + } else { + return (data as ConceptResults)?.data.results ?? ([] as Concept[]); + } + }, [isLoading, error, orderableConcepts, data, searchTerm]); return { data: results, @@ -55,8 +86,9 @@ export interface ConceptType { synonyms: Array; } -export function useOrderableConcepts(conceptClass: string, orderableConcepts: Array) { +export function useOrderableConcepts(searchTerm: string, conceptClass: string, orderableConcepts: Array) { const { data, isLoading, error } = useOrderableConceptSWR( + searchTerm, conceptClass, orderableConcepts.length ? orderableConcepts : null, ); @@ -86,3 +118,12 @@ export function useOrderableConcepts(conceptClass: string, orderableConcepts: Ar error: error, }; } + +export function matchOrder(javaClassName: OrderTypeJavaClassName, order: OrderBasketItem, concept: ConceptType) { + switch (javaClassName) { + // case 'org.openmrs.DrugOrder': + // return order1.action === order2.action && order1.commonMedicationName === order2.commonMedicationName; + case 'org.openmrs.TestOrder': + return (order as LabOrderBasketItem).testType.conceptUuid === concept.conceptUuid; + } +} diff --git a/packages/esm-patient-orders-app/translations/en.json b/packages/esm-patient-orders-app/translations/en.json index 468f5731f7..03c10e719b 100644 --- a/packages/esm-patient-orders-app/translations/en.json +++ b/packages/esm-patient-orders-app/translations/en.json @@ -4,12 +4,14 @@ "add": "Add", "addResults": "Add results", "allOrders": "All orders", + "backToOrderBasket": "Back to order basket", "cancel": "Cancel", "cancellationDate": "Cancellation date", "cancellationDateRequired": "Cancellation date is required", "cancelOrder": "Cancel order", "checkFilters": "Check the filters above", "chooseAnOption": "Choose an option", + "clearSearchResults": "Clear results", "dateCannotBeBeforeToday": "Date cannot be before today", "dateOfOrder": "Date of order", "dateRange": "Date range", @@ -17,13 +19,14 @@ "discontinued": "Discontinued", "dosage": "Dosage", "dose": "Dose", - "drugOrders": "Drug orders", "editResults": "Edit results", "endDate": "End date", "enterTestResults": "Enter test results", "error": "Error", "errorCancellingOrder": "Error cancelling order", + "errorFetchingTestTypes": "Error fetching results for \"{{searchTerm}}\"", "errorSavingLabResults": "Error saving lab results", + "goToDrugOrderForm": "Order form", "indication": "Indication", "launchOrderBasket": "Launch order basket", "loading": "Loading", @@ -34,8 +37,10 @@ "medications": "Medications", "modifyOrder": "Modify order", "noMatchingOrdersToDisplay": "No matching orders to display", + "noResultsForTestTypeSearch": "No results to display for \"{{searchTerm}}\"", "normalRange": "Normal range", "onDate": "on", + "or": "or", "order": "Order", "orderActionDiscontinue": "Discontinue", "orderActionIncomplete": "Incomplete", @@ -67,10 +72,16 @@ "refills": "Refills", "removeFromBasket": "Remove from basket", "result": "Result", + "returnToOrderBasket": "Return to order basket", "saveAndClose": "Save and close", "saveDrugOrderFailed": "Error ordering {{orderName}}", "saveLabResults": "Save lab results", "saving": "Saving", + "searchAgain": "search again", + "searchConcepts": "Search concepts", + "searchFieldPlaceholder": "Search for a drug or orderset (e.g. \"Aspirin\")", + "searchResultsMatchesForTerm_one": "{{count}} results for \"{{searchTerm}}\"", + "searchResultsMatchesForTerm_other": "{{count}} results for \"{{searchTerm}}\"", "searchTable": "Search table", "selectOrderType": "Select order type", "signAndClose": "Sign and close", @@ -82,6 +93,9 @@ "Test Order_few": "Test orders", "testType": "Test type", "tryReopeningTheWorkspaceAgain": "Please try launching the workspace again", + "trySearchingAgain": "Please try searching again", + "tryTo": "Try to", "unknownOrderType": "Unknown order type", - "updated": "Updated" + "updated": "Updated", + "usingADifferentTerm": "using a different term" } From 03b11434616127a6a1463c0c0ef7d9f2ef2bf9c9 Mon Sep 17 00:00:00 2001 From: Vineet Sharma Date: Wed, 20 Nov 2024 15:29:31 +0530 Subject: [PATCH 05/19] All test orders should be managed by esm-patient-test-orders --- .../src/orders/types/index.ts | 4 +- .../src/orders/types/test-order.ts | 2 +- .../src/orders/useOrders.ts | 15 +++- .../orders-details-table.component.tsx | 9 +- .../src/config-schema.ts | 16 ++-- .../order-basket/order-basket.workspace.tsx | 2 +- .../generic-order-type.component.tsx | 7 +- .../orderable-concept-search.workspace.tsx | 6 +- .../search-results.component.tsx | 6 +- .../src/order-basket/order-types/resources.ts | 4 +- .../translations/en.json | 2 +- .../src/config-schema.ts | 30 +++++++ packages/esm-patient-tests-app/src/index.ts | 4 +- .../add-test-order/add-test-order.scss} | 0 .../add-test-order/add-test-order.test.tsx} | 6 +- .../add-test-order.workspace.tsx} | 22 +++-- .../test-order-form.component.tsx} | 34 ++++--- .../add-test-order/test-order-form.scss} | 0 .../add-test-order/test-order.ts} | 8 +- .../test-type-search.component.tsx | 88 ++++++++++--------- .../add-test-order}/test-type-search.scss | 0 .../useOrderableConceptSets.ts} | 59 +++++++++---- .../add-test-order}/useTestTypes.test.ts | 10 +-- .../src/{lab-orders => test-orders}/api.ts | 6 +- .../lab-icon.component.tsx | 0 .../lab-order-basket-item-tile.component.tsx | 6 +- .../lab-order-basket-item-tile.scss | 0 .../lab-order-basket-panel.extension.tsx | 65 +++++++++++--- .../lab-order-basket-panel.scss | 0 .../lab-order-basket-panel.test.tsx | 6 +- .../lab-order-basket.scss | 0 .../results-viewer.extension.test.tsx | 1 + .../tree-view/tree-view-wrapper.test.tsx | 1 + packages/esm-patient-tests-app/src/types.ts | 2 +- .../translations/en.json | 5 +- 35 files changed, 280 insertions(+), 146 deletions(-) rename packages/esm-patient-tests-app/src/{lab-orders/add-lab-order/add-lab-order.scss => test-orders/add-test-order/add-test-order.scss} (100%) rename packages/esm-patient-tests-app/src/{lab-orders/add-lab-order/add-lab-order.test.tsx => test-orders/add-test-order/add-test-order.test.tsx} (97%) rename packages/esm-patient-tests-app/src/{lab-orders/add-lab-order/add-lab-order.workspace.tsx => test-orders/add-test-order/add-test-order.workspace.tsx} (82%) rename packages/esm-patient-tests-app/src/{lab-orders/add-lab-order/lab-order-form.component.tsx => test-orders/add-test-order/test-order-form.component.tsx} (92%) rename packages/esm-patient-tests-app/src/{lab-orders/add-lab-order/lab-order-form.scss => test-orders/add-test-order/test-order-form.scss} (100%) rename packages/esm-patient-tests-app/src/{lab-orders/add-lab-order/lab-order.ts => test-orders/add-test-order/test-order.ts} (70%) rename packages/esm-patient-tests-app/src/{lab-orders/add-lab-order => test-orders/add-test-order}/test-type-search.component.tsx (80%) rename packages/esm-patient-tests-app/src/{lab-orders/add-lab-order => test-orders/add-test-order}/test-type-search.scss (100%) rename packages/esm-patient-tests-app/src/{lab-orders/add-lab-order/useTestTypes.ts => test-orders/add-test-order/useOrderableConceptSets.ts} (53%) rename packages/esm-patient-tests-app/src/{lab-orders/add-lab-order => test-orders/add-test-order}/useTestTypes.test.ts (86%) rename packages/esm-patient-tests-app/src/{lab-orders => test-orders}/api.ts (96%) rename packages/esm-patient-tests-app/src/{lab-orders => test-orders}/lab-order-basket-panel/lab-icon.component.tsx (100%) rename packages/esm-patient-tests-app/src/{lab-orders => test-orders}/lab-order-basket-panel/lab-order-basket-item-tile.component.tsx (96%) rename packages/esm-patient-tests-app/src/{lab-orders => test-orders}/lab-order-basket-panel/lab-order-basket-item-tile.scss (100%) rename packages/esm-patient-tests-app/src/{lab-orders => test-orders}/lab-order-basket-panel/lab-order-basket-panel.extension.tsx (76%) rename packages/esm-patient-tests-app/src/{lab-orders => test-orders}/lab-order-basket-panel/lab-order-basket-panel.scss (100%) rename packages/esm-patient-tests-app/src/{lab-orders => test-orders}/lab-order-basket-panel/lab-order-basket-panel.test.tsx (91%) rename packages/esm-patient-tests-app/src/{lab-orders => test-orders}/lab-order-basket-panel/lab-order-basket.scss (100%) diff --git a/packages/esm-patient-common-lib/src/orders/types/index.ts b/packages/esm-patient-common-lib/src/orders/types/index.ts index db8287d80e..93344613a2 100644 --- a/packages/esm-patient-common-lib/src/orders/types/index.ts +++ b/packages/esm-patient-common-lib/src/orders/types/index.ts @@ -1,9 +1,9 @@ import type { DrugOrderBasketItem } from './drug-order'; import type { OrderBasketItem } from './order'; -import type { LabOrderBasketItem } from './test-order'; +import type { TestOrderBasketItem } from './test-order'; export * from './order'; export * from './drug-order'; export * from './test-order'; -export type GenericOrderBasketItem = OrderBasketItem | DrugOrderBasketItem | LabOrderBasketItem; +export type GenericOrderBasketItem = OrderBasketItem | DrugOrderBasketItem | TestOrderBasketItem; diff --git a/packages/esm-patient-common-lib/src/orders/types/test-order.ts b/packages/esm-patient-common-lib/src/orders/types/test-order.ts index 0ae855169c..6655c23319 100644 --- a/packages/esm-patient-common-lib/src/orders/types/test-order.ts +++ b/packages/esm-patient-common-lib/src/orders/types/test-order.ts @@ -1,6 +1,6 @@ import type { OrderBasketItem, OrderUrgency } from './order'; -export interface LabOrderBasketItem extends OrderBasketItem { +export interface TestOrderBasketItem extends OrderBasketItem { testType?: { label: string; conceptUuid: string; diff --git a/packages/esm-patient-common-lib/src/orders/useOrders.ts b/packages/esm-patient-common-lib/src/orders/useOrders.ts index b6fd7eecf0..2b85b55a04 100644 --- a/packages/esm-patient-common-lib/src/orders/useOrders.ts +++ b/packages/esm-patient-common-lib/src/orders/useOrders.ts @@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react'; import useSWR, { useSWRConfig } from 'swr'; import { type FetchResponse, openmrsFetch, restBaseUrl, toOmrsIsoString } from '@openmrs/esm-framework'; import { type OrderTypeFetchResponse, type PatientOrderFetchResponse } from '@openmrs/esm-patient-common-lib'; +import useSWRImmutable from 'swr/immutable'; export type Status = 'ACTIVE' | 'any'; export const careSettingUuid = '6f0c9a92-6f24-11e3-af88-005056821db0'; @@ -87,7 +88,19 @@ interface OrderTypeResponse { } export function useOrderType(orderTypeUuid: string) { - return useSWR>(`${restBaseUrl}/ordertype/${orderTypeUuid}`); + const { data, isLoading, isValidating, error } = useSWRImmutable>( + `${restBaseUrl}/ordertype/${orderTypeUuid}`, + ); + const results = useMemo( + () => ({ + isLoadingOrderType: isLoading, + orderType: data?.data, + errorFetchingOrderType: error, + isValidatingOrderType: isValidating, + }), + [data?.data, error, isLoading, isValidating], + ); + return results; } export function getDrugOrderByUuid(orderUuid: string) { diff --git a/packages/esm-patient-orders-app/src/components/orders-details-table.component.tsx b/packages/esm-patient-orders-app/src/components/orders-details-table.component.tsx index fbcb31664e..3307073781 100644 --- a/packages/esm-patient-orders-app/src/components/orders-details-table.component.tsx +++ b/packages/esm-patient-orders-app/src/components/orders-details-table.component.tsx @@ -35,7 +35,7 @@ import { launchPatientWorkspace, PatientChartPagination, type DrugOrderBasketItem, - type LabOrderBasketItem, + type TestOrderBasketItem, type Order, type OrderBasketItem, type OrderType, @@ -95,7 +95,7 @@ interface DataTableRow { isExpanded: boolean; } -type MutableOrderBasketItem = OrderBasketItem | LabOrderBasketItem | DrugOrderBasketItem; +type MutableOrderBasketItem = OrderBasketItem | TestOrderBasketItem | DrugOrderBasketItem; const medicationsOrderBasket = 'medications'; const labsOrderBasket = 'labs'; @@ -134,7 +134,10 @@ const OrderDetailsTable: React.FC = ({ patientUuid, showAddBu launchAddDrugOrder({ order: buildMedicationOrder(orderItem, 'REVISE') }); break; case 'testorder': - launchModifyLabOrder({ order: buildLabOrder(orderItem, 'REVISE') }); + launchModifyLabOrder({ + order: buildLabOrder(orderItem, 'REVISE'), + orderTypeUuid: orderItem?.orderType?.uuid, + }); break; default: launchOrderBasket(); diff --git a/packages/esm-patient-orders-app/src/config-schema.ts b/packages/esm-patient-orders-app/src/config-schema.ts index 47fb1dcf5a..7430cadfd3 100644 --- a/packages/esm-patient-orders-app/src/config-schema.ts +++ b/packages/esm-patient-orders-app/src/config-schema.ts @@ -21,14 +21,14 @@ export const configSchema = { orderTypes: { _type: Type.Array, _default: [ - { - orderTypeUuid: '67a92e56-0f88-11ea-8d71-362b9e155667', - orderableConcepts: [], - }, - { - orderTypeUuid: '67a9328e-0f88-11ea-8d71-362b9e155667', - orderableConcepts: [], - }, + // { + // orderTypeUuid: '67a92e56-0f88-11ea-8d71-362b9e155667', + // orderableConcepts: [], + // }, + // { + // orderTypeUuid: '67a9328e-0f88-11ea-8d71-362b9e155667', + // orderableConcepts: [], + // }, ], _elements: { orderTypeUuid: { diff --git a/packages/esm-patient-orders-app/src/order-basket/order-basket.workspace.tsx b/packages/esm-patient-orders-app/src/order-basket/order-basket.workspace.tsx index 8dd7dcf32e..f8382b82da 100644 --- a/packages/esm-patient-orders-app/src/order-basket/order-basket.workspace.tsx +++ b/packages/esm-patient-orders-app/src/order-basket/order-basket.workspace.tsx @@ -120,7 +120,7 @@ const OrderBasket: React.FC = ({ })} name="order-basket-slot" /> - {config?.orderTypes?.length && + {config?.orderTypes?.length > 0 && config?.orderTypes?.map((orderType) => ( = ({ orderTypeUuid, orderableConcepts, closeWorkspace }) => { const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; - const { data, isLoading, error } = useOrderType(orderTypeUuid); - const conceptClass = data?.data?.conceptClasses?.[0]?.uuid; - const orderType = data?.data; + const { orderType, isLoadingOrderType } = useOrderType(orderTypeUuid); + const conceptClass = orderType?.conceptClasses?.[0]?.uuid; const prepOrderPostFunc = useMemo(() => prepOrderPostData(orderTypeUuid), [orderTypeUuid]); const { orders, setOrders } = useOrderBasket(orderTypeUuid, prepOrderPostFunc); const [isExpanded, setIsExpanded] = useState(orders.length > 0); @@ -102,7 +101,7 @@ const GenericOrderType: React.FC = ({ orderTypeUuid, orde setIsExpanded(orders.length > 0); }, [orders]); - if (isLoading) { + if (isLoadingOrderType) { return null; } diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/orderable-concept-search.workspace.tsx b/packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/orderable-concept-search.workspace.tsx index b191921f91..863f0dd5a3 100644 --- a/packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/orderable-concept-search.workspace.tsx +++ b/packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/orderable-concept-search.workspace.tsx @@ -136,7 +136,7 @@ function ConceptSearch({ orderableConcepts, }: ConceptSearchProps) { const { t } = useTranslation(); - const { data } = useOrderType(orderTypeUuid); + const { orderType } = useOrderType(orderTypeUuid); const isTablet = useLayoutType() === 'tablet'; const [searchTerm, setSearchTerm] = useState(''); const { debounceDelayInMs } = useConfig(); @@ -164,10 +164,10 @@ function ConceptSearch({ autoFocus size="lg" placeholder={t('searchFieldOrder', 'Search for {{orderType}} order', { - orderType: data?.data?.display ?? '', + orderType: orderType?.display ?? '', })} labelText={t('searchFieldOrder', 'Search for {{orderType}} order', { - orderType: data?.data?.display ?? '', + orderType: orderType?.display ?? '', })} onChange={handleSearchTermChange} ref={searchInputRef} diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/search-results.component.tsx b/packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/search-results.component.tsx index 1bc61d97d6..b2ed0b1f51 100644 --- a/packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/search-results.component.tsx +++ b/packages/esm-patient-orders-app/src/order-basket/order-types/orderable-concept-search/search-results.component.tsx @@ -166,11 +166,11 @@ const TestTypeSearchResultItem: React.FC = ({ const isTablet = useLayoutType() === 'tablet'; const session = useSession(); const { orders, setOrders } = useGenericOrderBasket(orderTypeUuid); - const { data, isLoading } = useOrderType(orderTypeUuid); + const { orderType, isLoadingOrderType } = useOrderType(orderTypeUuid); const orderAlreadyInBasket = useMemo( - () => orders?.some((order) => matchOrder(data?.data?.javaClassName, order, concept)), - [orders, data, concept], + () => orders?.some((order) => matchOrder(orderType?.javaClassName, order, concept)), + [orders, orderType, concept], ); // const createLabOrder = useCallback( diff --git a/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts b/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts index 51aa4e5fae..8dfcf1a17f 100644 --- a/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts +++ b/packages/esm-patient-orders-app/src/order-basket/order-types/resources.ts @@ -1,6 +1,6 @@ import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; import { - type LabOrderBasketItem, + type TestOrderBasketItem, type OrderBasketItem, type OrderTypeJavaClassName, prepOrderPostData, @@ -124,6 +124,6 @@ export function matchOrder(javaClassName: OrderTypeJavaClassName, order: OrderBa // case 'org.openmrs.DrugOrder': // return order1.action === order2.action && order1.commonMedicationName === order2.commonMedicationName; case 'org.openmrs.TestOrder': - return (order as LabOrderBasketItem).testType.conceptUuid === concept.conceptUuid; + return (order as TestOrderBasketItem).testType.conceptUuid === concept.conceptUuid; } } diff --git a/packages/esm-patient-orders-app/translations/en.json b/packages/esm-patient-orders-app/translations/en.json index 03c10e719b..8066985851 100644 --- a/packages/esm-patient-orders-app/translations/en.json +++ b/packages/esm-patient-orders-app/translations/en.json @@ -79,7 +79,7 @@ "saving": "Saving", "searchAgain": "search again", "searchConcepts": "Search concepts", - "searchFieldPlaceholder": "Search for a drug or orderset (e.g. \"Aspirin\")", + "searchFieldOrder": "Search for {{orderType}} order", "searchResultsMatchesForTerm_one": "{{count}} results for \"{{searchTerm}}\"", "searchResultsMatchesForTerm_other": "{{count}} results for \"{{searchTerm}}\"", "searchTable": "Search table", diff --git a/packages/esm-patient-tests-app/src/config-schema.ts b/packages/esm-patient-tests-app/src/config-schema.ts index 327ea09822..3110be669f 100644 --- a/packages/esm-patient-tests-app/src/config-schema.ts +++ b/packages/esm-patient-tests-app/src/config-schema.ts @@ -1,4 +1,5 @@ import { Type } from '@openmrs/esm-framework'; +import _default from 'react-hook-form/dist/logic/appendErrors'; export const configSchema = { resultsViewerConcepts: { @@ -54,6 +55,31 @@ export const configSchema = { _description: 'Whether to display the Lab Reference number field in the Lab Order form. This field maps to the accesion_number property in the Order data model', }, + additionalOrderTypes: { + _type: Type.Array, + _description: '', + _elements: { + orderTypeUuid: { + _type: Type.UUID, + _description: 'UUID for the new order type', + }, + orderableConceptSets: { + _type: Type.UUID, + _description: + 'UUIDs of concepts that represent orderable concept sets. If an empty array `[]` is provided, every concept with class mentioned in the `orderType` will be considered orderable.', + }, + }, + _default: [ + { + orderTypeUuid: '67a92e56-0f88-11ea-8d71-362b9e155667', + orderableConceptSets: [], + }, + { + orderTypeUuid: '5338a5b1-2cbc-4081-9a9b-9e479e2acaad', + orderableConceptSets: [], + }, + ], + }, labTestsWithOrderReasons: { _type: Type.Array, _elements: { @@ -105,5 +131,9 @@ export interface ConfigObject { labOrderableConcepts: Array; }; showLabReferenceNumberField: boolean; + additionalOrderTypes: Array<{ + orderTypeUuid: string; + orderableConceptSets: Array; + }>; resultsViewerConcepts: Array; } diff --git a/packages/esm-patient-tests-app/src/index.ts b/packages/esm-patient-tests-app/src/index.ts index 2f1d0e0452..4a7c89f4d4 100644 --- a/packages/esm-patient-tests-app/src/index.ts +++ b/packages/esm-patient-tests-app/src/index.ts @@ -44,13 +44,13 @@ export const testResultsDashboardLink = ); export const labOrderPanel = getAsyncLifecycle( - () => import('./lab-orders/lab-order-basket-panel/lab-order-basket-panel.extension'), + () => import('./test-orders/lab-order-basket-panel/lab-order-basket-panel.extension'), options, ); // t('addLabOrderWorkspaceTitle', 'Add lab order') export const addLabOrderWorkspace = getAsyncLifecycle( - () => import('./lab-orders/add-lab-order/add-lab-order.workspace'), + () => import('./test-orders/add-test-order/add-test-order.workspace'), options, ); diff --git a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/add-lab-order.scss b/packages/esm-patient-tests-app/src/test-orders/add-test-order/add-test-order.scss similarity index 100% rename from packages/esm-patient-tests-app/src/lab-orders/add-lab-order/add-lab-order.scss rename to packages/esm-patient-tests-app/src/test-orders/add-test-order/add-test-order.scss diff --git a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/add-lab-order.test.tsx b/packages/esm-patient-tests-app/src/test-orders/add-test-order/add-test-order.test.tsx similarity index 97% rename from packages/esm-patient-tests-app/src/lab-orders/add-lab-order/add-lab-order.test.tsx rename to packages/esm-patient-tests-app/src/test-orders/add-test-order/add-test-order.test.tsx index a4eafcc043..6b0f1f9896 100644 --- a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/add-lab-order.test.tsx +++ b/packages/esm-patient-tests-app/src/test-orders/add-test-order/add-test-order.test.tsx @@ -15,8 +15,8 @@ import { type PostDataPrepFunction, useOrderBasket } from '@openmrs/esm-patient- import { configSchema, type ConfigObject } from '../../config-schema'; import { mockSessionDataResponse } from '__mocks__'; import { mockPatient } from 'tools'; -import { createEmptyLabOrder } from './lab-order'; -import AddLabOrderWorkspace from './add-lab-order.workspace'; +import { createEmptyLabOrder } from './test-order'; +import AddLabOrderWorkspace from './add-test-order.workspace'; const mockCloseWorkspace = closeWorkspace as jest.Mock; const mockUseLayoutType = jest.mocked(useLayoutType); @@ -86,6 +86,8 @@ function renderAddLabOrderWorkspace() { promptBeforeClosing={mockPromptBeforeClosing} patientUuid={ptUuid} setTitle={jest.fn()} + orderTypeUuid="" + orderableConceptSets={[]} />, ); return { mockCloseWorkspace, mockPromptBeforeClosing, mockCloseWorkspaceWithSavedChanges, ...view }; diff --git a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/add-lab-order.workspace.tsx b/packages/esm-patient-tests-app/src/test-orders/add-test-order/add-test-order.workspace.tsx similarity index 82% rename from packages/esm-patient-tests-app/src/lab-orders/add-lab-order/add-lab-order.workspace.tsx rename to packages/esm-patient-tests-app/src/test-orders/add-test-order/add-test-order.workspace.tsx index 7c9070d421..c4c190919d 100644 --- a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/add-lab-order.workspace.tsx +++ b/packages/esm-patient-tests-app/src/test-orders/add-test-order/add-test-order.workspace.tsx @@ -15,16 +15,19 @@ import { import { type DefaultPatientWorkspaceProps, type OrderBasketItem, - type LabOrderBasketItem, + type TestOrderBasketItem, launchPatientWorkspace, usePatientChartStore, + useOrderType, } from '@openmrs/esm-patient-common-lib'; -import { LabOrderForm } from './lab-order-form.component'; +import { LabOrderForm } from './test-order-form.component'; import { TestTypeSearch } from './test-type-search.component'; -import styles from './add-lab-order.scss'; +import styles from './add-test-order.scss'; export interface AddLabOrderWorkspaceAdditionalProps { order?: OrderBasketItem; + orderTypeUuid: string; + orderableConceptSets: Array; } export interface AddLabOrderWorkspace extends DefaultPatientWorkspaceProps, AddLabOrderWorkspaceAdditionalProps {} @@ -32,15 +35,18 @@ export interface AddLabOrderWorkspace extends DefaultPatientWorkspaceProps, AddL // Design: https://app.zeplin.io/project/60d5947dd636aebbd63dce4c/screen/640b06c440ee3f7af8747620 export default function AddLabOrderWorkspace({ order: initialOrder, + orderTypeUuid, + orderableConceptSets, closeWorkspace, closeWorkspaceWithSavedChanges, promptBeforeClosing, }: AddLabOrderWorkspace) { const { t } = useTranslation(); + const { orderType, isLoadingOrderType, errorFetchingOrderType } = useOrderType(orderTypeUuid); const isTablet = useLayoutType() === 'tablet'; const { patientUuid } = usePatientChartStore(); const { patient, isLoading: isLoadingPatient } = usePatient(patientUuid); - const [currentLabOrder, setCurrentLabOrder] = useState(initialOrder as LabOrderBasketItem); + const [currentLabOrder, setCurrentLabOrder] = useState(initialOrder as TestOrderBasketItem); const patientName = patient ? getPatientName(patient) : ''; @@ -83,9 +89,15 @@ export default function AddLabOrderWorkspace({ closeWorkspaceWithSavedChanges={closeWorkspaceWithSavedChanges} promptBeforeClosing={promptBeforeClosing} setTitle={() => {}} + orderTypeUuid={orderTypeUuid} + orderableConceptSets={orderableConceptSets} /> ) : ( - + )}
); diff --git a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/lab-order-form.component.tsx b/packages/esm-patient-tests-app/src/test-orders/add-test-order/test-order-form.component.tsx similarity index 92% rename from packages/esm-patient-tests-app/src/lab-orders/add-lab-order/lab-order-form.component.tsx rename to packages/esm-patient-tests-app/src/test-orders/add-test-order/test-order-form.component.tsx index e28b7ce6ab..6609513f9d 100644 --- a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/lab-order-form.component.tsx +++ b/packages/esm-patient-tests-app/src/test-orders/add-test-order/test-order-form.component.tsx @@ -1,10 +1,11 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import classNames from 'classnames'; import { + type TestOrderBasketItem, type DefaultPatientWorkspaceProps, - type LabOrderBasketItem, launchPatientWorkspace, useOrderBasket, + useOrderType, } from '@openmrs/esm-patient-common-lib'; import { ExtensionSlot, translateFrom, useConfig, useLayoutType, useSession } from '@openmrs/esm-framework'; import { prepLabOrderPostData, useOrderReasons } from '../api'; @@ -21,17 +22,18 @@ import { TextInput, } from '@carbon/react'; import { useTranslation } from 'react-i18next'; -import { ordersEqual, priorityOptions } from './lab-order'; -import { useTestTypes } from './useTestTypes'; +import { ordersEqual, priorityOptions } from './test-order'; import { Controller, type FieldErrors, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { moduleName } from '@openmrs/esm-patient-chart-app/src/constants'; import { type ConfigObject } from '../../config-schema'; -import styles from './lab-order-form.scss'; +import styles from './test-order-form.scss'; export interface LabOrderFormProps extends DefaultPatientWorkspaceProps { - initialOrder: LabOrderBasketItem; + initialOrder: TestOrderBasketItem; + orderTypeUuid: string; + orderableConceptSets: Array; } // Designs: @@ -42,15 +44,17 @@ export function LabOrderForm({ closeWorkspace, closeWorkspaceWithSavedChanges, promptBeforeClosing, + orderTypeUuid, + orderableConceptSets, }: LabOrderFormProps) { const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; const session = useSession(); const isEditing = useMemo(() => initialOrder && initialOrder.action === 'REVISE', [initialOrder]); - const { orders, setOrders } = useOrderBasket('labs', prepLabOrderPostData); - const { testTypes, isLoading: isLoadingTestTypes, error: errorLoadingTestTypes } = useTestTypes(); + const { orders, setOrders } = useOrderBasket(orderTypeUuid, prepLabOrderPostData); const [showErrorNotification, setShowErrorNotification] = useState(false); const config = useConfig(); + const { orderType, isLoadingOrderType } = useOrderType(orderTypeUuid); const orderReasonRequired = ( config.labTestsWithOrderReasons?.find((c) => c.labTestUuid === initialOrder?.testType?.conceptUuid) || {} ).required; @@ -92,7 +96,7 @@ export function LabOrderForm({ control, handleSubmit, formState: { errors, defaultValues, isDirty }, - } = useForm({ + } = useForm({ mode: 'all', resolver: zodResolver(labOrderFormSchema), defaultValues: { @@ -111,8 +115,8 @@ export function LabOrderForm({ }, []); const handleFormSubmission = useCallback( - (data: LabOrderBasketItem) => { - const finalizedOrder: LabOrderBasketItem = { + (data: TestOrderBasketItem) => { + const finalizedOrder: TestOrderBasketItem = { ...initialOrder, ...data, }; @@ -147,7 +151,7 @@ export function LabOrderForm({ }); }, [closeWorkspace, orders, setOrders, defaultValues]); - const onError = (errors: FieldErrors) => { + const onError = (errors: FieldErrors) => { if (errors) { setShowErrorNotification(true); } @@ -161,7 +165,7 @@ export function LabOrderForm({ return ( <> - {errorLoadingTestTypes && ( + {/* {errorLoadingTestTypes && ( - )} + )} */}
@@ -194,7 +198,9 @@ export function LabOrderForm({ id="labReferenceNumberInput" invalid={!!errors.accessionNumber} invalidText={errors.accessionNumber?.message} - labelText={t('labReferenceNumber', 'Lab reference number')} + labelText={t('testOrderReferenceNumber', '{{orderType}} reference number', { + orderType: orderType?.display, + })} maxLength={150} onBlur={onBlur} onChange={onChange} diff --git a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/lab-order-form.scss b/packages/esm-patient-tests-app/src/test-orders/add-test-order/test-order-form.scss similarity index 100% rename from packages/esm-patient-tests-app/src/lab-orders/add-lab-order/lab-order-form.scss rename to packages/esm-patient-tests-app/src/test-orders/add-test-order/test-order-form.scss diff --git a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/lab-order.ts b/packages/esm-patient-tests-app/src/test-orders/add-test-order/test-order.ts similarity index 70% rename from packages/esm-patient-tests-app/src/lab-orders/add-lab-order/lab-order.ts rename to packages/esm-patient-tests-app/src/test-orders/add-test-order/test-order.ts index 8286f90a87..1b208a562a 100644 --- a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/lab-order.ts +++ b/packages/esm-patient-tests-app/src/test-orders/add-test-order/test-order.ts @@ -1,7 +1,7 @@ -import { type LabOrderBasketItem, type OrderUrgency } from '@openmrs/esm-patient-common-lib'; -import { type TestType } from './useTestTypes'; +import { type TestOrderBasketItem, type OrderUrgency } from '@openmrs/esm-patient-common-lib'; +import { type OrderableConcept } from './useOrderableConceptSets'; -type LabOrderRequest = Pick; +type LabOrderRequest = Pick; type PriorityOption = { label: string; @@ -15,7 +15,7 @@ export const priorityOptions: PriorityOption[] = [ ]; // TODO add priority option `{ value: "ON_SCHEDULED_DATE", label: "On scheduled date" }` once the form supports a date. -export function createEmptyLabOrder(testType: TestType, orderer: string): LabOrderBasketItem { +export function createEmptyLabOrder(testType: OrderableConcept, orderer: string): TestOrderBasketItem { return { action: 'NEW', urgency: priorityOptions[0].value as OrderUrgency, diff --git a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/test-type-search.component.tsx b/packages/esm-patient-tests-app/src/test-orders/add-test-order/test-type-search.component.tsx similarity index 80% rename from packages/esm-patient-tests-app/src/lab-orders/add-lab-order/test-type-search.component.tsx rename to packages/esm-patient-tests-app/src/test-orders/add-test-order/test-type-search.component.tsx index 867822b52c..32e6fa18af 100644 --- a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/test-type-search.component.tsx +++ b/packages/esm-patient-tests-app/src/test-orders/add-test-order/test-type-search.component.tsx @@ -12,30 +12,36 @@ import { useSession, ResponsiveWrapper, } from '@openmrs/esm-framework'; -import { type LabOrderBasketItem, launchPatientWorkspace, useOrderBasket } from '@openmrs/esm-patient-common-lib'; -import { type TestType, useTestTypes } from './useTestTypes'; +import { + type TestOrderBasketItem, + launchPatientWorkspace, + useOrderBasket, + useOrderType, +} from '@openmrs/esm-patient-common-lib'; +import { type OrderableConcept, useOrderableConcepts } from './useOrderableConceptSets'; import { prepLabOrderPostData } from '../api'; -import { createEmptyLabOrder } from './lab-order'; +import { createEmptyLabOrder } from './test-order'; import styles from './test-type-search.scss'; -interface TestTypeSearchResultsProps { +export interface TestTypeSearchProps { + openLabForm: (searchResult: TestOrderBasketItem) => void; + orderTypeUuid: string; + orderableConceptSets: Array; +} + +interface TestTypeSearchResultsProps extends TestTypeSearchProps { cancelOrder: () => void; searchTerm: string; - openOrderForm: (searchResult: LabOrderBasketItem) => void; focusAndClearSearchInput: () => void; } interface TestTypeSearchResultItemProps { - t: TFunction; - testType: TestType; - openOrderForm: (searchResult: LabOrderBasketItem) => void; -} - -export interface TestTypeSearchProps { - openLabForm: (searchResult: LabOrderBasketItem) => void; + orderTypeUuid: string; + testType: OrderableConcept; + openOrderForm: (searchResult: TestOrderBasketItem) => void; } -export function TestTypeSearch({ openLabForm }: TestTypeSearchProps) { +export function TestTypeSearch({ openLabForm, orderTypeUuid, orderableConceptSets }: TestTypeSearchProps) { const { t } = useTranslation(); const [searchTerm, setSearchTerm] = useState(''); const debouncedSearchTerm = useDebounce(searchTerm); @@ -72,8 +78,10 @@ export function TestTypeSearch({ openLabForm }: TestTypeSearchProps) { @@ -83,26 +91,21 @@ export function TestTypeSearch({ openLabForm }: TestTypeSearchProps) { function TestTypeSearchResults({ cancelOrder, searchTerm, - openOrderForm, + orderTypeUuid, + orderableConceptSets, + openLabForm, focusAndClearSearchInput, }: TestTypeSearchResultsProps) { const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; - const { testTypes, isLoading, error } = useTestTypes(); - - const filteredTestTypes = useMemo(() => { - if (!searchTerm) { - return testTypes; - } - - if (searchTerm && searchTerm.trim() !== '') { - return testTypes?.filter((testType) => - testType.synonyms.some((name) => name.toLowerCase().includes(searchTerm.toLowerCase())), - ); - } - }, [searchTerm, testTypes]); + const { orderType, isLoadingOrderType, errorFetchingOrderType } = useOrderType(orderTypeUuid); + const { orderableConcepts, isLoading, error } = useOrderableConcepts( + searchTerm, + orderType?.conceptClasses?.[0]?.uuid, + orderableConceptSets, + ); - if (isLoading) { + if (isLoadingOrderType || isLoading) { return ; } @@ -123,7 +126,7 @@ function TestTypeSearchResults({ ); } - if (filteredTestTypes?.length) { + if (orderableConcepts?.length) { return ( <>
@@ -131,7 +134,7 @@ function TestTypeSearchResults({
{t('searchResultsMatchesForTerm', '{{count}} results for "{{searchTerm}}"', { - count: filteredTestTypes?.length, + count: orderableConcepts?.length, searchTerm, })} @@ -141,12 +144,12 @@ function TestTypeSearchResults({
)}
- {filteredTestTypes.map((testType) => ( + {orderableConcepts.map((orderableConcept) => ( ))}
@@ -183,10 +186,15 @@ function TestTypeSearchResults({ ); } -const TestTypeSearchResultItem: React.FC = ({ t, testType, openOrderForm }) => { +const TestTypeSearchResultItem: React.FC = ({ + testType, + openOrderForm, + orderTypeUuid, +}) => { + const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; const session = useSession(); - const { orders, setOrders } = useOrderBasket('labs', prepLabOrderPostData); + const { orders, setOrders } = useOrderBasket(orderTypeUuid, prepLabOrderPostData); const testTypeAlreadyInBasket = useMemo( () => orders?.some((order) => order.testType.conceptUuid === testType.conceptUuid), @@ -194,8 +202,8 @@ const TestTypeSearchResultItem: React.FC = ({ t, ); const createLabOrder = useCallback( - (testType: TestType) => { - return createEmptyLabOrder(testType, session.currentProvider?.uuid); + (orderableConcept: OrderableConcept) => { + return createEmptyLabOrder(orderableConcept, session.currentProvider?.uuid); }, [session.currentProvider.uuid], ); diff --git a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/test-type-search.scss b/packages/esm-patient-tests-app/src/test-orders/add-test-order/test-type-search.scss similarity index 100% rename from packages/esm-patient-tests-app/src/lab-orders/add-lab-order/test-type-search.scss rename to packages/esm-patient-tests-app/src/test-orders/add-test-order/test-type-search.scss diff --git a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/useTestTypes.ts b/packages/esm-patient-tests-app/src/test-orders/add-test-order/useOrderableConceptSets.ts similarity index 53% rename from packages/esm-patient-tests-app/src/lab-orders/add-lab-order/useTestTypes.ts rename to packages/esm-patient-tests-app/src/test-orders/add-test-order/useOrderableConceptSets.ts index 604e82fab5..d9c33dff9d 100644 --- a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/useTestTypes.ts +++ b/packages/esm-patient-tests-app/src/test-orders/add-test-order/useOrderableConceptSets.ts @@ -7,14 +7,14 @@ import { type ConfigObject } from '../../config-schema'; type ConceptResult = FetchResponse; type ConceptResults = FetchResponse<{ results: Array }>; -export interface TestType { +export interface OrderableConcept { label: string; conceptUuid: string; synonyms: string[]; } export interface UseTestType { - testTypes: Array; + orderableConcepts: Array; isLoading: boolean; error: Error; } @@ -26,16 +26,22 @@ function openmrsFetchMultiple(urls: Array) { return Promise.all(urls.map((url) => openmrsFetch<{ results: Array }>(url))); } -function useTestConceptsSWR(labOrderableConcepts?: Array) { +function useOrderableConceptSetsSWR( + searchTerm: string, + orderableConcepts?: Array, + conceptClassUuid: string = 'Test', +) { const { data, isLoading, error } = useSWRImmutable( () => - labOrderableConcepts - ? labOrderableConcepts.map( - (c) => - `${restBaseUrl}/concept/${c}?v=custom:(display,names:(display),uuid,setMembers:(display,uuid,names:(display),setMembers:(display,uuid,names:(display))))`, - ) - : `${restBaseUrl}/concept?class=Test?v=custom:(display,names:(display),uuid,setMembers:(display,uuid,names:(display),setMembers:(display,uuid,names:(display))))`, - (labOrderableConcepts ? openmrsFetchMultiple : openmrsFetch) as any, + orderableConcepts || conceptClassUuid + ? orderableConcepts + ? orderableConcepts.map( + (c) => + `${restBaseUrl}/concept/${c}?v=custom:(display,names:(display),uuid,setMembers:(display,uuid,names:(display),setMembers:(display,uuid,names:(display))))`, + ) + : `${restBaseUrl}/concept?class=${conceptClassUuid}&searchType=fuzzy&name=${searchTerm}&v=custom:(display,names:(display),uuid,setMembers:(display,uuid,names:(display),setMembers:(display,uuid,names:(display))))` + : null, + (orderableConcepts ? openmrsFetchMultiple : openmrsFetch) as any, { shouldRetryOnError(err) { return err instanceof Response; @@ -45,10 +51,18 @@ function useTestConceptsSWR(labOrderableConcepts?: Array) { const results = useMemo(() => { if (isLoading || error) return null; - return labOrderableConcepts - ? (data as Array)?.flatMap((d) => d.data.setMembers) - : (data as ConceptResults)?.data.results ?? ([] as Concept[]); - }, [data, isLoading, error, labOrderableConcepts]); + if (orderableConcepts) { + const concepts = (data as Array)?.flatMap((d) => d.data.setMembers); + if (searchTerm) { + return concepts?.filter((concept) => + concept.names.some((name) => name.display.toLowerCase().includes(searchTerm.toLowerCase())), + ); + } + return concepts; + } else { + return (data as ConceptResults)?.data.results ?? ([] as Concept[]); + } + }, [isLoading, error, orderableConcepts, data, searchTerm]); return { data: results, @@ -57,9 +71,16 @@ function useTestConceptsSWR(labOrderableConcepts?: Array) { }; } -export function useTestTypes(): UseTestType { - const { labOrderableConcepts } = useConfig().orders; - const { data, isLoading, error } = useTestConceptsSWR(labOrderableConcepts.length ? labOrderableConcepts : null); +export function useOrderableConcepts( + searchTerm: string, + conceptClassUuid: string, + orderableConceptSets: Array, +): UseTestType { + const { data, isLoading, error } = useOrderableConceptSetsSWR( + searchTerm, + orderableConceptSets.length ? orderableConceptSets : null, + conceptClassUuid, + ); useEffect(() => { if (error) { @@ -67,7 +88,7 @@ export function useTestTypes(): UseTestType { } }, [error]); - const testConcepts = useMemo( + const orderableConcepts = useMemo( () => data ?.map((concept) => ({ @@ -81,7 +102,7 @@ export function useTestTypes(): UseTestType { ); return { - testTypes: testConcepts, + orderableConcepts, isLoading: isLoading, error: error, }; diff --git a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/useTestTypes.test.ts b/packages/esm-patient-tests-app/src/test-orders/add-test-order/useTestTypes.test.ts similarity index 86% rename from packages/esm-patient-tests-app/src/lab-orders/add-lab-order/useTestTypes.test.ts rename to packages/esm-patient-tests-app/src/test-orders/add-test-order/useTestTypes.test.ts index 1769cf2ac9..346ef601b1 100644 --- a/packages/esm-patient-tests-app/src/lab-orders/add-lab-order/useTestTypes.test.ts +++ b/packages/esm-patient-tests-app/src/test-orders/add-test-order/useTestTypes.test.ts @@ -3,7 +3,7 @@ import useSWRImmutable from 'swr/immutable'; import { renderHook, waitFor } from '@testing-library/react'; import { getDefaultsFromConfigSchema, openmrsFetch, restBaseUrl, useConfig } from '@openmrs/esm-framework'; import { type ConfigObject, configSchema } from '../../config-schema'; -import { useTestTypes } from './useTestTypes'; +import { useOrderableConcepts } from './useOrderableConceptSets'; jest.mock('swr/immutable'); @@ -41,17 +41,17 @@ mockOpenrsFetch.mockImplementation((url: string) => { describe('useTestTypes is configurable', () => { it('should return all test concepts when no labOrderableConcepts are provided', async () => { - const { result } = renderHook(() => useTestTypes()); + const { result } = renderHook(() => useOrderableConcepts('', '', [])); expect(mockOpenrsFetch).toHaveBeenCalledWith( `${restBaseUrl}/concept?class=Test?v=custom:(display,names:(display),uuid,setMembers:(display,uuid,names:(display),setMembers:(display,uuid,names:(display))))`, ); await waitFor(() => expect(result.current.isLoading).toBeFalsy()); expect(result.current.error).toBeFalsy(); - expect(result.current.testTypes).toEqual([expect.objectContaining({ label: 'Test concept' })]); + expect(result.current.orderableConcepts).toEqual([expect.objectContaining({ label: 'Test concept' })]); }); it('should return children of labOrderableConcepts when provided', async () => { - const { result } = renderHook(() => useTestTypes()); + const { result } = renderHook(() => useOrderableConcepts('', '', [])); expect(mockOpenrsFetch).toHaveBeenCalledWith( expect.stringContaining( `${restBaseUrl}/concept?class=Test?v=custom:(display,names:(display),uuid,setMembers:(display,uuid,names:(display),setMembers:(display,uuid,names:(display))))`, @@ -59,7 +59,7 @@ describe('useTestTypes is configurable', () => { ); await waitFor(() => expect(result.current.isLoading).toBeFalsy()); expect(result.current.error).toBeFalsy(); - expect(result.current.testTypes).toEqual([ + expect(result.current.orderableConcepts).toEqual([ expect.objectContaining({ conceptUuid: undefined, label: 'Test concept' }), ]); }); diff --git a/packages/esm-patient-tests-app/src/lab-orders/api.ts b/packages/esm-patient-tests-app/src/test-orders/api.ts similarity index 96% rename from packages/esm-patient-tests-app/src/lab-orders/api.ts rename to packages/esm-patient-tests-app/src/test-orders/api.ts index 504c148baa..4b8c13420c 100644 --- a/packages/esm-patient-tests-app/src/lab-orders/api.ts +++ b/packages/esm-patient-tests-app/src/test-orders/api.ts @@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react'; import { chunk } from 'lodash-es'; import useSWR, { mutate } from 'swr'; import useSWRImmutable from 'swr/immutable'; -import type { LabOrderBasketItem, OrderPost, PatientOrderFetchResponse } from '@openmrs/esm-patient-common-lib'; +import type { TestOrderBasketItem, OrderPost, PatientOrderFetchResponse } from '@openmrs/esm-patient-common-lib'; import { type FetchResponse, openmrsFetch, restBaseUrl, showSnackbar, useConfig } from '@openmrs/esm-framework'; import { type ConfigObject } from '../config-schema'; @@ -77,7 +77,7 @@ function getConceptReferenceUrls(conceptUuids: Array) { } export function prepLabOrderPostData( - order: LabOrderBasketItem, + order: TestOrderBasketItem, patientUuid: string, encounterUuid: string | null, ): OrderPost { @@ -130,7 +130,7 @@ export function prepLabOrderPostData( } export type PostDataPrepLabOrderFunction = ( - order: LabOrderBasketItem, + order: TestOrderBasketItem, patientUuid: string, encounterUuid: string, ) => OrderPost; diff --git a/packages/esm-patient-tests-app/src/lab-orders/lab-order-basket-panel/lab-icon.component.tsx b/packages/esm-patient-tests-app/src/test-orders/lab-order-basket-panel/lab-icon.component.tsx similarity index 100% rename from packages/esm-patient-tests-app/src/lab-orders/lab-order-basket-panel/lab-icon.component.tsx rename to packages/esm-patient-tests-app/src/test-orders/lab-order-basket-panel/lab-icon.component.tsx diff --git a/packages/esm-patient-tests-app/src/lab-orders/lab-order-basket-panel/lab-order-basket-item-tile.component.tsx b/packages/esm-patient-tests-app/src/test-orders/lab-order-basket-panel/lab-order-basket-item-tile.component.tsx similarity index 96% rename from packages/esm-patient-tests-app/src/lab-orders/lab-order-basket-panel/lab-order-basket-item-tile.component.tsx rename to packages/esm-patient-tests-app/src/test-orders/lab-order-basket-panel/lab-order-basket-item-tile.component.tsx index bb0e9d2e89..9e3d4acb7b 100644 --- a/packages/esm-patient-tests-app/src/lab-orders/lab-order-basket-panel/lab-order-basket-item-tile.component.tsx +++ b/packages/esm-patient-tests-app/src/test-orders/lab-order-basket-panel/lab-order-basket-item-tile.component.tsx @@ -3,11 +3,11 @@ import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { Button, ClickableTile, Tile } from '@carbon/react'; import { TrashCanIcon, useLayoutType, WarningIcon } from '@openmrs/esm-framework'; -import { type LabOrderBasketItem } from '@openmrs/esm-patient-common-lib'; +import { type TestOrderBasketItem } from '@openmrs/esm-patient-common-lib'; import styles from './lab-order-basket-item-tile.scss'; export interface OrderBasketItemTileProps { - orderBasketItem: LabOrderBasketItem; + orderBasketItem: TestOrderBasketItem; onItemClick: () => void; onRemoveClick: () => void; } @@ -75,7 +75,7 @@ export function LabOrderBasketItemTile({ orderBasketItem, onItemClick, onRemoveC ); } -function OrderActionLabel({ orderBasketItem }: { orderBasketItem: LabOrderBasketItem }) { +function OrderActionLabel({ orderBasketItem }: { orderBasketItem: TestOrderBasketItem }) { const { t } = useTranslation(); if (orderBasketItem.isOrderIncomplete) { diff --git a/packages/esm-patient-tests-app/src/lab-orders/lab-order-basket-panel/lab-order-basket-item-tile.scss b/packages/esm-patient-tests-app/src/test-orders/lab-order-basket-panel/lab-order-basket-item-tile.scss similarity index 100% rename from packages/esm-patient-tests-app/src/lab-orders/lab-order-basket-panel/lab-order-basket-item-tile.scss rename to packages/esm-patient-tests-app/src/test-orders/lab-order-basket-panel/lab-order-basket-item-tile.scss diff --git a/packages/esm-patient-tests-app/src/lab-orders/lab-order-basket-panel/lab-order-basket-panel.extension.tsx b/packages/esm-patient-tests-app/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.extension.tsx similarity index 76% rename from packages/esm-patient-tests-app/src/lab-orders/lab-order-basket-panel/lab-order-basket-panel.extension.tsx rename to packages/esm-patient-tests-app/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.extension.tsx index 56ff1bbc0f..e893b16a06 100644 --- a/packages/esm-patient-tests-app/src/lab-orders/lab-order-basket-panel/lab-order-basket-panel.extension.tsx +++ b/packages/esm-patient-tests-app/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.extension.tsx @@ -2,25 +2,58 @@ import React, { type ComponentProps, useCallback, useEffect, useMemo, useState } import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { Button, Tile } from '@carbon/react'; -import { AddIcon, closeWorkspace, ChevronDownIcon, ChevronUpIcon, useLayoutType } from '@openmrs/esm-framework'; +import { + AddIcon, + closeWorkspace, + ChevronDownIcon, + ChevronUpIcon, + useLayoutType, + useConfig, +} from '@openmrs/esm-framework'; import { launchPatientWorkspace, type OrderBasketItem, useOrderBasket, - type LabOrderBasketItem, + type TestOrderBasketItem, + useOrderType, } from '@openmrs/esm-patient-common-lib'; import { LabOrderBasketItemTile } from './lab-order-basket-item-tile.component'; import { prepLabOrderPostData } from '../api'; import LabIcon from './lab-icon.component'; import styles from './lab-order-basket-panel.scss'; +import type { ConfigObject } from '../../config-schema'; /** * Designs: https://app.zeplin.io/project/60d59321e8100b0324762e05/screen/648c44d9d4052c613e7f23da */ export default function LabOrderBasketPanelExtension() { + const { orders, additionalOrderTypes } = useConfig(); + const allOrderTypes: ConfigObject['additionalOrderTypes'] = [ + { + orderTypeUuid: orders.labOrderTypeUuid, + orderableConceptSets: orders.labOrderableConcepts, + }, + ...additionalOrderTypes, + ]; + return ( + <> + {allOrderTypes.map(({ orderTypeUuid, orderableConceptSets }) => ( + + ))} + + ); +} + +interface LabOrderBasketPanelProps { + orderTypeUuid: string; + orderableConceptSets: Array; +} + +function LabOrderBasketPanel({ orderTypeUuid, orderableConceptSets }: LabOrderBasketPanelProps) { const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; - const { orders, setOrders } = useOrderBasket('labs', prepLabOrderPostData); + const { orderType, isLoadingOrderType } = useOrderType(orderTypeUuid); + const { orders, setOrders } = useOrderBasket(orderTypeUuid, prepLabOrderPostData); const [isExpanded, setIsExpanded] = useState(orders.length > 0); const { incompleteOrderBasketItems, @@ -29,11 +62,11 @@ export default function LabOrderBasketPanelExtension() { revisedOrderBasketItems, discontinuedOrderBasketItems, } = useMemo(() => { - const incompleteOrderBasketItems: Array = []; - const newOrderBasketItems: Array = []; - const renewedOrderBasketItems: Array = []; - const revisedOrderBasketItems: Array = []; - const discontinuedOrderBasketItems: Array = []; + const incompleteOrderBasketItems: Array = []; + const newOrderBasketItems: Array = []; + const renewedOrderBasketItems: Array = []; + const revisedOrderBasketItems: Array = []; + const discontinuedOrderBasketItems: Array = []; orders.forEach((order) => { if (order?.isOrderIncomplete) { @@ -61,9 +94,13 @@ export default function LabOrderBasketPanelExtension() { const openNewLabForm = useCallback(() => { closeWorkspace('order-basket', { ignoreChanges: true, - onWorkspaceClose: () => launchPatientWorkspace('add-lab-order'), + onWorkspaceClose: () => + launchPatientWorkspace('add-lab-order', { + orderTypeUuid: orderTypeUuid, + orderableConceptSets: orderableConceptSets, + }), }); - }, []); + }, [orderTypeUuid, orderableConceptSets]); const openEditLabForm = useCallback((order: OrderBasketItem) => { closeWorkspace('order-basket', { @@ -73,7 +110,7 @@ export default function LabOrderBasketPanelExtension() { }, []); const removeLabOrder = useCallback( - (order: LabOrderBasketItem) => { + (order: TestOrderBasketItem) => { const newOrders = [...orders]; newOrders.splice(orders.indexOf(order), 1); setOrders(newOrders); @@ -85,6 +122,10 @@ export default function LabOrderBasketPanelExtension() { setIsExpanded(orders.length > 0); }, [orders]); + if (isLoadingOrderType) { + return null; + } + return (
-

{`${t('labOrders', 'Lab orders')} (${orders.length})`}

+

{`${orderType?.display} (${orders.length})`}