@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
@@ -1,5 +1,4 @@
1
1
  import React, { type ChangeEvent, type ComponentProps, useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { useTranslation } from 'react-i18next';
3
2
  import classNames from 'classnames';
4
3
  import {
5
4
  Button,
@@ -7,36 +6,38 @@ import {
7
6
  Checkbox,
8
7
  Column,
9
8
  ComboBox,
10
- IconButton,
11
9
  Form,
12
10
  FormGroup,
13
11
  FormLabel,
14
12
  Grid,
13
+ IconButton,
15
14
  InlineNotification,
16
15
  Layer,
17
16
  NumberInput,
17
+ Stack,
18
18
  TextArea,
19
19
  TextInput,
20
20
  Toggle,
21
21
  } from '@carbon/react';
22
22
  import { Subtract } from '@carbon/react/icons';
23
23
  import { capitalize } from 'lodash-es';
24
+ import { useTranslation } from 'react-i18next';
25
+ import { type Control, Controller, type FieldErrors, useController } from 'react-hook-form';
24
26
  import {
25
27
  AddIcon,
26
28
  age,
27
- ArrowLeftIcon,
28
29
  ExtensionSlot,
29
30
  formatDate,
30
31
  getPatientName,
31
32
  OpenmrsDatePicker,
32
33
  parseDate,
34
+ showSnackbar,
33
35
  useConfig,
36
+ useFeatureFlag,
34
37
  useLayoutType,
38
+ useSession,
35
39
  } from '@openmrs/esm-framework';
36
- import { type Control, Controller, useController, useForm } from 'react-hook-form';
37
- import { zodResolver } from '@hookform/resolvers/zod';
38
- import { usePatientChartStore } from '@openmrs/esm-patient-common-lib';
39
- import { z } from 'zod';
40
+ import { type Drug } from '@openmrs/esm-patient-common-lib';
40
41
  import { useOrderConfig } from '../api/order-config';
41
42
  import { type ConfigObject } from '../config-schema';
42
43
  import type {
@@ -48,143 +49,40 @@ import type {
48
49
  MedicationRoute,
49
50
  QuantityUnit,
50
51
  } from '../types';
51
- import { useRequireOutpatientQuantity } from '../api';
52
+ import { type Provider, useActivePatientOrders, useProviders } from '../api';
53
+ import {
54
+ drugOrderBasketItemToFormValue,
55
+ type MedicationOrderFormData,
56
+ useDrugOrderForm,
57
+ } from './drug-order-form.resource';
58
+ import DrugSearchComboBox from './drug-search/drug-search-combobox.component';
52
59
  import styles from './drug-order-form.scss';
53
60
 
54
61
  export interface DrugOrderFormProps {
62
+ patientUuid: string;
55
63
  initialOrderBasketItem: DrugOrderBasketItem;
56
- onSave: (finalizedOrder: DrugOrderBasketItem) => void;
64
+ patient: fhir.Patient;
65
+ onSave: (finalizedOrder: DrugOrderBasketItem) => Promise<void>;
66
+ saveButtonText: string;
57
67
  onCancel: () => void;
58
68
  promptBeforeClosing: (testFcn: () => boolean) => void;
59
- }
60
69
 
61
- function useCreateMedicationOrderFormSchema() {
62
- const { t } = useTranslation();
63
- const { requireOutpatientQuantity } = useRequireOutpatientQuantity();
64
- const { requireIndication } = useConfig<ConfigObject>();
65
-
66
- const schema = useMemo(() => {
67
- const comboSchema = {
68
- default: z.boolean().optional(),
69
- value: z.string(),
70
- valueCoded: z.string(),
71
- };
72
-
73
- const baseSchemaFields = {
74
- freeTextDosage: z.string().refine((value) => !!value, {
75
- message: t('freeDosageErrorMessage', 'Add free dosage note'),
76
- }),
77
- dosage: z.number({
78
- invalid_type_error: t('dosageRequiredErrorMessage', 'Dosage is required'),
79
- }),
80
- unit: z.object(
81
- { ...comboSchema },
82
- {
83
- invalid_type_error: t('selectUnitErrorMessage', 'Dose unit is required'),
84
- },
85
- ),
86
- route: z.object(
87
- { ...comboSchema },
88
- {
89
- invalid_type_error: t('selectRouteErrorMessage', 'Route is required'),
90
- },
91
- ),
92
- patientInstructions: z.string().nullable(),
93
- asNeeded: z.boolean(),
94
- asNeededCondition: z.string().nullable(),
95
- duration: z.number().nullable(),
96
- durationUnit: z.object({ ...comboSchema }).nullable(),
97
- indication: requireIndication
98
- ? z.string().refine((value) => value !== '', {
99
- message: t('indicationErrorMessage', 'Indication is required'),
100
- })
101
- : z.string().nullish(),
102
- startDate: z.date(),
103
- frequency: z.object(
104
- { ...comboSchema },
105
- {
106
- invalid_type_error: t('selectFrequencyErrorMessage', 'Frequency is required'),
107
- },
108
- ),
109
- };
110
-
111
- const outpatientDrugOrderFields = {
112
- pillsDispensed: z
113
- .number()
114
- .nullable()
115
- .refine(
116
- (value) => {
117
- if (requireOutpatientQuantity && (typeof value !== 'number' || value < 1)) {
118
- return false;
119
- }
120
- return true;
121
- },
122
- {
123
- message: t('pillDispensedErrorMessage', 'Quantity to dispense is required'),
124
- },
125
- ),
126
- quantityUnits: z
127
- .object(comboSchema)
128
- .nullable()
129
- .refine(
130
- (value) => {
131
- if (requireOutpatientQuantity && !value) {
132
- return false;
133
- }
134
- return true;
135
- },
136
- {
137
- message: t('selectQuantityUnitsErrorMessage', 'Quantity unit is required'),
138
- },
139
- ),
140
- numRefills: z
141
- .number()
142
- .nullable()
143
- .refine(
144
- (value) => {
145
- if (requireOutpatientQuantity && (typeof value !== 'number' || value < 0)) {
146
- return false;
147
- }
148
- return true;
149
- },
150
- {
151
- message: t('numRefillsErrorMessage', 'Number of refills is required'),
152
- },
153
- ),
154
- };
155
-
156
- const nonFreeTextDosageSchema = z.object({
157
- ...baseSchemaFields,
158
- ...outpatientDrugOrderFields,
159
- isFreeTextDosage: z.literal(false),
160
- freeTextDosage: z.string().optional(),
161
- });
162
-
163
- const freeTextDosageSchema = z.object({
164
- ...baseSchemaFields,
165
- ...outpatientDrugOrderFields,
166
- isFreeTextDosage: z.literal(true),
167
- dosage: z.number().nullable(),
168
- unit: z.object(comboSchema).nullable(),
169
- route: z.object(comboSchema).nullable(),
170
- frequency: z.object(comboSchema).nullable(),
171
- });
172
-
173
- return z.discriminatedUnion('isFreeTextDosage', [nonFreeTextDosageSchema, freeTextDosageSchema]);
174
- }, [requireIndication, requireOutpatientQuantity, t]);
175
-
176
- return schema;
70
+ /**
71
+ * If true, allows user to select the prescribing provider when ordering medications from the order basket.
72
+ * (Otherwise, the prescribing provider always defaults to the current user.)
73
+ * The `prescriberProviderRoles` config must be set for this to work properly.
74
+ */
75
+ allowSelectingPrescribingClinician: boolean;
76
+ allowSelectingDrug: boolean;
177
77
  }
178
78
 
179
- type MedicationOrderFormData = z.infer<ReturnType<typeof useCreateMedicationOrderFormSchema>>;
180
-
181
79
  function MedicationInfoHeader({
182
- orderBasketItem,
80
+ drug,
183
81
  routeValue,
184
82
  unitValue,
185
83
  dosage,
186
84
  }: {
187
- orderBasketItem: DrugOrderBasketItem;
85
+ drug: Drug;
188
86
  routeValue: string;
189
87
  unitValue: string;
190
88
  dosage: number | null;
@@ -194,11 +92,11 @@ function MedicationInfoHeader({
194
92
  return (
195
93
  <div className={styles.medicationInfo} id="medicationInfo">
196
94
  <strong className={styles.productiveHeading02}>
197
- {orderBasketItem?.drug?.display} {orderBasketItem?.drug?.strength && `(${orderBasketItem.drug?.strength})`}
95
+ {drug?.display} {drug?.strength && `(${drug?.strength})`}
198
96
  </strong>{' '}
199
97
  <span className={styles.bodyLong01}>
200
98
  {routeValue && <>&mdash; {routeValue}</>}{' '}
201
- {orderBasketItem?.drug?.dosageForm?.display && <>&mdash; {orderBasketItem?.drug?.dosageForm?.display}</>}{' '}
99
+ {drug?.dosageForm?.display && <>&mdash; {drug?.dosageForm?.display}</>}{' '}
202
100
  </span>
203
101
  {dosage && unitValue ? (
204
102
  <>
@@ -210,7 +108,7 @@ function MedicationInfoHeader({
210
108
  </strong>
211
109
  </>
212
110
  ) : null}{' '}
213
- <ExtensionSlot name="medication-info-slot" state={{ order: orderBasketItem }} />
111
+ <ExtensionSlot name="medication-info-slot" state={{ order: { drug } as DrugOrderBasketItem }} />
214
112
  </div>
215
113
  );
216
114
  }
@@ -224,54 +122,67 @@ function InputWrapper({ children }) {
224
122
  );
225
123
  }
226
124
 
227
- export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, promptBeforeClosing }: DrugOrderFormProps) {
125
+ export function DrugOrderForm({
126
+ initialOrderBasketItem,
127
+ patient,
128
+ onSave,
129
+ saveButtonText,
130
+ onCancel,
131
+ promptBeforeClosing,
132
+ allowSelectingPrescribingClinician,
133
+ allowSelectingDrug,
134
+ }: DrugOrderFormProps) {
228
135
  const { t } = useTranslation();
229
- const config = useConfig<ConfigObject>();
136
+ const { daysDurationUnit, prescriberProviderRoles } = useConfig<ConfigObject>();
230
137
  const isTablet = useLayoutType() === 'tablet';
231
138
  const { orderConfigObject, error: errorFetchingOrderConfig } = useOrderConfig();
232
139
 
233
- const defaultStartDate = useMemo(() => {
234
- if (typeof initialOrderBasketItem?.startDate === 'string') parseDate(initialOrderBasketItem?.startDate);
235
-
236
- return initialOrderBasketItem?.startDate as Date;
237
- }, [initialOrderBasketItem?.startDate]);
238
-
239
- const medicationOrderFormSchema = useCreateMedicationOrderFormSchema();
140
+ const isProviderManagementModuleInstalled = useFeatureFlag('providermanagement-module');
141
+ const allowAndSupportSelectingPrescribingClinician =
142
+ isProviderManagementModuleInstalled && allowSelectingPrescribingClinician;
240
143
 
144
+ const {
145
+ data: providers,
146
+ isLoading: isLoadingProviders,
147
+ error: errorLoadingProviders,
148
+ } = useProviders(allowAndSupportSelectingPrescribingClinician ? prescriberProviderRoles : null);
149
+ const [isSaving, setIsSaving] = useState(false);
150
+
151
+ const { currentProvider } = useSession();
152
+ const defaultPrescribingProvider = allowAndSupportSelectingPrescribingClinician
153
+ ? providers?.find((p) => p.uuid === currentProvider.uuid)
154
+ : currentProvider;
155
+ const drugOrderForm = useDrugOrderForm(initialOrderBasketItem, defaultPrescribingProvider?.uuid);
241
156
  const {
242
157
  control,
243
158
  formState: { isDirty },
244
159
  getValues,
160
+ reset,
245
161
  handleSubmit,
246
162
  setValue,
247
163
  watch,
248
- } = useForm<MedicationOrderFormData>({
249
- mode: 'all',
250
- resolver: zodResolver(medicationOrderFormSchema),
251
- defaultValues: {
252
- isFreeTextDosage: initialOrderBasketItem?.isFreeTextDosage,
253
- freeTextDosage: initialOrderBasketItem?.freeTextDosage,
254
- dosage: initialOrderBasketItem?.dosage ?? null,
255
- unit: initialOrderBasketItem?.unit,
256
- route: initialOrderBasketItem?.route,
257
- patientInstructions: initialOrderBasketItem?.patientInstructions,
258
- asNeeded: initialOrderBasketItem?.asNeeded,
259
- asNeededCondition: initialOrderBasketItem?.asNeededCondition,
260
- duration: initialOrderBasketItem?.duration,
261
- durationUnit: initialOrderBasketItem?.durationUnit,
262
- pillsDispensed: initialOrderBasketItem?.pillsDispensed ?? null,
263
- quantityUnits: initialOrderBasketItem?.quantityUnits,
264
- numRefills: initialOrderBasketItem?.numRefills ?? null,
265
- indication: initialOrderBasketItem?.indication,
266
- frequency: initialOrderBasketItem?.frequency,
267
- startDate: defaultStartDate,
268
- },
269
- });
164
+ } = drugOrderForm;
270
165
 
271
166
  useEffect(() => {
272
167
  promptBeforeClosing(() => isDirty);
273
168
  }, [isDirty, promptBeforeClosing]);
274
169
 
170
+ // reset the dosage information if set to free text dosage
171
+ const handleIsFreeTextDosageAfterChange = useCallback(
172
+ (newValue: MedicationOrderFormData['isFreeTextDosage']) => {
173
+ if (newValue) {
174
+ setValue('dosage', null, { shouldValidate: true });
175
+ setValue('unit', null, { shouldValidate: true });
176
+ setValue('route', null, { shouldValidate: true });
177
+ setValue('frequency', null, { shouldValidate: true });
178
+ setValue('patientInstructions', null, { shouldValidate: true });
179
+ } else {
180
+ setValue('freeTextDosage', null, { shouldValidate: true });
181
+ }
182
+ },
183
+ [setValue],
184
+ );
185
+
275
186
  const handleUnitAfterChange = useCallback(
276
187
  (newValue: MedicationOrderFormData['unit'], prevValue: MedicationOrderFormData['unit']) => {
277
188
  if (prevValue?.valueCoded === getValues('quantityUnits')?.valueCoded) {
@@ -281,13 +192,18 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
281
192
  [setValue, getValues],
282
193
  );
283
194
 
195
+ const drug = watch('drug') as Drug;
284
196
  const routeValue = watch('route')?.value;
285
197
  const unitValue = watch('unit')?.value;
286
198
  const dosage = watch('dosage');
199
+ const startDate = watch('startDate');
287
200
 
288
- const handleFormSubmission = (data: MedicationOrderFormData) => {
201
+ const handleFormSubmission = async (data: MedicationOrderFormData) => {
289
202
  const newBasketItems = {
290
203
  ...initialOrderBasketItem,
204
+ drug: data.drug,
205
+ orderer: data.orderer.uuid,
206
+ careSetting: data.careSetting,
291
207
  isFreeTextDosage: data.isFreeTextDosage,
292
208
  freeTextDosage: data.freeTextDosage,
293
209
  dosage: data.dosage,
@@ -304,8 +220,23 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
304
220
  indication: data.indication,
305
221
  frequency: data.frequency,
306
222
  startDate: data.startDate,
307
- };
308
- onSave(newBasketItems as DrugOrderBasketItem);
223
+ } as DrugOrderBasketItem;
224
+ setIsSaving(true);
225
+ await onSave(newBasketItems);
226
+ setIsSaving(false);
227
+ };
228
+
229
+ const handleFormSubmissionError = (errors: FieldErrors<MedicationOrderFormData>) => {
230
+ if (errors) {
231
+ console.error('Error in drug order form', errors);
232
+ showSnackbar({
233
+ title: t('drugOrderValidationFailed', 'Validation failed'),
234
+ subtitle: t('drugOrderValidationFailedDescription', 'Please check the form for errors and try again.'),
235
+ kind: 'error',
236
+ timeoutInMs: 5000,
237
+ isLowContrast: true,
238
+ });
239
+ }
309
240
  };
310
241
 
311
242
  const drugDosingUnits: Array<DosingUnit> = useMemo(
@@ -336,11 +267,11 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
336
267
  () =>
337
268
  orderConfigObject?.durationUnits ?? [
338
269
  {
339
- valueCoded: config?.daysDurationUnit?.uuid,
340
- value: config?.daysDurationUnit?.display,
270
+ valueCoded: daysDurationUnit?.uuid,
271
+ value: daysDurationUnit?.display,
341
272
  },
342
273
  ],
343
- [orderConfigObject, config?.daysDurationUnit],
274
+ [orderConfigObject, daysDurationUnit],
344
275
  );
345
276
 
346
277
  const orderFrequencies: Array<MedicationFrequency> = useMemo(() => {
@@ -351,6 +282,10 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
351
282
  return menu?.item?.value?.toLowerCase().includes(menu?.inputValue?.toLowerCase());
352
283
  }, []);
353
284
 
285
+ const filterItemsByProviderName = useCallback((menu) => {
286
+ return menu?.item?.person?.display?.toLowerCase().includes(menu?.inputValue?.toLowerCase());
287
+ }, []);
288
+
354
289
  const filterItemsBySynonymNames = useCallback((menu) => {
355
290
  if (menu?.inputValue?.length) {
356
291
  return menu.item?.names?.some((abbr: string) => abbr.toLowerCase().includes(menu.inputValue.toLowerCase()));
@@ -359,7 +294,6 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
359
294
  }, []);
360
295
 
361
296
  const [showStickyMedicationHeader, setShowMedicationHeader] = useState(false);
362
- const { patient } = usePatientChartStore();
363
297
  const patientName = patient ? getPatientName(patient) : '';
364
298
 
365
299
  const observer = useRef(null);
@@ -384,30 +318,38 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
384
318
  },
385
319
  [setShowMedicationHeader],
386
320
  );
321
+ const {
322
+ fieldState: { error: drugFieldError },
323
+ } = useController<MedicationOrderFormData>({ name: 'drug', control });
324
+
325
+ // TODO: use the backend instead of this to determine whether the drug formulation can be ordered
326
+ // See: https://openmrs.atlassian.net/browse/RESTWS-1003
327
+ const { data: activeOrders } = useActivePatientOrders(patient.id);
328
+ const drugAlreadyPrescribedForNewOrder = useMemo(
329
+ () => initialOrderBasketItem.action == 'NEW' && activeOrders?.some((order) => order?.drug?.uuid === drug?.uuid),
330
+ [activeOrders, drug, initialOrderBasketItem.action],
331
+ );
387
332
 
388
333
  return (
389
334
  <div className={styles.container}>
390
335
  {showStickyMedicationHeader && (
391
336
  <div className={styles.stickyMedicationInfo}>
392
- <MedicationInfoHeader
393
- dosage={dosage}
394
- orderBasketItem={initialOrderBasketItem}
395
- routeValue={routeValue}
396
- unitValue={unitValue}
397
- />
337
+ <MedicationInfoHeader dosage={dosage} drug={drug} routeValue={routeValue} unitValue={unitValue} />
398
338
  </div>
399
339
  )}
400
- {isTablet && (
401
- <div className={styles.patientHeader}>
402
- <span className={styles.bodyShort02}>{patientName}</span>
403
- <span className={classNames(styles.text02, styles.bodyShort01)}>
404
- {capitalize(patient?.gender)} &middot; {age(patient?.birthDate)} &middot;{' '}
405
- <span>{formatDate(parseDate(patient?.birthDate), { mode: 'wide', time: false })}</span>
406
- </span>
407
- <ExtensionSlot name="allergy-list-pills-slot" state={{ patientUuid: patient?.id }} />
408
- </div>
409
- )}
410
- <Form className={styles.orderForm} onSubmit={handleSubmit(handleFormSubmission)} id="drugOrderForm">
340
+ <div className={styles.patientHeader}>
341
+ <span className={styles.bodyShort02}>{patientName}</span>
342
+ <span className={classNames(styles.text02, styles.bodyShort01)}>
343
+ {capitalize(patient?.gender)} &middot; {age(patient?.birthDate)} &middot;{' '}
344
+ <span>{formatDate(parseDate(patient?.birthDate), { mode: 'wide', time: false })}</span>
345
+ </span>
346
+ </div>
347
+ <ExtensionSlot name="allergy-list-pills-slot" state={{ patientUuid: patient?.id }} />
348
+ <Form
349
+ className={styles.orderForm}
350
+ onSubmit={handleSubmit(handleFormSubmission, handleFormSubmissionError)}
351
+ id="drugOrderForm"
352
+ >
411
353
  <div>
412
354
  {errorFetchingOrderConfig && (
413
355
  <InlineNotification
@@ -418,36 +360,73 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
418
360
  subtitle={t('tryReopeningTheForm', 'Please try launching the form again')}
419
361
  />
420
362
  )}
421
- {!isTablet && (
422
- <div>
423
- <div className={styles.backButton}>
424
- <Button
425
- kind="ghost"
426
- renderIcon={(props: ComponentProps<typeof ArrowLeftIcon>) => <ArrowLeftIcon size={24} {...props} />}
427
- iconDescription="Return to order basket"
428
- size="sm"
429
- onClick={onCancel}
430
- >
431
- <span>{t('backToOrderBasket', 'Back to order basket')}</span>
432
- </Button>
433
- </div>
434
- <ExtensionSlot name="allergy-list-pills-slot" state={{ patientUuid: patient?.id }} />
435
- </div>
436
- )}
437
-
438
363
  <h1 className={styles.orderFormHeading}>{t('orderForm', 'Order Form')}</h1>
364
+ {(allowSelectingDrug || allowSelectingPrescribingClinician) && (
365
+ <section className={styles.formSection}>
366
+ <h3 className={styles.sectionHeader}>{t('prescriptionInfo', 'Prescription info')}</h3>
367
+ <Stack gap={5}>
368
+ {allowSelectingDrug && (
369
+ <InputWrapper>
370
+ <DrugSearchComboBox
371
+ setSelectedDrugItem={(item) => {
372
+ // when selecting a new drug, it can have its own order template that populates many fields
373
+ // we should just reset the entire form
374
+ reset(drugOrderBasketItemToFormValue(item, startDate, currentProvider.uuid));
375
+ }}
376
+ />
377
+ {drugAlreadyPrescribedForNewOrder && (
378
+ <FormLabel className={styles.errorLabel}>
379
+ {t('activePrescriptionExists', 'Active prescription exists for this drug')}
380
+ </FormLabel>
381
+ )}
382
+ <FormLabel className={styles.errorLabel}>{drugFieldError?.message}</FormLabel>
383
+ </InputWrapper>
384
+ )}
385
+ {allowAndSupportSelectingPrescribingClinician &&
386
+ !isLoadingProviders &&
387
+ (providers?.length > 0 ? (
388
+ <ControlledFieldInput
389
+ control={control}
390
+ name="orderer"
391
+ type="comboBox"
392
+ getValues={getValues}
393
+ id="orderer"
394
+ shouldFilterItem={filterItemsByProviderName}
395
+ placeholder={t('prescribingClinician', 'Prescribing Clinician')}
396
+ titleText={t('prescribingClinician', 'Prescribing Clinician')}
397
+ items={providers}
398
+ itemToString={(item: Provider) => item?.person?.display}
399
+ />
400
+ ) : errorLoadingProviders ? (
401
+ <InlineNotification
402
+ kind="warning"
403
+ lowContrast
404
+ className={styles.inlineNotification}
405
+ title={t('errorLoadingClinicians', 'Error loading clinicians')}
406
+ subtitle={t('tryReopeningTheForm', 'Please try launching the form again')}
407
+ />
408
+ ) : (
409
+ <InlineNotification
410
+ kind="warning"
411
+ lowContrast
412
+ className={styles.inlineNotification}
413
+ title={t('noCliniciansFound', 'No clinicians found')}
414
+ subtitle={t(
415
+ 'noCliniciansFoundDescription',
416
+ 'Cannot select prescribing clinician because no clinicians with appropriate roles are found. Check configuration.',
417
+ )}
418
+ />
419
+ ))}
420
+ </Stack>
421
+ </section>
422
+ )}
439
423
  <div ref={medicationInfoHeaderRef}>
440
- <MedicationInfoHeader
441
- dosage={dosage}
442
- orderBasketItem={initialOrderBasketItem}
443
- routeValue={routeValue}
444
- unitValue={unitValue}
445
- />
424
+ <MedicationInfoHeader dosage={dosage} drug={drug} routeValue={routeValue} unitValue={unitValue} />
446
425
  </div>
447
426
  <section className={styles.formSection}>
448
427
  <Grid className={styles.gridRow}>
449
428
  <Column lg={12} md={6} sm={4}>
450
- <h3 className={styles.sectionHeader}>{t('dosageInstructions', '1. Dosage instructions')}</h3>
429
+ <h3 className={styles.sectionHeader}>{t('dosageInstructions', 'Dosage instructions')}</h3>
451
430
  </Column>
452
431
  <Column className={styles.freeTextDosageToggle} lg={4} md={2} sm={4}>
453
432
  <ControlledFieldInput
@@ -458,6 +437,7 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
458
437
  id="freeTextDosageToggle"
459
438
  aria-label={t('freeTextDosage', 'Free text dosage')}
460
439
  labelText={t('freeTextDosage', 'Free text dosage')}
440
+ handleAfterChange={handleIsFreeTextDosageAfterChange}
461
441
  />
462
442
  </Column>
463
443
  </Grid>
@@ -599,7 +579,7 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
599
579
  )}
600
580
  </section>
601
581
  <section className={styles.formSection}>
602
- <h3 className={styles.sectionHeader}>{t('prescriptionDuration', '2. Prescription duration')}</h3>
582
+ <h3 className={styles.sectionHeader}>{t('prescriptionDuration', 'Prescription duration')}</h3>
603
583
  <Grid className={styles.gridRow}>
604
584
  {/* TODO: This input does nothing */}
605
585
  <Column lg={16} md={4} sm={4}>
@@ -665,7 +645,7 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
665
645
  </Grid>
666
646
  </section>
667
647
  <section className={styles.formSection}>
668
- <h3 className={styles.sectionHeader}>{t('dispensingInformation', '3. Dispensing instructions')}</h3>
648
+ <h3 className={styles.sectionHeader}>{t('dispensingInformation', 'Dispensing instructions')}</h3>
669
649
  <Grid className={styles.gridRow}>
670
650
  <Column lg={8} md={3} sm={4}>
671
651
  <InputWrapper>
@@ -739,9 +719,7 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
739
719
  </section>
740
720
  </div>
741
721
 
742
- <ButtonSet
743
- className={classNames(styles.buttonSet, isTablet ? styles.tabletButtonSet : styles.desktopButtonSet)}
744
- >
722
+ <ButtonSet className={styles.buttonSet}>
745
723
  <Button className={styles.button} kind="secondary" onClick={onCancel} size="xl">
746
724
  {t('discard', 'Discard')}
747
725
  </Button>
@@ -750,9 +728,9 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
750
728
  kind="primary"
751
729
  type="submit"
752
730
  size="xl"
753
- disabled={!!errorFetchingOrderConfig}
731
+ disabled={!!errorFetchingOrderConfig || isSaving || drugAlreadyPrescribedForNewOrder}
754
732
  >
755
- {t('saveOrder', 'Save order')}
733
+ {saveButtonText}
756
734
  </Button>
757
735
  </ButtonSet>
758
736
  </Form>
@@ -795,7 +773,9 @@ const CustomNumberInput = ({ setValue, control, name, labelText, isTablet, ...in
795
773
 
796
774
  return (
797
775
  <div className={styles.customElement}>
798
- <span className="cds--label">{labelText}</span>
776
+ <span className="cds--label" id={`${name}-label`}>
777
+ {labelText}
778
+ </span>
799
779
  <div className={styles.customNumberInput}>
800
780
  <IconButton onClick={decrement} label={t('decrement', 'Decrement')} size={responsiveSize}>
801
781
  <Subtract size={16} />
@@ -809,6 +789,7 @@ const CustomNumberInput = ({ setValue, control, name, labelText, isTablet, ...in
809
789
  size={responsiveSize}
810
790
  id={name}
811
791
  labelText=""
792
+ aria-labelledby={`${name}-label`}
812
793
  {...inputProps}
813
794
  />
814
795
  <IconButton onClick={increment} label={t('increment', 'Increment')} size={responsiveSize}>
@@ -847,12 +828,12 @@ const ControlledFieldInput = ({
847
828
  handleAfterChange,
848
829
  ...restProps
849
830
  }: ControlledFieldInputProps) => {
831
+ const { t } = useTranslation();
850
832
  const {
851
833
  field: { onBlur, onChange, value, ref },
852
834
  fieldState: { error },
853
835
  } = useController<MedicationOrderFormData>({ name, control });
854
836
  const isTablet = useLayoutType() === 'tablet';
855
- const responsiveSize = isTablet ? 'md' : 'sm';
856
837
 
857
838
  const fieldErrorStyles = classNames({
858
839
  [styles.fieldError]: error?.message,
@@ -876,6 +857,8 @@ const ControlledFieldInput = ({
876
857
  ref={ref}
877
858
  // @ts-ignore
878
859
  size={isTablet ? 'md' : 'sm'}
860
+ labelA={t('on', 'On')}
861
+ labelB={t('off', 'Off')}
879
862
  {...restProps}
880
863
  />
881
864
  );
@@ -953,13 +936,14 @@ const ControlledFieldInput = ({
953
936
  ref={ref}
954
937
  size={isTablet ? 'md' : 'sm'}
955
938
  selectedItem={value}
939
+ initialSelectedItem={value}
956
940
  {...comboBoxProps}
957
941
  />
958
942
  );
959
943
  }
960
944
 
961
945
  return null;
962
- }, [type, value, restProps, handleChange, fieldErrorStyles, onBlur, ref, isTablet]);
946
+ }, [type, value, restProps, handleChange, fieldErrorStyles, onBlur, ref, isTablet, t]);
963
947
 
964
948
  return (
965
949
  <>
@@ -968,3 +952,5 @@ const ControlledFieldInput = ({
968
952
  </>
969
953
  );
970
954
  };
955
+
956
+ export default DrugOrderForm;