Skip to content

Commit

Permalink
Get schema validation working based on requireOutpatientQuantity GP
Browse files Browse the repository at this point in the history
  • Loading branch information
denniskigen committed Jul 11, 2024
1 parent 4ab422c commit f01ab21
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 120 deletions.
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
/* eslint-disable testing-library/no-node-access */
import React from 'react';
import { screen, render, within, renderHook, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { screen, render, within, renderHook, waitFor } from '@testing-library/react';
import { getByTextWithMarkup } from 'tools';
import { getTemplateOrderBasketItem, useDrugSearch, useDrugTemplate } from './drug-search/drug-search.resource';
import AddDrugOrderWorkspace from './add-drug-order.workspace';
import { mockDrugSearchResultApiData, mockDrugOrderTemplateApiData, mockPatientDrugOrdersApiData } from '__mocks__';
import { type PostDataPrepFunction, useOrderBasket } from '@openmrs/esm-patient-common-lib';
import { closeWorkspace, useSession } from '@openmrs/esm-framework';
import { type PostDataPrepFunction, useOrderBasket } from '@openmrs/esm-patient-common-lib';
import { _resetOrderBasketStore } from '@openmrs/esm-patient-common-lib/src/orders/store';
import AddDrugOrderWorkspace from './add-drug-order.workspace';

const mockCloseWorkspace = closeWorkspace as jest.Mock;
mockCloseWorkspace.mockImplementation((name, { onWorkspaceClose }) => onWorkspaceClose());

const mockLaunchPatientWorkspace = jest.fn();
const mockUseSession = useSession as jest.Mock;
const usePatientOrdersMock = jest.fn();

mockCloseWorkspace.mockImplementation((name, { onWorkspaceClose }) => onWorkspaceClose());
mockUseSession.mockReturnValue({
currentProvider: {
uuid: 'mock-provider-uuid',
},
});

const mockLaunchPatientWorkspace = jest.fn();
jest.mock('@openmrs/esm-patient-common-lib', () => ({
...jest.requireActual('@openmrs/esm-patient-common-lib'),
launchPatientWorkspace: (...args) => mockLaunchPatientWorkspace(...args),
Expand All @@ -42,10 +43,12 @@ jest.mock('./drug-search/drug-search.resource', () => ({
useDebounce: jest.fn().mockImplementation((x) => x),
}));

const usePatientOrdersMock = jest.fn();
jest.mock('../api/api', () => ({
...jest.requireActual('../api/api'),
usePatientOrders: () => usePatientOrdersMock(),
useRequireOutpatientQuantity: jest
.fn()
.mockReturnValue({ requireOutpatientQuantity: false, error: null, isLoading: false }),
}));

function renderDrugSearch() {
Expand Down Expand Up @@ -166,7 +169,7 @@ describe('AddDrugOrderWorkspace drug search', () => {
const indicationField = screen.getByRole('textbox', { name: 'Indication' });
await user.type(indicationField, 'Hypertension');
const saveFormButton = screen.getByText(/Save order/i);
fireEvent.click(saveFormButton);
await user.click(saveFormButton);

await waitFor(() =>
expect(hookResult.current.orders).toEqual([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,126 +57,124 @@ export interface DrugOrderFormProps {
promptBeforeClosing: (testFcn: () => boolean) => void;
}

const comboSchema = {
value: z.string(),
valueCoded: z.string(),
default: z.boolean().optional(),
};

const schemaFields = {
// t( 'freeDosageErrorMessage', 'Add free dosage note')
freeTextDosage: z.string().refine((value) => value !== '', {
message: translateFrom(moduleName, 'freeDosageErrorMessage', 'Add free dosage note'),
}),

// t( 'dosageRequiredErrorMessage', 'A dosage is required' )
dosage: z.number({
invalid_type_error: translateFrom(moduleName, 'dosageRequiredErrorMessage', 'A dosage is required'),
}),

// t( 'selectUnitErrorMessage', 'Please select a unit' )
unit: z.object(
{ ...comboSchema },
{
invalid_type_error: translateFrom(moduleName, 'selectUnitErrorMessage', 'Please select a unit'),
},
),
const createMedicationOrderFormSchema = (requireOutpatientQuantity: boolean) => {
const comboSchema = {
value: z.string(),
valueCoded: z.string(),
default: z.boolean().optional(),
};

// t( 'selectRouteErrorMessage', 'Please select a route' )
route: z.object(
{ ...comboSchema },
{
invalid_type_error: translateFrom(moduleName, 'selectRouteErrorMessage', 'Please select a route'),
},
),

patientInstructions: z.string().nullable(),
asNeeded: z.boolean(),
asNeededCondition: z.string().nullable(),
duration: z.number().nullable(),
durationUnit: z.object({ ...comboSchema }).nullable(),
// t( 'pillDispensedErrorMessage', 'The quantity to dispense is required' )
pillsDispensed: z
.number({
invalid_type_error: translateFrom(
moduleName,
'pillDispensedErrorMessage',
'The quantity to dispense is required',
),
})
.nullable(),
// t( 'selectQuantityUnitsErrorMessage', 'Dispensing requires a quantity unit' )
quantityUnits: z
.object(
const baseSchemaFields = {
// t('freeDosageErrorMessage', 'Add free dosage note')
freeTextDosage: z.string().refine((value) => !!value, {
message: translateFrom(moduleName, 'freeDosageErrorMessage', 'Add free dosage note'),
}),
// t('dosageRequiredErrorMessage', 'A dosage is required')
dosage: z.number({
invalid_type_error: translateFrom(moduleName, 'dosageRequiredErrorMessage', 'A dosage is required'),
}),
// t('selectUnitErrorMessage', 'Please select a unit')
unit: z.object(
{ ...comboSchema },
{
invalid_type_error: translateFrom(
moduleName,
'selectQuantityUnitsErrorMessage',
'Dispensing requires a quantity unit',
),
invalid_type_error: translateFrom(moduleName, 'selectUnitErrorMessage', 'Please select a unit'),
},
)
.nullable(),
numRefills: z.number().nullable(),
// t( 'indicationErrorMessage', 'Please add an indication' )
indication: z.string().refine((value) => value !== '', {
message: translateFrom(moduleName, 'indicationErrorMessage', 'Please add an indication'),
}),
startDate: z.date(),
// t( 'selectFrequencyErrorMessage', 'Please select a frequency' )
frequency: z.object(
{ ...comboSchema },
{
invalid_type_error: translateFrom(moduleName, 'selectFrequencyErrorMessage', 'Please select a frequency'),
},
),
requireOutpatientQuantity: z.boolean().optional(),
};

const medicationOrderFormSchema = z
.discriminatedUnion('isFreeTextDosage', [
z.object({
...schemaFields,
isFreeTextDosage: z.literal(false),
freeTextDosage: z.string().optional(),
}),
z.object({
...schemaFields,
isFreeTextDosage: z.literal(true),
dosage: z.number().nullable(),
unit: z.object({ ...comboSchema }).nullable(),
route: z.object({ ...comboSchema }).nullable(),
frequency: z.object({ ...comboSchema }).nullable(),
),
// t('selectRouteErrorMessage', 'Please select a route')
route: z.object(
{ ...comboSchema },
{
invalid_type_error: translateFrom(moduleName, 'selectRouteErrorMessage', 'Please select a route'),
},
),
patientInstructions: z.string().nullable(),
asNeeded: z.boolean(),
asNeededCondition: z.string().nullable(),
duration: z.number().nullable(),
durationUnit: z.object({ ...comboSchema }).nullable(),
// t('indicationErrorMessage', 'Please add an indication')
indication: z.string().refine((value) => value !== '', {
message: translateFrom(moduleName, 'indicationErrorMessage', 'Please add an indication'),
}),
])
.superRefine((formValues, ctx) => {
if (formValues.requireOutpatientQuantity === true) {
if (!z.number().safeParse(formValues.pillsDispensed).success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['pillsDispensed'],
startDate: z.date(),
// t('selectFrequencyErrorMessage', 'Please select a frequency')
frequency: z.object(
{ ...comboSchema },
{
invalid_type_error: translateFrom(moduleName, 'selectFrequencyErrorMessage', 'Please select a frequency'),
},
),
};

const outpatientDrugOrderFields = {
// t('pillDispensedErrorMessage', 'The quantity to dispense is required')
pillsDispensed: z
.number()
.nullable()
.refine(
(value) => {
if (requireOutpatientQuantity && !value) {
return false;
}
return true;
},
{
message: translateFrom(moduleName, 'pillDispensedErrorMessage', 'The quantity to dispense is required'),
});
}
if (!z.object({ ...comboSchema }).safeParse(formValues.quantityUnits).success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['quantityUnits'],
},
),
// t('selectQuantityUnitsErrorMessage', 'Dispensing requires a quantity unit')
quantityUnits: z
.object(comboSchema)
.nullable()
.refine(
(value) => {
if (requireOutpatientQuantity && !value) {
return false;
}
return true;
},
{
message: translateFrom(moduleName, 'selectQuantityUnitsErrorMessage', 'Dispensing requires a quantity unit'),
});
}
if (!z.number().safeParse(formValues.numRefills).success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['numRefills'],
},
),
// t('numRefillsErrorMessage', 'The number of refills is required' )
numRefills: z
.number()
.nullable()
.refine(
(value) => {
if (requireOutpatientQuantity && !value) {
return false;
}
return true;
},
{
message: translateFrom(moduleName, 'numRefillsErrorMessage', 'The number of refills is required'),
});
}
}
},
),
};

const nonFreeTextDosageSchema = z.object({
...baseSchemaFields,
...outpatientDrugOrderFields,
isFreeTextDosage: z.literal(false),
freeTextDosage: z.string().optional(),
});

type MedicationOrderFormData = z.infer<typeof medicationOrderFormSchema>;
const freeTextDosageSchema = z.object({
...baseSchemaFields,
...outpatientDrugOrderFields,
isFreeTextDosage: z.literal(true),
dosage: z.number().nullable(),
unit: z.object(comboSchema).nullable(),
route: z.object(comboSchema).nullable(),
frequency: z.object(comboSchema).nullable(),
});

return z.discriminatedUnion('isFreeTextDosage', [nonFreeTextDosageSchema, freeTextDosageSchema]);
};

type MedicationOrderFormData = z.infer<ReturnType<typeof createMedicationOrderFormSchema>>;

function MedicationInfoHeader({
orderBasketItem,
Expand Down Expand Up @@ -237,6 +235,11 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
return initialOrderBasketItem?.startDate as Date;
}, [initialOrderBasketItem?.startDate]);

const medicationOrderFormSchema = useMemo(
() => createMedicationOrderFormSchema(requireOutpatientQuantity),
[requireOutpatientQuantity],
);

const {
control,
formState: { isDirty },
Expand Down Expand Up @@ -264,7 +267,6 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
indication: initialOrderBasketItem?.indication,
frequency: initialOrderBasketItem?.frequency,
startDate: defaultStartDate,
requireOutpatientQuantity: requireOutpatientQuantity,
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,5 +240,6 @@

.errorLabel {
color: colors.$red-60;
margin-top: layout.$spacing-02;
}

3 changes: 2 additions & 1 deletion packages/esm-patient-medications-app/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ export function prepMedicationOrderPostData(
* and number of refills for outpatient drug orders.
*
* @returns {Object} An object containing:
* - requireOutpatientQuantity: A boolean indicating if outpatient quantity is required.
* - requireOutpatientQuantity: A boolean indicating whether to require quantity, quantity units,
* and number of refills for outpatient drug orders.
* - error: Any error encountered during the fetch operation.
* - isLoading: A boolean indicating if the fetch operation is in progress.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/esm-patient-medications-app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"modify": "Modify",
"none": "None",
"noResultsForDrugSearch": "No results to display for \"{{searchTerm}}\"",
"numRefillsErrorMessage": "The number of refills is required",
"onDate": "on",
"orderActionDiscontinue": "Discontinue",
"orderActionIncomplete": "Incomplete",
Expand Down

0 comments on commit f01ab21

Please sign in to comment.