@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.
Files changed (152) hide show
  1. package/.turbo/turbo-build.log +22 -27
  2. package/dist/1022.js +1 -0
  3. package/dist/1022.js.map +1 -0
  4. package/dist/1119.js +1 -1
  5. package/dist/1197.js +1 -1
  6. package/dist/2146.js +1 -1
  7. package/dist/2690.js +1 -1
  8. package/dist/3099.js +1 -1
  9. package/dist/3338.js +2 -0
  10. package/dist/3338.js.map +1 -0
  11. package/dist/3584.js +1 -1
  12. package/dist/4055.js +1 -1
  13. package/dist/4132.js +1 -1
  14. package/dist/4300.js +1 -1
  15. package/dist/4335.js +1 -1
  16. package/dist/439.js +1 -0
  17. package/dist/4618.js +1 -1
  18. package/dist/4652.js +1 -1
  19. package/dist/4944.js +1 -1
  20. package/dist/4953.js +1 -0
  21. package/dist/4953.js.map +1 -0
  22. package/dist/5134.js +2 -0
  23. package/dist/5134.js.map +1 -0
  24. package/dist/5173.js +1 -1
  25. package/dist/5241.js +1 -1
  26. package/dist/5442.js +1 -1
  27. package/dist/5661.js +1 -1
  28. package/dist/5670.js +1 -1
  29. package/dist/5670.js.map +1 -1
  30. package/dist/6022.js +1 -1
  31. package/dist/6336.js +1 -0
  32. package/dist/6336.js.map +1 -0
  33. package/dist/6468.js +1 -1
  34. package/dist/6589.js +1 -0
  35. package/dist/6679.js +1 -1
  36. package/dist/6840.js +1 -1
  37. package/dist/6859.js +1 -1
  38. package/dist/7097.js +1 -1
  39. package/dist/7151.js +1 -0
  40. package/dist/7151.js.map +1 -0
  41. package/dist/7159.js +1 -1
  42. package/dist/723.js +1 -1
  43. package/dist/7617.js +1 -1
  44. package/dist/795.js +1 -1
  45. package/dist/8163.js +1 -1
  46. package/dist/8349.js +1 -1
  47. package/dist/8371.js +1 -0
  48. package/dist/8437.js +1 -0
  49. package/dist/8437.js.map +1 -0
  50. package/dist/8618.js +1 -1
  51. package/dist/8803.js +1 -1
  52. package/dist/8803.js.map +1 -1
  53. package/dist/8812.js +1 -1
  54. package/dist/8812.js.map +1 -1
  55. package/dist/890.js +1 -1
  56. package/dist/9214.js +1 -1
  57. package/dist/9538.js +1 -1
  58. package/dist/9569.js +1 -1
  59. package/dist/986.js +1 -1
  60. package/dist/9879.js +1 -1
  61. package/dist/9895.js +1 -1
  62. package/dist/9900.js +1 -1
  63. package/dist/9913.js +1 -1
  64. package/dist/main.js +1 -1
  65. package/dist/main.js.map +1 -1
  66. package/dist/openmrs-esm-patient-medications-app.js +1 -1
  67. package/dist/openmrs-esm-patient-medications-app.js.buildmanifest.json +353 -259
  68. package/dist/openmrs-esm-patient-medications-app.js.map +1 -1
  69. package/dist/routes.json +1 -1
  70. package/package.json +4 -4
  71. package/src/active-medications/active-medications.component.tsx +1 -1
  72. package/src/active-medications/active-medications.test.tsx +1 -6
  73. package/src/add-drug-order/add-drug-order.scss +6 -0
  74. package/src/add-drug-order/add-drug-order.test.tsx +7 -4
  75. package/src/add-drug-order/add-drug-order.workspace.tsx +42 -15
  76. package/src/add-drug-order/drug-order-form.component.tsx +208 -222
  77. package/src/add-drug-order/drug-order-form.resource.ts +213 -0
  78. package/src/add-drug-order/drug-order-form.scss +11 -7
  79. package/src/add-drug-order/drug-search/drug-search-combobox.component.tsx +54 -0
  80. package/src/add-drug-order/drug-search/drug-search-combobox.test.tsx +56 -0
  81. package/src/add-drug-order/drug-search/drug-search.component.tsx +3 -1
  82. package/src/add-drug-order/drug-search/drug-search.resource.tsx +73 -17
  83. package/src/add-drug-order/drug-search/order-basket-search-results.component.tsx +16 -5
  84. package/src/add-drug-order/fill-prescription-form.workspace.tsx +111 -0
  85. package/src/api/api.ts +18 -1
  86. package/src/components/medications-details-table.component.tsx +3 -3
  87. package/src/config-schema.ts +15 -2
  88. package/src/drug-order-basket-panel/drug-order-basket-panel.extension.tsx +22 -5
  89. package/src/drug-order-basket-panel/drug-order-basket-panel.scss +27 -12
  90. package/src/drug-order-basket-panel/drug-order-basket-panel.test.tsx +11 -4
  91. package/src/index.ts +9 -1
  92. package/src/medications-summary/medications-summary.component.tsx +4 -2
  93. package/src/past-medications/past-medications.component.tsx +44 -0
  94. package/src/past-medications/past-medications.scss +7 -0
  95. package/src/past-medications/past-medications.test.tsx +85 -0
  96. package/src/routes.json +43 -1
  97. package/translations/am.json +24 -4
  98. package/translations/ar.json +24 -4
  99. package/translations/ar_SY.json +24 -4
  100. package/translations/bn.json +24 -4
  101. package/translations/cs.json +115 -0
  102. package/translations/de.json +24 -4
  103. package/translations/en.json +22 -4
  104. package/translations/en_US.json +24 -4
  105. package/translations/es.json +37 -17
  106. package/translations/es_MX.json +24 -4
  107. package/translations/fr.json +30 -10
  108. package/translations/he.json +24 -4
  109. package/translations/hi.json +24 -4
  110. package/translations/hi_IN.json +24 -4
  111. package/translations/id.json +33 -13
  112. package/translations/it.json +33 -13
  113. package/translations/ka.json +31 -11
  114. package/translations/km.json +24 -4
  115. package/translations/ku.json +24 -4
  116. package/translations/ky.json +24 -4
  117. package/translations/lg.json +24 -4
  118. package/translations/ne.json +24 -4
  119. package/translations/pl.json +24 -4
  120. package/translations/pt.json +33 -13
  121. package/translations/pt_BR.json +33 -13
  122. package/translations/qu.json +24 -4
  123. package/translations/ro_RO.json +33 -13
  124. package/translations/ru_RU.json +24 -4
  125. package/translations/si.json +24 -4
  126. package/translations/sq.json +115 -0
  127. package/translations/sw.json +24 -4
  128. package/translations/sw_KE.json +24 -4
  129. package/translations/tr.json +24 -4
  130. package/translations/tr_TR.json +24 -4
  131. package/translations/uk.json +24 -4
  132. package/translations/uz.json +24 -4
  133. package/translations/uz@Latn.json +24 -4
  134. package/translations/uz_UZ.json +24 -4
  135. package/translations/vi.json +24 -4
  136. package/translations/zh.json +25 -5
  137. package/translations/zh_CN.json +33 -13
  138. package/translations/zh_TW.json +115 -0
  139. package/dist/2537.js +0 -1
  140. package/dist/2537.js.map +0 -1
  141. package/dist/4918.js +0 -1
  142. package/dist/4918.js.map +0 -1
  143. package/dist/5837.js +0 -2
  144. package/dist/5837.js.map +0 -1
  145. package/dist/6783.js +0 -2
  146. package/dist/6783.js.map +0 -1
  147. package/dist/9365.js +0 -1
  148. package/dist/9365.js.map +0 -1
  149. package/dist/9647.js +0 -1
  150. package/dist/9647.js.map +0 -1
  151. /package/dist/{5837.js.LICENSE.txt → 3338.js.LICENSE.txt} +0 -0
  152. /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: 1.25rem;
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
- export function useDrugSearch(query: string): {
23
- isLoading: boolean;
24
- drugs: Array<DrugSearchResult>;
25
- error: Error;
26
- } {
27
- const { data, error, isLoading } = useSWRImmutable<FetchResponse<{ results: Array<DrugSearchResult> }>, Error>(
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
- error,
45
+ ...rest,
39
46
  }),
40
- [data, error, isLoading],
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
- >(drugUuid ? `${restBaseUrl}/ordertemplates/orderTemplate?drug=${drugUuid}` : null, openmrsFetch);
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, usePatientChartStore } from '@openmrs/esm-patient-common-lib';
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) => <DrugSearchResultItem key={drug.uuid} drug={drug} openOrderForm={openOrderForm} />)}
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>('medications', prepMedicationOrderPostData);
117
- const { patientUuid } = usePatientChartStore();
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;