@openmrs/esm-patient-medications-app 11.3.1-patch.9064 → 11.3.1-patch.9508
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +22 -27
- package/dist/1022.js +1 -0
- package/dist/1022.js.map +1 -0
- package/dist/1119.js +1 -1
- package/dist/1197.js +1 -1
- package/dist/2146.js +1 -1
- package/dist/2690.js +1 -1
- package/dist/3099.js +1 -1
- package/dist/3338.js +2 -0
- package/dist/3338.js.map +1 -0
- package/dist/3584.js +1 -1
- package/dist/4055.js +1 -1
- package/dist/4132.js +1 -1
- package/dist/4300.js +1 -1
- package/dist/4335.js +1 -1
- package/dist/439.js +1 -0
- package/dist/4618.js +1 -1
- package/dist/4652.js +1 -1
- package/dist/4944.js +1 -1
- package/dist/4953.js +1 -0
- package/dist/4953.js.map +1 -0
- package/dist/5134.js +2 -0
- package/dist/5134.js.map +1 -0
- package/dist/5173.js +1 -1
- package/dist/5241.js +1 -1
- package/dist/5442.js +1 -1
- package/dist/5661.js +1 -1
- package/dist/5670.js +1 -1
- package/dist/5670.js.map +1 -1
- package/dist/6022.js +1 -1
- package/dist/6336.js +1 -0
- package/dist/6336.js.map +1 -0
- package/dist/6468.js +1 -1
- package/dist/6589.js +1 -0
- package/dist/6679.js +1 -1
- package/dist/6840.js +1 -1
- package/dist/6859.js +1 -1
- package/dist/7097.js +1 -1
- package/dist/7151.js +1 -0
- package/dist/7151.js.map +1 -0
- package/dist/7159.js +1 -1
- package/dist/723.js +1 -1
- package/dist/7617.js +1 -1
- package/dist/795.js +1 -1
- package/dist/8163.js +1 -1
- package/dist/8349.js +1 -1
- package/dist/8371.js +1 -0
- package/dist/8437.js +1 -0
- package/dist/8437.js.map +1 -0
- package/dist/8618.js +1 -1
- package/dist/8803.js +1 -1
- package/dist/8803.js.map +1 -1
- package/dist/8812.js +1 -1
- package/dist/8812.js.map +1 -1
- package/dist/890.js +1 -1
- package/dist/9214.js +1 -1
- package/dist/9538.js +1 -1
- package/dist/9569.js +1 -1
- package/dist/986.js +1 -1
- package/dist/9879.js +1 -1
- package/dist/9895.js +1 -1
- package/dist/9900.js +1 -1
- package/dist/9913.js +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-patient-medications-app.js +1 -1
- package/dist/openmrs-esm-patient-medications-app.js.buildmanifest.json +353 -259
- package/dist/openmrs-esm-patient-medications-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +4 -4
- package/src/active-medications/active-medications.component.tsx +1 -1
- package/src/active-medications/active-medications.test.tsx +1 -6
- package/src/add-drug-order/add-drug-order.scss +6 -0
- package/src/add-drug-order/add-drug-order.test.tsx +7 -4
- package/src/add-drug-order/add-drug-order.workspace.tsx +42 -15
- package/src/add-drug-order/drug-order-form.component.tsx +208 -222
- package/src/add-drug-order/drug-order-form.resource.ts +213 -0
- package/src/add-drug-order/drug-order-form.scss +11 -7
- package/src/add-drug-order/drug-search/drug-search-combobox.component.tsx +54 -0
- package/src/add-drug-order/drug-search/drug-search-combobox.test.tsx +56 -0
- package/src/add-drug-order/drug-search/drug-search.component.tsx +3 -1
- package/src/add-drug-order/drug-search/drug-search.resource.tsx +73 -17
- package/src/add-drug-order/drug-search/order-basket-search-results.component.tsx +16 -5
- package/src/add-drug-order/fill-prescription-form.workspace.tsx +111 -0
- package/src/api/api.ts +18 -1
- package/src/components/medications-details-table.component.tsx +3 -3
- package/src/config-schema.ts +15 -2
- package/src/drug-order-basket-panel/drug-order-basket-panel.extension.tsx +22 -5
- package/src/drug-order-basket-panel/drug-order-basket-panel.scss +27 -12
- package/src/drug-order-basket-panel/drug-order-basket-panel.test.tsx +11 -4
- package/src/index.ts +9 -1
- package/src/medications-summary/medications-summary.component.tsx +4 -2
- package/src/past-medications/past-medications.component.tsx +44 -0
- package/src/past-medications/past-medications.scss +7 -0
- package/src/past-medications/past-medications.test.tsx +85 -0
- package/src/routes.json +43 -1
- package/translations/am.json +24 -4
- package/translations/ar.json +24 -4
- package/translations/ar_SY.json +24 -4
- package/translations/bn.json +24 -4
- package/translations/cs.json +115 -0
- package/translations/de.json +24 -4
- package/translations/en.json +22 -4
- package/translations/en_US.json +24 -4
- package/translations/es.json +37 -17
- package/translations/es_MX.json +24 -4
- package/translations/fr.json +30 -10
- package/translations/he.json +24 -4
- package/translations/hi.json +24 -4
- package/translations/hi_IN.json +24 -4
- package/translations/id.json +33 -13
- package/translations/it.json +33 -13
- package/translations/ka.json +31 -11
- package/translations/km.json +24 -4
- package/translations/ku.json +24 -4
- package/translations/ky.json +24 -4
- package/translations/lg.json +24 -4
- package/translations/ne.json +24 -4
- package/translations/pl.json +24 -4
- package/translations/pt.json +33 -13
- package/translations/pt_BR.json +33 -13
- package/translations/qu.json +24 -4
- package/translations/ro_RO.json +33 -13
- package/translations/ru_RU.json +24 -4
- package/translations/si.json +24 -4
- package/translations/sq.json +115 -0
- package/translations/sw.json +24 -4
- package/translations/sw_KE.json +24 -4
- package/translations/tr.json +24 -4
- package/translations/tr_TR.json +24 -4
- package/translations/uk.json +24 -4
- package/translations/uz.json +24 -4
- package/translations/uz@Latn.json +24 -4
- package/translations/uz_UZ.json +24 -4
- package/translations/vi.json +24 -4
- package/translations/zh.json +25 -5
- package/translations/zh_CN.json +33 -13
- package/translations/zh_TW.json +115 -0
- package/dist/2537.js +0 -1
- package/dist/2537.js.map +0 -1
- package/dist/4918.js +0 -1
- package/dist/4918.js.map +0 -1
- package/dist/5837.js +0 -2
- package/dist/5837.js.map +0 -1
- package/dist/6783.js +0 -2
- package/dist/6783.js.map +0 -1
- package/dist/9365.js +0 -1
- package/dist/9365.js.map +0 -1
- package/dist/9647.js +0 -1
- package/dist/9647.js.map +0 -1
- /package/dist/{5837.js.LICENSE.txt → 3338.js.LICENSE.txt} +0 -0
- /package/dist/{6783.js.LICENSE.txt → 5134.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useForm, type UseFormReturn } from 'react-hook-form';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { parseDate, useConfig } from '@openmrs/esm-framework';
|
|
7
|
+
import { careSettingUuid, useRequireOutpatientQuantity } from '../api';
|
|
8
|
+
import { type ConfigObject } from '../config-schema';
|
|
9
|
+
import { type DrugOrderBasketItem } from '../types';
|
|
10
|
+
import { type Drug } from '@openmrs/esm-patient-common-lib';
|
|
11
|
+
|
|
12
|
+
export function useDrugOrderForm(initialOrderBasketItem: DrugOrderBasketItem, defaultPrescribingProviderUuid: string) {
|
|
13
|
+
const medicationOrderFormSchema = useCreateMedicationOrderFormSchema();
|
|
14
|
+
|
|
15
|
+
const defaultValues = useMemo(() => {
|
|
16
|
+
const defaultStartDate =
|
|
17
|
+
typeof initialOrderBasketItem?.startDate === 'string'
|
|
18
|
+
? parseDate(initialOrderBasketItem?.startDate)
|
|
19
|
+
: (initialOrderBasketItem?.startDate as Date) ?? new Date();
|
|
20
|
+
|
|
21
|
+
return drugOrderBasketItemToFormValue(initialOrderBasketItem, defaultStartDate, defaultPrescribingProviderUuid);
|
|
22
|
+
}, [initialOrderBasketItem, defaultPrescribingProviderUuid]);
|
|
23
|
+
|
|
24
|
+
const drugOrderForm: UseFormReturn<MedicationOrderFormData> = useForm<MedicationOrderFormData>({
|
|
25
|
+
mode: 'all',
|
|
26
|
+
resolver: zodResolver(medicationOrderFormSchema),
|
|
27
|
+
defaultValues,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return drugOrderForm;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function drugOrderBasketItemToFormValue(
|
|
34
|
+
item: DrugOrderBasketItem,
|
|
35
|
+
startDate: Date,
|
|
36
|
+
providerUuid: string,
|
|
37
|
+
): MedicationOrderFormData {
|
|
38
|
+
return {
|
|
39
|
+
orderer: {
|
|
40
|
+
uuid: providerUuid,
|
|
41
|
+
},
|
|
42
|
+
drug: item?.drug as Partial<Drug>,
|
|
43
|
+
careSetting: careSettingUuid,
|
|
44
|
+
isFreeTextDosage: item?.isFreeTextDosage ?? false,
|
|
45
|
+
freeTextDosage: item?.freeTextDosage,
|
|
46
|
+
dosage: item?.dosage ?? null,
|
|
47
|
+
unit: item?.unit,
|
|
48
|
+
route: item?.route,
|
|
49
|
+
patientInstructions: item?.patientInstructions ?? '',
|
|
50
|
+
asNeeded: item?.asNeeded ?? false,
|
|
51
|
+
asNeededCondition: item?.asNeededCondition ?? '',
|
|
52
|
+
duration: item?.duration,
|
|
53
|
+
durationUnit: item?.durationUnit,
|
|
54
|
+
pillsDispensed: item?.pillsDispensed ?? null,
|
|
55
|
+
quantityUnits: item?.quantityUnits,
|
|
56
|
+
numRefills: item?.numRefills ?? null,
|
|
57
|
+
indication: item?.indication,
|
|
58
|
+
frequency: item?.frequency,
|
|
59
|
+
startDate,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function useCreateMedicationOrderFormSchema() {
|
|
64
|
+
const { t } = useTranslation();
|
|
65
|
+
const { requireOutpatientQuantity } = useRequireOutpatientQuantity();
|
|
66
|
+
const { requireIndication } = useConfig<ConfigObject>();
|
|
67
|
+
|
|
68
|
+
const schema = useMemo(() => {
|
|
69
|
+
const comboSchema = {
|
|
70
|
+
default: z.boolean().optional(),
|
|
71
|
+
value: z.string(),
|
|
72
|
+
valueCoded: z.string(),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const baseSchemaFields = {
|
|
76
|
+
drug: z
|
|
77
|
+
.object(
|
|
78
|
+
{
|
|
79
|
+
uuid: z.string(),
|
|
80
|
+
concept: z
|
|
81
|
+
.object({
|
|
82
|
+
uuid: z.string(),
|
|
83
|
+
})
|
|
84
|
+
.passthrough(),
|
|
85
|
+
dosageForm: z
|
|
86
|
+
.object({
|
|
87
|
+
uuid: z.string(),
|
|
88
|
+
})
|
|
89
|
+
.passthrough()
|
|
90
|
+
.nullable(),
|
|
91
|
+
strength: z.string().nullable(),
|
|
92
|
+
display: z.string().nullable(),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
message: t('drugRequiredErrorMessage', 'Drug is required'),
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
.passthrough(),
|
|
99
|
+
orderer: z.object(
|
|
100
|
+
{
|
|
101
|
+
uuid: z.string(),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
message: t('prescribingClinicianRequiredErrorMessage', 'Prescribing clinician is required'),
|
|
105
|
+
},
|
|
106
|
+
),
|
|
107
|
+
careSetting: z.string(),
|
|
108
|
+
freeTextDosage: z.string().refine((value) => !!value, {
|
|
109
|
+
message: t('freeDosageErrorMessage', 'Add free dosage note'),
|
|
110
|
+
}),
|
|
111
|
+
dosage: z.number({
|
|
112
|
+
invalid_type_error: t('dosageRequiredErrorMessage', 'Dosage is required'),
|
|
113
|
+
}),
|
|
114
|
+
unit: z.object(
|
|
115
|
+
{ ...comboSchema },
|
|
116
|
+
{
|
|
117
|
+
invalid_type_error: t('selectUnitErrorMessage', 'Dose unit is required'),
|
|
118
|
+
},
|
|
119
|
+
),
|
|
120
|
+
route: z.object(
|
|
121
|
+
{ ...comboSchema },
|
|
122
|
+
{
|
|
123
|
+
invalid_type_error: t('selectRouteErrorMessage', 'Route is required'),
|
|
124
|
+
},
|
|
125
|
+
),
|
|
126
|
+
patientInstructions: z.string().nullable(),
|
|
127
|
+
asNeeded: z.boolean(),
|
|
128
|
+
asNeededCondition: z.string().nullable(),
|
|
129
|
+
duration: z.number().nullable(),
|
|
130
|
+
durationUnit: z.object({ ...comboSchema }).nullable(),
|
|
131
|
+
indication: requireIndication
|
|
132
|
+
? z.string().refine((value) => value !== '', {
|
|
133
|
+
message: t('indicationErrorMessage', 'Indication is required'),
|
|
134
|
+
})
|
|
135
|
+
: z.string().nullish(),
|
|
136
|
+
startDate: z.date(),
|
|
137
|
+
frequency: z.object(
|
|
138
|
+
{ ...comboSchema },
|
|
139
|
+
{
|
|
140
|
+
invalid_type_error: t('selectFrequencyErrorMessage', 'Frequency is required'),
|
|
141
|
+
},
|
|
142
|
+
),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const outpatientDrugOrderFields = {
|
|
146
|
+
pillsDispensed: z
|
|
147
|
+
.number()
|
|
148
|
+
.nullable()
|
|
149
|
+
.refine(
|
|
150
|
+
(value) => {
|
|
151
|
+
if (requireOutpatientQuantity && (typeof value !== 'number' || value < 1)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
message: t('pillDispensedErrorMessage', 'Quantity to dispense is required'),
|
|
158
|
+
},
|
|
159
|
+
),
|
|
160
|
+
quantityUnits: z
|
|
161
|
+
.object(comboSchema)
|
|
162
|
+
.nullable()
|
|
163
|
+
.refine(
|
|
164
|
+
(value) => {
|
|
165
|
+
if (requireOutpatientQuantity && !value) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
message: t('selectQuantityUnitsErrorMessage', 'Quantity unit is required'),
|
|
172
|
+
},
|
|
173
|
+
),
|
|
174
|
+
numRefills: z
|
|
175
|
+
.number()
|
|
176
|
+
.nullable()
|
|
177
|
+
.refine(
|
|
178
|
+
(value) => {
|
|
179
|
+
if (requireOutpatientQuantity && (typeof value !== 'number' || value < 0)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
message: t('numRefillsErrorMessage', 'Number of refills is required'),
|
|
186
|
+
},
|
|
187
|
+
),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const nonFreeTextDosageSchema = z.object({
|
|
191
|
+
...baseSchemaFields,
|
|
192
|
+
...outpatientDrugOrderFields,
|
|
193
|
+
isFreeTextDosage: z.literal(false),
|
|
194
|
+
freeTextDosage: z.string().nullable(),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const freeTextDosageSchema = z.object({
|
|
198
|
+
...baseSchemaFields,
|
|
199
|
+
...outpatientDrugOrderFields,
|
|
200
|
+
isFreeTextDosage: z.literal(true),
|
|
201
|
+
dosage: z.number().nullable(),
|
|
202
|
+
unit: z.object(comboSchema).nullable(),
|
|
203
|
+
route: z.object(comboSchema).nullable(),
|
|
204
|
+
frequency: z.object(comboSchema).nullable(),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return z.discriminatedUnion('isFreeTextDosage', [nonFreeTextDosageSchema, freeTextDosageSchema]);
|
|
208
|
+
}, [requireIndication, requireOutpatientQuantity, t]);
|
|
209
|
+
|
|
210
|
+
return schema;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export type MedicationOrderFormData = z.infer<ReturnType<typeof useCreateMedicationOrderFormSchema>>;
|
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
@use '@carbon/layout';
|
|
3
3
|
@use '@carbon/type';
|
|
4
4
|
|
|
5
|
-
.container {
|
|
6
|
-
height: 100%;
|
|
7
|
-
display: flex;
|
|
8
|
-
flex-direction: column;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
5
|
.orderForm {
|
|
12
6
|
flex-grow: 1;
|
|
13
7
|
display: flex;
|
|
@@ -43,6 +37,16 @@
|
|
|
43
37
|
:global(.cds--number__control-btn) {
|
|
44
38
|
border: 0;
|
|
45
39
|
}
|
|
40
|
+
|
|
41
|
+
counter-reset: section;
|
|
42
|
+
|
|
43
|
+
h3 {
|
|
44
|
+
counter-increment: section;
|
|
45
|
+
|
|
46
|
+
&::before {
|
|
47
|
+
content: counter(section) '. ';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
.medicationInfo {
|
|
@@ -90,7 +94,7 @@
|
|
|
90
94
|
.orderFormHeading {
|
|
91
95
|
@include type.type-style('heading-03');
|
|
92
96
|
margin-bottom: 0;
|
|
93
|
-
margin-top:
|
|
97
|
+
margin-top: layout.$spacing-03;
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
.formSection {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ComboBox } from '@carbon/react';
|
|
2
|
+
import { useConfig, useDebounce } from '@openmrs/esm-framework';
|
|
3
|
+
import React, { useMemo, useState } from 'react';
|
|
4
|
+
import { getTemplateOrderBasketItem, useDrugSearch, useDrugTemplates } from './drug-search.resource';
|
|
5
|
+
import { type DrugOrderBasketItem } from '../../types';
|
|
6
|
+
import { type ConfigObject } from '../../config-schema';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
|
|
9
|
+
interface DrugSearchComboBoxProps {
|
|
10
|
+
setSelectedDrugItem(drug: DrugOrderBasketItem);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* This component is a ComboBox for searching for drugs. Similar to drug-search.component.tsx,
|
|
15
|
+
* but allows for custom behavior when a drug is selected.
|
|
16
|
+
* This extension is currently not used anywhere in the patient-medications-app, but
|
|
17
|
+
* is used by other apps (ex: dispensing) for selecting drugs.
|
|
18
|
+
*/
|
|
19
|
+
const DrugSearchComboBox: React.FC<DrugSearchComboBoxProps> = ({ setSelectedDrugItem }) => {
|
|
20
|
+
const { daysDurationUnit } = useConfig<ConfigObject>();
|
|
21
|
+
const { t } = useTranslation();
|
|
22
|
+
const [drugSearchTerm, setDrugSearchTerm] = useState('');
|
|
23
|
+
const debouncedDrugSearchTerm = useDebounce(drugSearchTerm);
|
|
24
|
+
const { drugs } = useDrugSearch(debouncedDrugSearchTerm);
|
|
25
|
+
const { templateByDrugUuid } = useDrugTemplates(drugs);
|
|
26
|
+
const drugItemTemplateOptions: Array<DrugOrderBasketItem> = useMemo(() => {
|
|
27
|
+
return drugs?.flatMap((drug) => {
|
|
28
|
+
const templates = templateByDrugUuid.get(drug.uuid);
|
|
29
|
+
if (templates?.length > 0) {
|
|
30
|
+
return templates.map((template) => getTemplateOrderBasketItem(drug, daysDurationUnit, template));
|
|
31
|
+
} else {
|
|
32
|
+
return [getTemplateOrderBasketItem(drug, daysDurationUnit)];
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}, [drugs, templateByDrugUuid, daysDurationUnit]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<ComboBox
|
|
39
|
+
id="drug-search-combobox"
|
|
40
|
+
items={drugItemTemplateOptions ?? []}
|
|
41
|
+
onChange={({ selectedItem }) => {
|
|
42
|
+
setSelectedDrugItem(selectedItem);
|
|
43
|
+
}}
|
|
44
|
+
onInputChange={(inputText) => {
|
|
45
|
+
setDrugSearchTerm(inputText);
|
|
46
|
+
}}
|
|
47
|
+
itemToString={(item: DrugOrderBasketItem) => item?.display ?? ''}
|
|
48
|
+
placeholder={t('searchFieldPlaceholder', 'Search for a drug or orderset (e.g. "Aspirin")')}
|
|
49
|
+
titleText={t('drugName', 'Drug name')}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default DrugSearchComboBox;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import DrugSearchComboBox from './drug-search-combobox.component';
|
|
4
|
+
import { useDrugSearch, useDrugTemplates } from './drug-search.resource';
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { mockDrugSearchResultApiData } from '__mocks__';
|
|
7
|
+
|
|
8
|
+
const mockUseDrugSearch = jest.mocked(useDrugSearch);
|
|
9
|
+
const mockUseDrugTemplates = jest.mocked(useDrugTemplates);
|
|
10
|
+
|
|
11
|
+
jest.mock('./drug-search.resource', () => ({
|
|
12
|
+
...jest.requireActual('./drug-search.resource'),
|
|
13
|
+
useDrugSearch: jest.fn(),
|
|
14
|
+
useDrugTemplates: jest.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const mockSetSelectedDrugItem = jest.fn();
|
|
18
|
+
|
|
19
|
+
describe('DrugSearchComboBox', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockUseDrugSearch.mockImplementation(() => ({
|
|
22
|
+
isLoading: false,
|
|
23
|
+
drugs: mockDrugSearchResultApiData,
|
|
24
|
+
error: null,
|
|
25
|
+
isValidating: false,
|
|
26
|
+
mutate: jest.fn(),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
mockUseDrugTemplates.mockImplementation(() => ({
|
|
30
|
+
isLoading: false,
|
|
31
|
+
error: null,
|
|
32
|
+
templateByDrugUuid: new Map(),
|
|
33
|
+
isValidating: false,
|
|
34
|
+
mutate: jest.fn(),
|
|
35
|
+
}));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders DrugSearchComboBox', async () => {
|
|
39
|
+
const user = userEvent.setup();
|
|
40
|
+
renderDrugSearchComboBox();
|
|
41
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
42
|
+
await user.type(screen.getByRole('combobox'), 'Aspirin');
|
|
43
|
+
const aspirin81 = screen.getByText(/Aspirin 81mg/i);
|
|
44
|
+
expect(aspirin81).toBeInTheDocument();
|
|
45
|
+
await aspirin81.click();
|
|
46
|
+
expect(mockSetSelectedDrugItem).toHaveBeenCalledWith(
|
|
47
|
+
expect.objectContaining({
|
|
48
|
+
display: 'Aspirin 81mg',
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
function renderDrugSearchComboBox() {
|
|
55
|
+
render(<DrugSearchComboBox setSelectedDrugItem={mockSetSelectedDrugItem} />);
|
|
56
|
+
}
|
|
@@ -16,9 +16,10 @@ import styles from './order-basket-search.scss';
|
|
|
16
16
|
|
|
17
17
|
export interface DrugSearchProps {
|
|
18
18
|
openOrderForm: (searchResult: DrugOrderBasketItem) => void;
|
|
19
|
+
patient: fhir.Patient;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
export default function DrugSearch({ openOrderForm }: DrugSearchProps) {
|
|
22
|
+
export default function DrugSearch({ openOrderForm, patient }: DrugSearchProps) {
|
|
22
23
|
const { t } = useTranslation();
|
|
23
24
|
const isTablet = useLayoutType() === 'tablet';
|
|
24
25
|
const [searchTerm, setSearchTerm] = useState('');
|
|
@@ -60,6 +61,7 @@ export default function DrugSearch({ openOrderForm }: DrugSearchProps) {
|
|
|
60
61
|
searchTerm={debouncedSearchTerm}
|
|
61
62
|
openOrderForm={openOrderForm}
|
|
62
63
|
focusAndClearSearchInput={focusAndClearSearchInput}
|
|
64
|
+
patient={patient}
|
|
63
65
|
/>
|
|
64
66
|
{isTablet && (
|
|
65
67
|
<div className={styles.separatorContainer}>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import useSWRImmutable from 'swr/immutable';
|
|
3
|
-
import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
3
|
+
import { type FetchResponse, openmrsFetch, restBaseUrl, useFeatureFlag } from '@openmrs/esm-framework';
|
|
4
4
|
import type { Drug } from '@openmrs/esm-patient-common-lib';
|
|
5
5
|
import { type DrugOrderBasketItem, type DrugOrderTemplate, type OrderTemplate } from '../../types';
|
|
6
6
|
|
|
@@ -19,12 +19,20 @@ export interface DrugSearchResult {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
interface OrderTemplateResource {
|
|
23
|
+
uuid: string;
|
|
24
|
+
drug: Drug;
|
|
25
|
+
name: string;
|
|
26
|
+
template: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Search for a list of drugs based on the given query string
|
|
31
|
+
* @param query
|
|
32
|
+
* @returns
|
|
33
|
+
*/
|
|
34
|
+
export function useDrugSearch(query: string) {
|
|
35
|
+
const { data, ...rest } = useSWRImmutable<FetchResponse<{ results: Array<DrugSearchResult> }>, Error>(
|
|
28
36
|
query
|
|
29
37
|
? `${restBaseUrl}/drug?q=${query}&v=custom:(uuid,display,name,strength,dosageForm:(display,uuid),concept:(display,uuid))`
|
|
30
38
|
: null,
|
|
@@ -33,32 +41,36 @@ export function useDrugSearch(query: string): {
|
|
|
33
41
|
|
|
34
42
|
const results = useMemo(
|
|
35
43
|
() => ({
|
|
36
|
-
isLoading,
|
|
37
44
|
drugs: data?.data?.results,
|
|
38
|
-
|
|
45
|
+
...rest,
|
|
39
46
|
}),
|
|
40
|
-
[data,
|
|
47
|
+
[data, rest],
|
|
41
48
|
);
|
|
42
49
|
|
|
43
50
|
return results;
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Search for a list of order templates associated with the given drug.
|
|
55
|
+
* Requires the ordertemplates module installed to work properly.
|
|
56
|
+
* @param drugUuid
|
|
57
|
+
* @returns
|
|
58
|
+
*/
|
|
46
59
|
export function useDrugTemplate(drugUuid: string): {
|
|
47
60
|
isLoading: boolean;
|
|
48
61
|
templates: Array<DrugOrderTemplate>;
|
|
49
62
|
error: Error;
|
|
50
63
|
} {
|
|
64
|
+
const isOrderTemplatesModuleInstalled = useFeatureFlag('ordertemplates-module');
|
|
51
65
|
const { data, error, isLoading } = useSWRImmutable<
|
|
52
66
|
FetchResponse<{
|
|
53
|
-
results: Array<
|
|
54
|
-
uuid: string;
|
|
55
|
-
drug: Drug;
|
|
56
|
-
name: string;
|
|
57
|
-
template: string;
|
|
58
|
-
}>;
|
|
67
|
+
results: Array<OrderTemplateResource>;
|
|
59
68
|
}>,
|
|
60
69
|
Error
|
|
61
|
-
>(
|
|
70
|
+
>(
|
|
71
|
+
isOrderTemplatesModuleInstalled && drugUuid ? `${restBaseUrl}/ordertemplates/orderTemplate?drug=${drugUuid}` : null,
|
|
72
|
+
openmrsFetch,
|
|
73
|
+
);
|
|
62
74
|
|
|
63
75
|
const results = useMemo(
|
|
64
76
|
() => ({
|
|
@@ -74,6 +86,50 @@ export function useDrugTemplate(drugUuid: string): {
|
|
|
74
86
|
return results;
|
|
75
87
|
}
|
|
76
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Search for a list of order templates associated with drugs in the given array
|
|
91
|
+
* Requires the ordertemplates module installed to work properly.
|
|
92
|
+
*
|
|
93
|
+
* Note: This hook is inefficient as it makes a request for each uuid. Having a
|
|
94
|
+
* backend search handler that supports passing in multiple drug uuids will fix that.
|
|
95
|
+
* See: https://openmrs.atlassian.net/browse/OEUI-312
|
|
96
|
+
* @param drugUuids
|
|
97
|
+
* @returns a Map mapping each drug uuid to a list of associated order templates
|
|
98
|
+
*/
|
|
99
|
+
export function useDrugTemplates(drugs: Drug[]) {
|
|
100
|
+
const drugUuids = drugs?.map((d) => d.uuid);
|
|
101
|
+
const isOrderTemplatesModuleInstalled = useFeatureFlag('ordertemplates-module');
|
|
102
|
+
const { data, ...rest } = useSWRImmutable<FetchResponse<OrderTemplateResource>[], Error>(
|
|
103
|
+
isOrderTemplatesModuleInstalled ? drugUuids : null,
|
|
104
|
+
(drugUuids: string[]) => {
|
|
105
|
+
return Promise.all(
|
|
106
|
+
drugUuids.map((drugUuid) => {
|
|
107
|
+
return openmrsFetch<OrderTemplateResource>(`${restBaseUrl}/ordertemplates/orderTemplate?drug=${drugUuid}`);
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const templateByDrugUuid = useMemo(() => {
|
|
114
|
+
const templateByDrugUuid: Map<string, DrugOrderTemplate[]> = new Map();
|
|
115
|
+
for (const d of data ?? []) {
|
|
116
|
+
if (d.data?.drug) {
|
|
117
|
+
const key = d.data.drug.uuid;
|
|
118
|
+
if (!templateByDrugUuid.has(key)) {
|
|
119
|
+
templateByDrugUuid.set(key, []);
|
|
120
|
+
}
|
|
121
|
+
templateByDrugUuid.get(key).push({
|
|
122
|
+
...d.data,
|
|
123
|
+
template: JSON.parse(d.data.template) as OrderTemplate,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return templateByDrugUuid;
|
|
128
|
+
}, [data]);
|
|
129
|
+
|
|
130
|
+
return { templateByDrugUuid, ...rest };
|
|
131
|
+
}
|
|
132
|
+
|
|
77
133
|
export function getDefault(template: OrderTemplate, prop: string) {
|
|
78
134
|
return template.dosingInstructions[prop]?.find((x) => x.default) || template.dosingInstructions[prop]?.[0];
|
|
79
135
|
}
|
|
@@ -3,7 +3,7 @@ import classNames from 'classnames';
|
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { Button, ButtonSkeleton, SkeletonText, Tile } from '@carbon/react';
|
|
5
5
|
import { ShoppingCartArrowUp } from '@carbon/react/icons';
|
|
6
|
-
import { useOrderBasket
|
|
6
|
+
import { useOrderBasket } from '@openmrs/esm-patient-common-lib';
|
|
7
7
|
import {
|
|
8
8
|
ArrowRightIcon,
|
|
9
9
|
closeWorkspace,
|
|
@@ -26,12 +26,14 @@ import type { DrugOrderBasketItem } from '../../types';
|
|
|
26
26
|
import styles from './order-basket-search-results.scss';
|
|
27
27
|
|
|
28
28
|
export interface OrderBasketSearchResultsProps {
|
|
29
|
+
patient: fhir.Patient;
|
|
29
30
|
searchTerm: string;
|
|
30
31
|
openOrderForm: (searchResult: DrugOrderBasketItem) => void;
|
|
31
32
|
focusAndClearSearchInput: () => void;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
interface DrugSearchResultItemProps {
|
|
36
|
+
patient: fhir.Patient;
|
|
35
37
|
drug: DrugSearchResult;
|
|
36
38
|
openOrderForm: (searchResult: DrugOrderBasketItem) => void;
|
|
37
39
|
}
|
|
@@ -40,6 +42,7 @@ export default function OrderBasketSearchResults({
|
|
|
40
42
|
searchTerm,
|
|
41
43
|
openOrderForm,
|
|
42
44
|
focusAndClearSearchInput,
|
|
45
|
+
patient,
|
|
43
46
|
}: OrderBasketSearchResultsProps) {
|
|
44
47
|
const { t } = useTranslation();
|
|
45
48
|
const isTablet = useLayoutType() === 'tablet';
|
|
@@ -105,21 +108,29 @@ export default function OrderBasketSearchResults({
|
|
|
105
108
|
</Button>
|
|
106
109
|
</div>
|
|
107
110
|
<div className={styles.resultsContainer}>
|
|
108
|
-
{drugs?.map((drug) =>
|
|
111
|
+
{drugs?.map((drug) => (
|
|
112
|
+
<DrugSearchResultItem patient={patient} key={drug.uuid} drug={drug} openOrderForm={openOrderForm} />
|
|
113
|
+
))}
|
|
109
114
|
</div>
|
|
110
115
|
</div>
|
|
111
116
|
);
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
const DrugSearchResultItem: React.FC<DrugSearchResultItemProps> = ({ drug, openOrderForm }) => {
|
|
119
|
+
const DrugSearchResultItem: React.FC<DrugSearchResultItemProps> = ({ patient, drug, openOrderForm }) => {
|
|
115
120
|
const isTablet = useLayoutType() === 'tablet';
|
|
116
|
-
const { orders, setOrders } = useOrderBasket<DrugOrderBasketItem>(
|
|
117
|
-
|
|
121
|
+
const { orders, setOrders } = useOrderBasket<DrugOrderBasketItem>(
|
|
122
|
+
patient,
|
|
123
|
+
'medications',
|
|
124
|
+
prepMedicationOrderPostData,
|
|
125
|
+
);
|
|
126
|
+
const patientUuid = patient.id;
|
|
118
127
|
const { data: activeOrders, isLoading: isLoadingActiveOrders } = useActivePatientOrders(patientUuid);
|
|
119
128
|
const drugAlreadyInBasket = useMemo(
|
|
120
129
|
() => orders?.some((order) => ordersEqual(order, getTemplateOrderBasketItem(drug))),
|
|
121
130
|
[orders, drug],
|
|
122
131
|
);
|
|
132
|
+
// TODO: use the backend instead of this to determine whether the drug formulation can be ordered
|
|
133
|
+
// See: https://openmrs.atlassian.net/browse/RESTWS-1003
|
|
123
134
|
const drugAlreadyPrescribed = useMemo(
|
|
124
135
|
() => activeOrders?.some((order) => order?.drug?.uuid === drug?.uuid),
|
|
125
136
|
[activeOrders, drug],
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import {
|
|
4
|
+
type ConfigObject,
|
|
5
|
+
type DefaultWorkspaceProps,
|
|
6
|
+
showSnackbar,
|
|
7
|
+
useConfig,
|
|
8
|
+
useSession,
|
|
9
|
+
useVisit,
|
|
10
|
+
} from '@openmrs/esm-framework';
|
|
11
|
+
import { type EncounterPost, postEncounter } from '@openmrs/esm-patient-common-lib';
|
|
12
|
+
import { type DrugOrderBasketItem } from '../types';
|
|
13
|
+
import { prepMedicationOrderPostData } from '../api';
|
|
14
|
+
import DrugOrderForm from './drug-order-form.component';
|
|
15
|
+
|
|
16
|
+
export interface FillPrescriptionFormProps extends DefaultWorkspaceProps {
|
|
17
|
+
patient: fhir.Patient;
|
|
18
|
+
onAfterSave?(patient: fhir.Patient, encounterUuid: string);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* This form is meant for backfilling prescriptions from paper forms, and is similar to the drug order form in the
|
|
23
|
+
* order basket, except:
|
|
24
|
+
* - it contains additional fields select the drug (inline), the prescribing clinician and the prescribing date.
|
|
25
|
+
* - when submitting the form, it directly submits the order instead of saving to the order basket
|
|
26
|
+
*
|
|
27
|
+
* This component is not used in the medications app itself, but is intended to be used in other apps
|
|
28
|
+
* (like dispensing).
|
|
29
|
+
*/
|
|
30
|
+
const FillPrescriptionForm: React.FC<FillPrescriptionFormProps> = ({
|
|
31
|
+
patient,
|
|
32
|
+
closeWorkspace,
|
|
33
|
+
closeWorkspaceWithSavedChanges,
|
|
34
|
+
promptBeforeClosing,
|
|
35
|
+
onAfterSave,
|
|
36
|
+
}) => {
|
|
37
|
+
const { sessionLocation } = useSession();
|
|
38
|
+
const { t } = useTranslation();
|
|
39
|
+
const { drugOrderEncounterType } = useConfig<ConfigObject>();
|
|
40
|
+
const patientUuid = patient.id;
|
|
41
|
+
const { activeVisit, isLoading: isLoadingVisit } = useVisit(patientUuid);
|
|
42
|
+
|
|
43
|
+
const submitDrugOrder = useCallback(
|
|
44
|
+
async (finalizedOrder: DrugOrderBasketItem) => {
|
|
45
|
+
const drugOrderPost = prepMedicationOrderPostData(finalizedOrder, patientUuid, null);
|
|
46
|
+
const encounter: EncounterPost = {
|
|
47
|
+
patient: patientUuid,
|
|
48
|
+
location: sessionLocation.uuid,
|
|
49
|
+
encounterType: drugOrderEncounterType,
|
|
50
|
+
encounterDatetime: new Date(),
|
|
51
|
+
visit: null, // let the server's logic associate the encounter with a visit
|
|
52
|
+
obs: [],
|
|
53
|
+
orders: [drugOrderPost],
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
const encounterUuid = await postEncounter(encounter);
|
|
57
|
+
showSnackbar({
|
|
58
|
+
isLowContrast: true,
|
|
59
|
+
kind: 'success',
|
|
60
|
+
title: t('orderCompleted', 'Placed orders'),
|
|
61
|
+
subtitle: t('ordered', 'Placed order for {{order}}', {
|
|
62
|
+
order: finalizedOrder.drug.display,
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
closeWorkspaceWithSavedChanges();
|
|
66
|
+
onAfterSave?.(patient, encounterUuid);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
showSnackbar({
|
|
69
|
+
isLowContrast: true,
|
|
70
|
+
kind: 'error',
|
|
71
|
+
title: t('saveDrugOrderFailed', 'Error ordering {{orderName}}', {
|
|
72
|
+
orderName: finalizedOrder.drug.display,
|
|
73
|
+
}),
|
|
74
|
+
subtitle: e?.responseBody?.error?.translatedMessage ?? e?.responseBody?.error?.message,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
[
|
|
79
|
+
patientUuid,
|
|
80
|
+
closeWorkspaceWithSavedChanges,
|
|
81
|
+
sessionLocation.uuid,
|
|
82
|
+
t,
|
|
83
|
+
drugOrderEncounterType,
|
|
84
|
+
onAfterSave,
|
|
85
|
+
patient,
|
|
86
|
+
],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (!isLoadingVisit && !activeVisit) {
|
|
90
|
+
return (
|
|
91
|
+
<div>
|
|
92
|
+
{t('visitRequiredForFillingPrescription', 'Patient is required to have an active visit to fill prescription')}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return (
|
|
97
|
+
<DrugOrderForm
|
|
98
|
+
patientUuid={patientUuid}
|
|
99
|
+
initialOrderBasketItem={{ action: 'NEW' } as DrugOrderBasketItem}
|
|
100
|
+
patient={patient}
|
|
101
|
+
onSave={submitDrugOrder}
|
|
102
|
+
saveButtonText={t('fillPrescription', 'Fill prescription')}
|
|
103
|
+
onCancel={closeWorkspace}
|
|
104
|
+
promptBeforeClosing={promptBeforeClosing}
|
|
105
|
+
allowSelectingPrescribingClinician={true}
|
|
106
|
+
allowSelectingDrug={true}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export default FillPrescriptionForm;
|