Skip to content

Commit

Permalink
(feat) O3-4346: Order basket improvements (#4)
Browse files Browse the repository at this point in the history
* Keep the theme color same for all order types

* Workspace title must be same as the order type

* Update the framework and patient common lib

* Make the showing/hiding reference number field configurable

* The reference fields and instructions fields should be nullable

* Replace the priority combobox with select

* Support scheduled date for medical supply orders

* Closing and opening new workspace should not close workspaceGroup
  • Loading branch information
vasharma05 authored Jan 24, 2025
1 parent 45e41aa commit 0993c50
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 241 deletions.
7 changes: 7 additions & 0 deletions src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export const configSchema = {
},
],
},
showReferenceNumberField: {
_type: Type.Boolean,
_default: true,
_description:
'Whether to display the Reference number field in the Order form. This field maps to the accesion_number property in the Order data model',
},
quantityUnits: {
_type: Type.Object,
_description: 'Concept to be used for fetching quantity units',
Expand Down Expand Up @@ -53,6 +59,7 @@ export type ConfigObject = {
orderTypeUuid: string;
orderableConceptSets: Array<string>;
}>;
showReferenceNumberField: boolean;
quantityUnits: {
conceptUuid: string;
map: 'answers' | 'setMembers';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { type ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
import classNames from 'classnames';
import {
type DefaultPatientWorkspaceProps,
launchPatientWorkspace,
useOrderBasket,
useOrderType,
priorityOptions,
type OrderUrgency,
} from '@openmrs/esm-patient-common-lib';
import { translateFrom, useLayoutType, useSession, useConfig, ExtensionSlot } from '@openmrs/esm-framework';
import { useLayoutType, useSession, useConfig, ExtensionSlot, OpenmrsDatePicker } from '@openmrs/esm-framework';
import {
Button,
ButtonSet,
Expand All @@ -19,17 +20,19 @@ import {
Layer,
NumberInput,
SelectSkeleton,
Select,
SelectItem,
TextArea,
TextInput,
} from '@carbon/react';
import { useTranslation } from 'react-i18next';
import { Controller, type FieldErrors, useForm } from 'react-hook-form';
import { Controller, type ControllerRenderProps, type FieldErrors, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import styles from './medical-supply-order-form.scss';
import { type Concept, ordersEqual, prepOrderPostData, useQuantityUnits } from '../resources';
import { moduleName } from '../../constants';
import { type MedicalSupplyOrderBasketItem } from '../types';
import { type ConfigObject } from '../../config-schema';

export interface OrderFormProps extends DefaultPatientWorkspaceProps {
initialOrder: MedicalSupplyOrderBasketItem;
Expand All @@ -55,44 +58,53 @@ export function OrderForm({
const [showErrorNotification, setShowErrorNotification] = useState(false);
const { orderType } = useOrderType(orderTypeUuid);
const { concepts, isLoadingQuantityUnits, errorFetchingQuantityUnits } = useQuantityUnits();
const { showReferenceNumberField } = useConfig<ConfigObject>();

const OrderFormSchema = useMemo(
() =>
z.object({
instructions: z.string().optional(),
urgency: z.string().refine((value) => value !== '', {
message: t('addLabOrderPriorityRequired', 'Priority is required'),
z
.object({
instructions: z.string().nullish(),
urgency: z.string().refine((value) => value !== '', {
message: t('addLabOrderPriorityRequired', 'Priority is required'),
}),
quantity: z.number({
required_error: t('quantityRequired', 'Quantity is required'),
invalid_type_error: t('quantityRequired', 'Quantity is required'),
}),
quantityUnits: z.object(
{
display: z.string(),
uuid: z.string(),
},
{
required_error: t('quantityUnitsRequired', 'Quantity units is required'),
invalid_type_error: t('quantityUnitsRequired', 'Quantity units is required'),
},
),
accessionNumber: z.string().nullish(),
concept: z.object(
{ display: z.string(), uuid: z.string() },
{
required_error: t('addOrderableConceptRequired', 'Orderable concept is required'),
invalid_type_error: t('addOrderableConceptRequired', 'Orderable concept is required'),
},
),
scheduledDate: z.date().nullish(),
})
.refine((data) => data.urgency !== 'ON_SCHEDULED_DATE' || Boolean(data.scheduledDate), {
message: t('scheduledDateRequired', 'Scheduled date is required'),
path: ['scheduledDate'],
}),
quantity: z.number({
required_error: t('quantityRequired', 'Quantity is required'),
invalid_type_error: t('quantityRequired', 'Quantity is required'),
}),
quantityUnits: z.object(
{
display: z.string(),
uuid: z.string(),
},
{
required_error: t('quantityUnitsRequired', 'Quantity units is required'),
invalid_type_error: t('quantityUnitsRequired', 'Quantity units is required'),
},
),
accessionNumber: z.string().optional(),
concept: z.object(
{ display: z.string(), uuid: z.string() },
{
required_error: t('addOrderableConceptRequired', 'Orderable concept is required'),
invalid_type_error: t('addOrderableConceptRequired', 'Orderable concept is required'),
},
),
}),
[t],
);

const {
control,
handleSubmit,
formState: { errors, defaultValues, isDirty },
setValue,
watch,
} = useForm<MedicalSupplyOrderBasketItem>({
mode: 'all',
resolver: zodResolver(OrderFormSchema),
Expand All @@ -101,9 +113,7 @@ export function OrderForm({
},
});

const filterItemsByName = useCallback((menu) => {
return menu?.item?.value?.toLowerCase().includes(menu?.inputValue?.toLowerCase());
}, []);
const isScheduledDateRequired = watch('urgency') === 'ON_SCHEDULED_DATE';

const handleFormSubmission = useCallback(
(data: MedicalSupplyOrderBasketItem) => {
Expand All @@ -130,6 +140,7 @@ export function OrderForm({

closeWorkspaceWithSavedChanges({
onWorkspaceClose: () => launchPatientWorkspace('order-basket'),
closeWorkspaceGroup: false,
});
},
[orders, setOrders, session?.currentProvider?.uuid, closeWorkspaceWithSavedChanges, initialOrder],
Expand All @@ -139,6 +150,7 @@ export function OrderForm({
setOrders(orders.filter((order) => order.concept.uuid !== defaultValues.concept.conceptUuid));
closeWorkspace({
onWorkspaceClose: () => launchPatientWorkspace('order-basket'),
closeWorkspaceGroup: false,
});
}, [closeWorkspace, orders, setOrders, defaultValues]);

Expand All @@ -152,6 +164,19 @@ export function OrderForm({
promptBeforeClosing(() => isDirty);
}, [isDirty, promptBeforeClosing]);

const handleUpdateUrgency = useCallback(
(fieldOnChange: ControllerRenderProps['onChange']) => {
return (e: ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value as OrderUrgency;
if (value !== 'ON_SCHEDULED_DATE') {
setValue('scheduledDate', null);
}
fieldOnChange(e);
};
},
[setValue],
);

const responsiveSize = isTablet ? 'lg' : 'sm';

return (
Expand All @@ -167,31 +192,31 @@ export function OrderForm({
</InputWrapper>
</Column>
</Grid>
<Grid className={styles.gridRow}>
<Column lg={16} md={8} sm={4}>
<InputWrapper>
<Controller
name="accessionNumber"
control={control}
render={({ field: { onChange, onBlur, value } }) => (
<TextInput
id="labReferenceNumberInput"
invalid={!!errors.accessionNumber}
invalidText={errors.accessionNumber?.message}
labelText={t('testOrderReferenceNumber', '{{orderType}} reference number', {
orderType: orderType?.display,
})}
maxLength={150}
onBlur={onBlur}
onChange={onChange}
size={responsiveSize}
value={value}
/>
)}
/>
</InputWrapper>
</Column>
</Grid>
{showReferenceNumberField && (
<Grid className={styles.gridRow}>
<Column lg={16} md={8} sm={4}>
<InputWrapper>
<Controller
name="accessionNumber"
control={control}
render={({ field: { onChange, onBlur, value } }) => (
<TextInput
id="labReferenceNumberInput"
invalid={!!errors.accessionNumber}
invalidText={errors.accessionNumber?.message}
labelText={t('referenceFieldLabelText', 'Reference number')}
maxLength={150}
onBlur={onBlur}
onChange={onChange}
size={responsiveSize}
value={value}
/>
)}
/>
</InputWrapper>
</Column>
</Grid>
)}

<Grid className={styles.gridRow}>
<Column lg={16} md={8} sm={4}>
Expand Down Expand Up @@ -266,24 +291,49 @@ export function OrderForm({
<Controller
name="urgency"
control={control}
render={({ field: { onBlur, onChange, value } }) => (
<ComboBox
id="priorityInput"
invalid={!!errors.urgency}
invalidText={errors.urgency?.message}
items={priorityOptions}
onBlur={onBlur}
onChange={({ selectedItem }) => onChange(selectedItem?.value || '')}
selectedItem={priorityOptions.find((option) => option.value === value) || null}
shouldFilterItem={filterItemsByName}
render={({ field, fieldState }) => (
<Select
id="priorityField"
{...field}
onChange={handleUpdateUrgency(field.onChange)}
invalid={Boolean(fieldState.error?.message)}
invalidText={fieldState.error?.message}
labelText={t('priority', 'Priority')}
size={responsiveSize}
titleText={t('priority', 'Priority')}
/>
>
{priorityOptions.map((priority) => (
<SelectItem key={priority.value} value={priority.value} text={priority.label} />
))}
</Select>
)}
/>
</InputWrapper>
</Column>
</Grid>

{isScheduledDateRequired && (
<Grid className={styles.gridRow}>
<Column lg={8} md={8} sm={4}>
<InputWrapper>
<Controller
name="scheduledDate"
control={control}
render={({ field, fieldState }) => (
<OpenmrsDatePicker
labelText={t('scheduledDate', 'Scheduled date')}
id="scheduledDate"
{...field}
minDate={new Date()}
invalid={Boolean(fieldState?.error?.message)}
invalidText={fieldState?.error?.message}
/>
)}
/>
</InputWrapper>
</Column>
</Grid>
)}

<Grid className={styles.gridRow}>
<Column lg={16} md={8} sm={4}>
<InputWrapper>
Expand Down
4 changes: 2 additions & 2 deletions src/medical-orders/medical-supply-order-panel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
@use '@openmrs/esm-styleguide/src/vars' as *;

.desktopTile {
border-left: layout.$spacing-02 solid colors.$cyan-20;
border-left: layout.$spacing-02 solid colors.$purple-70;
background-color: $ui-02;
border-top: 1px solid colors.$cyan-20;
border-top: 1px solid colors.$purple-20;
border-right: none;
padding: 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
useOrderType,
usePatientChartStore,
} from '@openmrs/esm-patient-common-lib';
import React, { type ComponentProps, useCallback, useMemo, useRef, useState } from 'react';
import React, { type ComponentProps, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './medical-supply-orderable-concept-search.scss';
import { Button, Search } from '@carbon/react';
Expand Down Expand Up @@ -45,6 +45,7 @@ const OrderableConceptSearchWorkspace: React.FC<OrderableConceptSearchWorkspaceP
closeWorkspace,
closeWorkspaceWithSavedChanges,
promptBeforeClosing,
setTitle,
}) => {
const { t } = useTranslation();
const isTablet = useLayoutType() === 'tablet';
Expand All @@ -53,6 +54,16 @@ const OrderableConceptSearchWorkspace: React.FC<OrderableConceptSearchWorkspaceP
const { orderType } = useOrderType(orderTypeUuid);
const { orderTypes } = useConfig<ConfigObject>();

useEffect(() => {
if (orderType) {
setTitle(
t('addOrderForOrderType', 'Add {{orderTypeDisplay}}', {
orderTypeDisplay: orderType.display.toLocaleLowerCase(),
}),
);
}
}, [setTitle, orderType, t]);

const [currentOrder, setCurrentOrder] = useState<MedicalSupplyOrderBasketItem>(initialOrder);

const orderableConceptSets = useMemo(
Expand Down
13 changes: 12 additions & 1 deletion src/medical-orders/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ import {
type OrderAction,
} from '@openmrs/esm-patient-common-lib';
import { type MedicalSupplyOrderBasketItem } from './types';
import { openmrsFetch, type OpenmrsResource, restBaseUrl, type FetchResponse, useConfig } from '@openmrs/esm-framework';
import {
openmrsFetch,
type OpenmrsResource,
restBaseUrl,
type FetchResponse,
useConfig,
toOmrsIsoString,
} from '@openmrs/esm-framework';
import useSWRImmutable from 'swr/immutable';
import { useEffect, useMemo } from 'react';
import { type ConfigObject } from '../config-schema';
Expand Down Expand Up @@ -53,6 +60,7 @@ export function prepOrderPostData(
accessionNumber: order.accessionNumber,
urgency: order.urgency,
orderType: order.orderType,
scheduledDate: order.scheduledDate ? toOmrsIsoString(order.scheduledDate) : null,
quantity: order.quantity,
quantityUnits: order.quantityUnits.uuid,
};
Expand All @@ -70,6 +78,7 @@ export function prepOrderPostData(
accessionNumber: order.accessionNumber,
urgency: order.urgency,
orderType: order.orderType,
scheduledDate: order.scheduledDate ? toOmrsIsoString(order.scheduledDate) : null,
quantity: order.quantity,
quantityUnits: order.quantityUnits.uuid,
};
Expand All @@ -86,6 +95,7 @@ export function prepOrderPostData(
accessionNumber: order.accessionNumber,
urgency: order.urgency,
orderType: order.orderType,
scheduledDate: order.scheduledDate ? toOmrsIsoString(order.scheduledDate) : null,
};
} else {
throw new Error(`Unknown order action: ${order.action}.`);
Expand Down Expand Up @@ -138,5 +148,6 @@ export function buildMedicalSupplyOrderItem(order: Order, action: OrderAction):
orderType: order.orderType.uuid,
quantity: order.quantity,
quantityUnits: order.quantityUnits,
scheduledDate: order.scheduledDate ? new Date(order.scheduledDate) : null,
};
}
Loading

0 comments on commit 0993c50

Please sign in to comment.