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-4223: Allow adding different order types in the order basket #2109

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions packages/esm-patient-common-lib/src/orders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ export * from './useOrderBasket';
export * from './postOrders';
export * from './useOrders';
export * from './types';
export * from './useOrderableConceptSets';
export * from './useOrderType';
export * from './useOrderTypes';
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,7 +20,6 @@ export interface DrugOrderBasketItem extends OrderBasketItem {
patientInstructions: string;
asNeeded: boolean;
asNeededCondition: string;
// TODO: This is unused
startDate: Date | string;
durationUnit: DurationUnit;
duration: number | null;
Expand Down Expand Up @@ -74,3 +82,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;
}
4 changes: 4 additions & 0 deletions packages/esm-patient-common-lib/src/orders/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './order';
export * from './drug-order';
export * from './test-order';
export * from './order-type';
14 changes: 14 additions & 0 deletions packages/esm-patient-common-lib/src/orders/types/order-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type OrderTypeJavaClassName = 'org.openmrs.Order' | 'org.openmrs.TestOrder' | 'org.openmrs.DrugOrder';

export interface OrderTypeResponse {
uuid: string;
display: string;
name: string;
javaClassName: OrderTypeJavaClassName;
retired: false;
description: string;
conceptClasses: Array<{
uuid: string;
display: string;
}>;
}
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -43,10 +44,21 @@ export interface OrderBasketItem {
* An optional identifier from the fulfiller (e.g., lab system) for the specimen or record associated with the order.
*/
accessionNumber?: string;
concept?: OpenmrsResource;
instructions?: string;
urgency?: OrderUrgency;
previousOrder?: string;
orderType?: string;
orderNumber?: string;
}

export type OrderUrgency = 'ROUTINE' | 'STAT' | 'ON_SCHEDULED_DATE';

export type PriorityOption = {
label: string;
value: OrderUrgency;
};

export interface OrderPost {
urgency?: OrderUrgency;
action?: OrderAction;
Expand Down Expand Up @@ -76,6 +88,7 @@ export interface OrderPost {
orderReason?: string;
instructions?: string;
accessionNumber?: string;
orderType?: string;
}

export interface PatientOrderFetchResponse {
Expand Down Expand Up @@ -131,7 +144,7 @@ export interface Order {
quantityUnits: OpenmrsResource;
route: OpenmrsResource;
scheduleDate: null;
urgency: 'ROUTINE' | 'STAT' | 'ON_SCHEDULED_DATE';
urgency: OrderUrgency;

// additional properties
accessionNumber: string;
Expand Down Expand Up @@ -167,54 +180,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';
10 changes: 10 additions & 0 deletions packages/esm-patient-common-lib/src/orders/types/test-order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { OrderBasketItem } from './order';

export interface TestOrderBasketItem extends OrderBasketItem {
vasharma05 marked this conversation as resolved.
Show resolved Hide resolved
testType: {
label: string;
conceptUuid: string;
};
orderReason?: string;
specimenSource?: string;
}
21 changes: 21 additions & 0 deletions packages/esm-patient-common-lib/src/orders/useOrderType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
import { useMemo } from 'react';
import useSWRImmutable from 'swr/immutable';
import type { OrderTypeResponse } from './types';

export function useOrderType(orderTypeUuid: string) {
const { data, isLoading, isValidating, error } = useSWRImmutable<FetchResponse<OrderTypeResponse>>(
`${restBaseUrl}/ordertype/${orderTypeUuid}`,
openmrsFetch,
);
const results = useMemo(
() => ({
isLoadingOrderType: isLoading,
orderType: data?.data,
errorFetchingOrderType: error,
isValidatingOrderType: isValidating,
}),
[data?.data, error, isLoading, isValidating],
);
return results;
}
24 changes: 24 additions & 0 deletions packages/esm-patient-common-lib/src/orders/useOrderTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
import useSWRImmutable from 'swr/immutable';
import type { OrderTypeFetchResponse } from './types';
import { useMemo } from 'react';

export function useOrderTypes() {
const orderTypesUrl = `${restBaseUrl}/ordertype`;
const { data, error, isLoading, isValidating } = useSWRImmutable<FetchResponse<OrderTypeFetchResponse>, Error>(
orderTypesUrl,
openmrsFetch,
);

const results = useMemo(
() => ({
data: data?.data?.results,
error,
isLoading,
isValidating,
}),
[data?.data?.results, error, isLoading, isValidating],
);

return results;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useEffect, useRef, useState } from 'react';
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 '../../../esm-patient-tests-app/src/config-schema';
import { useOrderableConceptSets } from './useOrderableConceptSets';

const mockOpenrsFetch = openmrsFetch as jest.Mock;
const mockUseConfig = jest.mocked(useConfig<ConfigObject>);

mockUseConfig.mockReturnValue({
...getDefaultsFromConfigSchema(configSchema),
orders: {
labOrderableConcepts: [],
labOrderTypeUuid: 'lab-order-type-uuid',
labOrderConceptClasses: ['8d4907b2-c2cc-11de-8d13-0010c6dffd0f'],
},
});

mockOpenrsFetch.mockImplementation((url: string) => {
if (url.includes('concept?class=concept-class-uuid')) {
return Promise.resolve({ data: { results: [{ display: 'Test concept' }] } });
} else if (/.*concept\/[0-9a-f]+.*/.test(url)) {
return Promise.resolve({
data: {
display: 'Orderable set',
setMembers: [
{ display: 'Configured concept', names: [{ display: 'Configured concept' }], uuid: 'concept-one' },
{
display: 'Another configured concept',
names: [{ display: 'Another configured concept' }],
uuid: 'concept-two',
},
],
},
});
} else {
throw Error('Unexpected URL: ' + url);
}
});

describe('useOrderableConceptSets is configurable', () => {
it('should fetch orderable concept sets if passed', async () => {
const { result } = renderHook(() => useOrderableConceptSets('', [], ['concept-set-uuid']));
expect(openmrsFetch).toHaveBeenCalledWith(
`${restBaseUrl}/concept/concept-set-uuid?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.concepts).toEqual([
expect.objectContaining({ display: 'Another configured concept' }),
expect.objectContaining({ display: 'Configured concept' }),
]);
expect(result.current.error).toBeFalsy();
});

xit('should filter through fetched concepts sets based on the search term', async () => {
const { result } = renderHook(() => useOrderableConceptSets('another', [], ['concept-set-uuid']));
expect(openmrsFetch).toHaveBeenCalledWith(
`${restBaseUrl}/concept/concept-set-uuid?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.concepts).toEqual([
expect.objectContaining({ display: 'Another configured concept' }),
// expect.objectContaining({ display: 'Configured concept' }),
]);
expect(result.current.error).toBeFalsy();
});

it('should fetch concept class if orderable concept set is not passed', async () => {
const { result } = renderHook(() => useOrderableConceptSets('', ['concept-class-uuid'], []));
expect(openmrsFetch).toHaveBeenCalledWith(
`${restBaseUrl}/concept?class=concept-class-uuid&name=&searchType=fuzzy&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.concepts).toEqual([expect.objectContaining({ display: 'Test concept' })]);
expect(result.current.error).toBeFalsy();
});

it('should fetch concept class if orderable concept set is not passed and search term is passed', async () => {
const { result } = renderHook(() => useOrderableConceptSets('concept', ['concept-class-uuid'], []));
expect(openmrsFetch).toHaveBeenCalledWith(
`${restBaseUrl}/concept?class=concept-class-uuid&name=concept&searchType=fuzzy&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.concepts).toEqual([expect.objectContaining({ display: 'Test concept' })]);
expect(result.current.error).toBeFalsy();
});
});
Loading
Loading