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-4346: Order basket improvements #4

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading