Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) O3-4009: Display stock and price information upon ordering #2028

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fc2a2d6
(feat) add price and stock details on order items
usamaidrsk Sep 21, 2024
8f63916
(chore) translations
usamaidrsk Sep 21, 2024
f36b7c3
mock requests
usamaidrsk Sep 23, 2024
6de143b
(fix): reset addLabOrderWorkspace width
usamaidrsk Sep 23, 2024
748a827
(feat) update design
usamaidrsk Sep 23, 2024
c01a6e2
(chore) extract translations
usamaidrsk Sep 23, 2024
6e23b2c
(fix) fix component styles
usamaidrsk Sep 23, 2024
120b8f2
(fix) remove price and stock on labsa and drugs search items
usamaidrsk Sep 24, 2024
50c10d7
(fix) extract translations
usamaidrsk Sep 24, 2024
1be6482
(fix) memoize slot state
usamaidrsk Sep 24, 2024
faf8392
(fix) memoize price data
usamaidrsk Sep 24, 2024
88163fd
(fix) memoize price data
usamaidrsk Sep 24, 2024
ee5766e
(fix) fix import icons from framework
usamaidrsk Sep 25, 2024
29c131b
feat: add backed dependencies
usamaidrsk Oct 25, 2024
86aa3fe
chore: add unit tests
usamaidrsk Nov 1, 2024
8f32101
address pr reviews
usamaidrsk Nov 6, 2024
8e1269e
check if needed modules are installed
usamaidrsk Nov 6, 2024
c8bed8c
address pr reviews
usamaidrsk Nov 6, 2024
ae366cd
fix tests
usamaidrsk Nov 6, 2024
8a2d02e
fix tests
usamaidrsk Nov 6, 2024
17feaf3
(fix) address reviews, rename useIsBackendModuleInstalled.tsx to useA…
usamaidrsk Nov 7, 2024
f1a3272
fix: remover unnecessary clear mocks
usamaidrsk Nov 7, 2024
b7e6c64
Merge branch 'main' into ft-O3-4009
usamaidrsk Nov 18, 2024
b3596bc
(chore) Bump RFE version (#2110)
pirupius Nov 19, 2024
c6b8ad2
(chore) Bump form engine version (#2112)
samuelmale Nov 19, 2024
ecdaee3
(chore) Update translations from Transifex (#2108)
github-actions[bot] Nov 20, 2024
94e956b
(feat) O3-4208: Add Configuration to Display Lab Reference Number Inp…
usamaidrsk Nov 21, 2024
c91ed0f
add optionalBackendDependencies
usamaidrsk Nov 18, 2024
6b48346
Merge branch 'main' into ft-O3-4009
usamaidrsk Nov 21, 2024
1535c4d
Merge branch 'main' into ft-O3-4009
usamaidrsk Nov 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions __mocks__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export * from './location.mock';
export * from './medication.mock';
export * from './mockBasePanel.mock';
export * from './mockDeceasedPatient';
export * from './order-stock-data.mock';
export * from './order-price-data.mock';
export * from './patient-flags.mock';
export * from './programs.mock';
export * from './relationships.mock';
Expand Down
40 changes: 40 additions & 0 deletions __mocks__/order-price-data.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { OrderPriceData } from '@openmrs/esm-patient-orders-app/src/types/order';

export const mockOrderPriceData: OrderPriceData = {
resourceType: 'Bundle',
id: 'test-id',
meta: {
lastUpdated: '2024-01-01T00:00:00Z',
},
type: 'searchset',
link: [
{
relation: 'self',
url: 'test-url',
},
],
entry: [
{
resource: {
resourceType: 'ChargeItemDefinition',
id: 'test-resource-id',
name: 'Test Item',
status: 'active',
date: '2024-01-01',
propertyGroup: [
{
priceComponent: [
{
type: 'base',
amount: {
value: 99.99,
currency: 'USD',
},
},
],
},
],
},
},
],
};
46 changes: 46 additions & 0 deletions __mocks__/order-stock-data.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export const mockOrderStockData = {
resourceType: 'Bundle',
id: 'test-id',
meta: {
lastUpdated: '2024-01-01T00:00:00Z',
},
type: 'searchset',
link: [
{
relation: 'self',
url: 'test-url',
},
],
entry: [
{
resource: {
resourceType: 'InventoryItem',
id: 'test-resource-id',
meta: {
profile: ['test-profile'],
},
status: 'active',
code: [
{
coding: [
{
system: 'test-system',
code: 'test-code',
display: 'Test Item',
},
],
},
],
name: [
{
name: 'Test Item',
},
],
netContent: {
value: 10,
unit: 'units',
},
},
},
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
flex-direction: column;
justify-content: space-between;
border: 1px solid $grey-2;

&:not(:last-of-type) {
margin-bottom: layout.$spacing-03;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { type ComponentProps, useRef } from 'react';
import React, { useMemo, useRef } from 'react';
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 { ClickableTile, IconButton, Tile } from '@carbon/react';
import { ExtensionSlot, TrashCanIcon, useLayoutType, WarningIcon } from '@openmrs/esm-framework';
import { type DrugOrderBasketItem } from '../types';
import styles from './order-basket-item-tile.scss';

Expand All @@ -24,70 +24,84 @@ export default function OrderBasketItemTile({ orderBasketItem, onItemClick, onRe
// Hence, we manually prevent the handleClick callback from being invoked as soon as the button is pressed once.
const shouldOnClickBeCalled = useRef(true);

const additionalInfoSlotState = useMemo(
() => ({
orderItemUuid: orderBasketItem.drug.uuid,
}),
[orderBasketItem],
);

const tileContent = (
<div className={styles.orderBasketItemTile}>
<div className={styles.clipTextWithEllipsis}>
<OrderActionLabel orderBasketItem={orderBasketItem} />
{orderBasketItem.isFreeTextDosage ? (
<div>
<span className={styles.drugName}>{orderBasketItem.drug?.display}</span>
{orderBasketItem.freeTextDosage && (
<span className={styles.dosageInfo}> &mdash; {orderBasketItem.freeTextDosage}</span>
)}
</div>
) : (
<div>
<span className={styles.drugName}>{orderBasketItem.drug?.display}</span>
<div>
<div className={styles.orderBasketItemTile}>
<div className={styles.clipTextWithEllipsis}>
<OrderActionLabel orderBasketItem={orderBasketItem} />
{orderBasketItem.isFreeTextDosage ? (
<div>
<span className={styles.drugName}>{orderBasketItem.drug?.display}</span>
{orderBasketItem.freeTextDosage && (
<span className={styles.dosageInfo}> &mdash; {orderBasketItem.freeTextDosage}</span>
)}
</div>
) : (
<div>
<span className={styles.drugName}>{orderBasketItem.drug?.display}</span>
<span className={styles.dosageInfo}>
{' '}
{orderBasketItem.drug?.strength && <>&mdash; {orderBasketItem.drug?.strength}</>}{' '}
{orderBasketItem.drug?.dosageForm?.display && <>&mdash; {orderBasketItem.drug.dosageForm?.display}</>}
</span>
</div>
)}
<span className={styles.label01}>
<span className={styles.doseCaption}>{t('dose', 'Dose').toUpperCase()}</span>{' '}
<span className={styles.dosageLabel}>
{orderBasketItem.dosage} {orderBasketItem.unit?.value}
</span>{' '}
<span className={styles.dosageInfo}>
{' '}
{orderBasketItem.drug?.strength && <>&mdash; {orderBasketItem.drug?.strength}</>}{' '}
{orderBasketItem.drug?.dosageForm?.display && <>&mdash; {orderBasketItem.drug.dosageForm?.display}</>}
&mdash; {orderBasketItem.route?.value ? <>{orderBasketItem.route.value} &mdash; </> : null}
{orderBasketItem.frequency?.value ? <>{orderBasketItem.frequency.value} &mdash; </> : null}
{t('refills', 'Refills').toUpperCase()} {orderBasketItem.numRefills}{' '}
{t('quantity', 'Quantity').toUpperCase()}{' '}
{`${orderBasketItem.pillsDispensed} ${orderBasketItem.quantityUnits?.value?.toLowerCase() ?? ''}`}
{orderBasketItem.patientInstructions && <>&mdash; {orderBasketItem.patientInstructions}</>}
</span>
</div>
)}
<span className={styles.label01}>
<span className={styles.doseCaption}>{t('dose', 'Dose').toUpperCase()}</span>{' '}
<span className={styles.dosageLabel}>
{orderBasketItem.dosage} {orderBasketItem.unit?.value}
</span>{' '}
<span className={styles.dosageInfo}>
&mdash; {orderBasketItem.route?.value ? <>{orderBasketItem.route.value} &mdash; </> : null}
{orderBasketItem.frequency?.value ? <>{orderBasketItem.frequency.value} &mdash; </> : null}
{t('refills', 'Refills').toUpperCase()} {orderBasketItem.numRefills}{' '}
{t('quantity', 'Quantity').toUpperCase()}{' '}
{`${orderBasketItem.pillsDispensed} ${orderBasketItem.quantityUnits?.value?.toLowerCase() ?? ''}`}
{orderBasketItem.patientInstructions && <>&mdash; {orderBasketItem.patientInstructions}</>}
</span>
</span>
<br />
<span className={styles.label01}>
<span className={styles.indicationLabel}>{t('indication', 'Indication').toUpperCase()}</span>{' '}
<span className={styles.dosageInfo}>
{!!orderBasketItem.indication ? orderBasketItem.indication : <i>{t('none', 'None')}</i>}
<br />
<span className={styles.label01}>
<span className={styles.indicationLabel}>{t('indication', 'Indication').toUpperCase()}</span>{' '}
<span className={styles.dosageInfo}>
{!!orderBasketItem.indication ? orderBasketItem.indication : <i>{t('none', 'None')}</i>}
</span>
{!!orderBasketItem.orderError && (
<>
<br />
<span className={styles.orderErrorText}>
<WarningIcon size={16} /> &nbsp;{' '}
<span className={styles.label01}>{t('error', 'Error').toUpperCase()}</span> &nbsp;
{orderBasketItem.orderError.responseBody?.error?.message ?? orderBasketItem.orderError.message}
</span>
</>
)}
</span>
{!!orderBasketItem.orderError && (
<>
<br />
<span className={styles.orderErrorText}>
<WarningIcon size={16} /> &nbsp;{' '}
<span className={styles.label01}>{t('error', 'Error').toUpperCase()}</span> &nbsp;
{orderBasketItem.orderError.responseBody?.error?.message ?? orderBasketItem.orderError.message}
</span>
</>
)}
</span>
</div>
<IconButton
kind="ghost"
align="left"
size={isTablet ? 'lg' : 'sm'}
label={t('removeFromBasket', 'Remove from basket')}
onClick={() => {
shouldOnClickBeCalled.current = false;
onRemoveClick();
}}
>
<TrashCanIcon size={16} className={styles.removeButton} />
</IconButton>
</div>
<Button
className={styles.removeButton}
kind="ghost"
hasIconOnly={true}
renderIcon={(props: ComponentProps<typeof TrashCanIcon>) => <TrashCanIcon size={16} {...props} />}
iconDescription={t('removeFromBasket', 'Remove from basket')}
onClick={() => {
shouldOnClickBeCalled.current = false;
onRemoveClick();
}}
tooltipPosition="left"
<ExtensionSlot
name="order-item-additional-info-slot"
state={additionalInfoSlotState}
className={styles.additionalInfoContainer}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@
}

.removeButton {
svg {
fill: $danger;
}
fill: $danger;
}

.label01 {
Expand All @@ -99,3 +97,16 @@
overflow: hidden;
white-space: nowrap;
}

.additionalInfoContainer {
padding: layout.$spacing-05 0;
display: flex;
flex-flow: row;
justify-content: flex-start;
align-items: center;
gap: layout.$spacing-05;
}

.additionalInfoContainer:not(:has(div:not(:empty))) {
padding: 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { useMemo } from 'react';
import { useOrderPrice } from '../hooks/useOrderPrice';
import styles from './order-price-details.scss';
import { SkeletonText, Tooltip } from '@carbon/react';
import { useTranslation } from 'react-i18next';
import { getLocale, InformationIcon } from '@openmrs/esm-framework';

interface OrderPriceDetailsComponentProps {
orderItemUuid: string;
}

const OrderPriceDetailsComponent: React.FC<OrderPriceDetailsComponentProps> = ({ orderItemUuid }) => {
const { t } = useTranslation();
const locale = getLocale();
const { data: priceData, isLoading, error } = useOrderPrice(orderItemUuid);

const amount = useMemo(() => {
if (!priceData || priceData.entry.length === 0) {
return null;
}
return priceData.entry[0].resource.propertyGroup[0]?.priceComponent[0]?.amount;
}, [priceData]);

const formattedPrice = useMemo((): string => {
if (!amount) return '';
try {
new Intl.NumberFormat(locale, {
style: 'currency',
currency: amount.currency,
});

return new Intl.NumberFormat(locale, {
style: 'currency',
currency: amount.currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount.value);
} catch (error) {
console.error(`Invalid currency code: ${amount.currency}. Error: ${error.message}`);
return `${new Intl.NumberFormat(locale, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount.value)} ${amount.currency}`;
}
}, [locale, amount]);

if (isLoading) {
return <SkeletonText width="100px" role="progressbar" />;
}

if (!priceData || !amount || error) {
return null;
}

return (
<div className={styles.priceDetailsContainer}>
<span className={styles.priceLabel}>{t('price', 'Price')}:</span>
{formattedPrice}
<Tooltip
align="bottom-left"
className={styles.priceToolTip}
label={t(
'priceDisclaimer',
'This price is indicative and may not reflect final costs, which could vary due to discounts, insurance coverage, or other pricing rules',
)}
>
<button className={styles.priceToolTipTrigger} type="button">
<InformationIcon size={16} />
</button>
</Tooltip>
</div>
);
};

export default OrderPriceDetailsComponent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@use '@carbon/type';
@use '@carbon/layout';

.priceDetailsContainer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}

.priceLabel {
@include type.type-style('heading-compact-01');
padding-inline-end: layout.$spacing-02;
}

.priceToolTip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

.priceToolTipTrigger {
display: flex;
padding: 0 0 0 layout.$spacing-02;
border: none;
outline: none;
}
Loading