@kenyaemr/esm-patient-clinical-view-app 5.4.2-pre.2714 → 5.4.2-pre.2722

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 (66) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/dist/805.js +1 -0
  3. package/dist/805.js.map +1 -0
  4. package/dist/kenyaemr-esm-patient-clinical-view-app.js +2 -2
  5. package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +27 -27
  6. package/dist/main.js +27 -27
  7. package/dist/main.js.map +1 -1
  8. package/dist/routes.json +1 -1
  9. package/package.json +1 -1
  10. package/src/config-schema.ts +97 -0
  11. package/src/contact-list/contact-tracing-history.component.tsx +18 -15
  12. package/src/maternal-and-child-health/partography/components/pulse-bp-graph.component.tsx +1 -0
  13. package/src/maternal-and-child-health/partography/components/temperature-graph.component.tsx +218 -0
  14. package/src/maternal-and-child-health/partography/components/uterine-contractions-graph.component.tsx +209 -0
  15. package/src/maternal-and-child-health/partography/forms/cervical-contractions-form.component.tsx +211 -0
  16. package/src/maternal-and-child-health/partography/forms/cervix-form.component.tsx +354 -0
  17. package/src/maternal-and-child-health/partography/forms/drugs-iv-fluids-form.component.tsx +321 -0
  18. package/src/maternal-and-child-health/partography/forms/fetal-heart-rate-form.component.tsx +275 -0
  19. package/src/maternal-and-child-health/partography/forms/index.ts +9 -0
  20. package/src/maternal-and-child-health/partography/forms/membrane-amniotic-fluid-form.component.tsx +330 -0
  21. package/src/maternal-and-child-health/partography/forms/oxytocin-form.component.tsx +207 -0
  22. package/src/maternal-and-child-health/partography/forms/pulse-bp-form.component.tsx +174 -0
  23. package/src/maternal-and-child-health/partography/forms/temperature-form.component.tsx +210 -0
  24. package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.component.tsx +218 -0
  25. package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.scss +107 -0
  26. package/src/maternal-and-child-health/partography/forms/time-picker-with-clock.component.tsx +174 -0
  27. package/src/maternal-and-child-health/partography/forms/time-picker-with-clock.scss +178 -0
  28. package/src/maternal-and-child-health/partography/forms/urine-test-form.component.tsx +255 -0
  29. package/src/maternal-and-child-health/partography/forms/useCervixData.ts +16 -0
  30. package/src/maternal-and-child-health/partography/graphs/cervical-contractions-graph.component.tsx +266 -0
  31. package/src/maternal-and-child-health/partography/graphs/cervix-graph.component.tsx +429 -0
  32. package/src/maternal-and-child-health/partography/graphs/drugs-iv-fluids-graph-wrapper.component.tsx +163 -0
  33. package/src/maternal-and-child-health/partography/graphs/drugs-iv-fluids-graph.component.tsx +82 -0
  34. package/src/maternal-and-child-health/partography/graphs/fetal-heart-rate-graph.component.tsx +359 -0
  35. package/src/maternal-and-child-health/partography/graphs/index.ts +10 -0
  36. package/src/maternal-and-child-health/partography/graphs/membrane-amniotic-fluid-graph.component.tsx +266 -0
  37. package/src/maternal-and-child-health/partography/graphs/oxytocin-graph-wrapper.component.tsx +190 -0
  38. package/src/maternal-and-child-health/partography/graphs/oxytocin-graph.component.tsx +126 -0
  39. package/src/maternal-and-child-health/partography/graphs/partograph-graph.component.tsx +266 -0
  40. package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph-wrapper.component.tsx +298 -0
  41. package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph.component.tsx +267 -0
  42. package/src/maternal-and-child-health/partography/graphs/temperature-graph.component.tsx +242 -0
  43. package/src/maternal-and-child-health/partography/graphs/urine-test-graph.component.tsx +246 -0
  44. package/src/maternal-and-child-health/partography/partograph.component.tsx +2141 -118
  45. package/src/maternal-and-child-health/partography/partography-dashboard.meta.ts +8 -0
  46. package/src/maternal-and-child-health/partography/partography-data-form.scss +163 -0
  47. package/src/maternal-and-child-health/partography/partography.resource.ts +233 -326
  48. package/src/maternal-and-child-health/partography/partography.scss +1341 -3
  49. package/src/maternal-and-child-health/partography/resources/blood-pressure.resource.ts +96 -0
  50. package/src/maternal-and-child-health/partography/resources/cervical-dilation.resource.ts +109 -0
  51. package/src/maternal-and-child-health/partography/resources/cervix.resource.ts +362 -0
  52. package/src/maternal-and-child-health/partography/resources/descent-of-head.resource.ts +101 -0
  53. package/src/maternal-and-child-health/partography/resources/drugs-fluids.resource.ts +88 -0
  54. package/src/maternal-and-child-health/partography/resources/fetal-heart-rate.resource.ts +122 -0
  55. package/src/maternal-and-child-health/partography/resources/maternal-pulse.resource.ts +77 -0
  56. package/src/maternal-and-child-health/partography/resources/membrane-amniotic-fluid.resource.ts +108 -0
  57. package/src/maternal-and-child-health/partography/resources/oxytocin.resource.ts +159 -0
  58. package/src/maternal-and-child-health/partography/resources/progress-events.resource.ts +6 -0
  59. package/src/maternal-and-child-health/partography/resources/pulse-bp-combined.resource.ts +53 -0
  60. package/src/maternal-and-child-health/partography/resources/temperature.resource.ts +84 -0
  61. package/src/maternal-and-child-health/partography/resources/uterine-contractions.resource.ts +173 -0
  62. package/src/maternal-and-child-health/partography/table/temperature-table.component.tsx +99 -0
  63. package/src/maternal-and-child-health/partography/table/uterine-contractions-table.component.tsx +86 -0
  64. package/src/maternal-and-child-health/partography/types/index.ts +319 -101
  65. package/dist/397.js +0 -1
  66. package/dist/397.js.map +0 -1
@@ -0,0 +1,354 @@
1
+ import React, { useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useForm, Controller } from 'react-hook-form';
4
+ import { Button, Modal, Grid, Column, NumberInput, Select, SelectItem } from '@carbon/react';
5
+ import TimePickerDropdown from './time-picker-dropdown.component';
6
+ import styles from '../partography-data-form.scss';
7
+
8
+ type CervixFormData = {
9
+ hour: string;
10
+ time: string;
11
+ cervicalDilation: string;
12
+ descent: string;
13
+ };
14
+
15
+ type CervixFormProps = {
16
+ isOpen: boolean;
17
+ onClose: () => void;
18
+ onSubmit: (data: { hour: number; time: string; cervicalDilation: number; descentOfHead: number }) => void;
19
+ onDataSaved?: () => void;
20
+ selectedHours?: number[];
21
+ existingTimeEntries?: Array<{ hour: number; time: string }>;
22
+ existingCervixData?: Array<{ cervicalDilation: number; descentOfHead: number }>;
23
+ patient?: {
24
+ uuid: string;
25
+ name: string;
26
+ gender: string;
27
+ age: string;
28
+ };
29
+ };
30
+
31
+ const CervixForm: React.FC<CervixFormProps> = ({
32
+ isOpen,
33
+ onClose,
34
+ onSubmit,
35
+ onDataSaved,
36
+ selectedHours = [],
37
+ existingTimeEntries = [],
38
+ existingCervixData = [],
39
+ patient,
40
+ }) => {
41
+ const { t } = useTranslation();
42
+
43
+ const { control, handleSubmit, reset, setError, clearErrors } = useForm<CervixFormData>({
44
+ defaultValues: {
45
+ hour: '',
46
+ time: '',
47
+ cervicalDilation: '',
48
+ descent: '5',
49
+ },
50
+ });
51
+
52
+ const validationLimits = useMemo(() => {
53
+ if (!existingCervixData || existingCervixData.length === 0) {
54
+ return {
55
+ cervicalDilationMin: 0,
56
+ descentOfHeadMax: 5,
57
+ };
58
+ }
59
+
60
+ const maxCervicalDilation = Math.max(...existingCervixData.map((data) => data.cervicalDilation));
61
+ const minDescentOfHead = Math.min(...existingCervixData.map((data) => data.descentOfHead));
62
+
63
+ return {
64
+ cervicalDilationMin: maxCervicalDilation,
65
+ descentOfHeadMax: minDescentOfHead,
66
+ };
67
+ }, [existingCervixData]);
68
+
69
+ const maxSelectedHour = useMemo(
70
+ () => (selectedHours && selectedHours.length > 0 ? Math.max(...selectedHours) : -1),
71
+ [selectedHours],
72
+ );
73
+
74
+ const hourOptions = useMemo(() => {
75
+ return Array.from({ length: 24 }, (_, i) => {
76
+ const hourValue = String(i).padStart(2, '0');
77
+ const isDisabled = i <= maxSelectedHour;
78
+ const displayText = isDisabled ? `${hourValue} (used)` : hourValue;
79
+
80
+ return {
81
+ value: hourValue,
82
+ text: displayText,
83
+ disabled: isDisabled,
84
+ };
85
+ });
86
+ }, [maxSelectedHour]);
87
+
88
+ const onSubmitForm = async (data: CervixFormData) => {
89
+ if (!data.hour || data.hour === '') {
90
+ setError('hour', {
91
+ type: 'manual',
92
+ message: 'Hour selection is required',
93
+ });
94
+ return;
95
+ }
96
+
97
+ if (!data.time || data.time === '') {
98
+ setError('time', {
99
+ type: 'manual',
100
+ message: 'Time selection is required',
101
+ });
102
+ return;
103
+ }
104
+
105
+ if (!data.cervicalDilation || data.cervicalDilation === '') {
106
+ setError('cervicalDilation', {
107
+ type: 'manual',
108
+ message: 'Cervical dilation is required',
109
+ });
110
+ return;
111
+ }
112
+
113
+ if (!data.descent || data.descent === '') {
114
+ setError('descent', {
115
+ type: 'manual',
116
+ message: 'Descent of head is required',
117
+ });
118
+ return;
119
+ }
120
+
121
+ const hourValue = parseInt(data.hour);
122
+ const cervicalDilation = parseFloat(data.cervicalDilation);
123
+ const descentOfHead = parseInt(data.descent);
124
+
125
+ if (isNaN(hourValue)) {
126
+ setError('hour', {
127
+ type: 'manual',
128
+ message: 'Invalid hour value',
129
+ });
130
+ return;
131
+ }
132
+
133
+ if (isNaN(cervicalDilation)) {
134
+ setError('cervicalDilation', {
135
+ type: 'manual',
136
+ message: 'Invalid cervical dilation value',
137
+ });
138
+ return;
139
+ }
140
+
141
+ if (isNaN(descentOfHead)) {
142
+ setError('descent', {
143
+ type: 'manual',
144
+ message: 'Invalid descent of head value',
145
+ });
146
+ return;
147
+ }
148
+
149
+ if (cervicalDilation < validationLimits.cervicalDilationMin) {
150
+ setError('cervicalDilation', {
151
+ type: 'manual',
152
+ message: `Cervical dilation cannot be less than previous measurement (${validationLimits.cervicalDilationMin}cm)`,
153
+ });
154
+ return;
155
+ }
156
+
157
+ if (cervicalDilation > 10) {
158
+ setError('cervicalDilation', {
159
+ type: 'manual',
160
+ message: 'Cervical dilation cannot exceed 10cm',
161
+ });
162
+ return;
163
+ }
164
+
165
+ if (descentOfHead < 1) {
166
+ setError('descent', {
167
+ type: 'manual',
168
+ message: 'Descent of head cannot be less than 1 (most descended)',
169
+ });
170
+ return;
171
+ }
172
+ if (descentOfHead > 5) {
173
+ setError('descent', {
174
+ type: 'manual',
175
+ message: 'Descent of head cannot exceed 5',
176
+ });
177
+ return;
178
+ }
179
+
180
+ clearErrors();
181
+
182
+ onSubmit({
183
+ hour: hourValue,
184
+ time: data.time,
185
+ cervicalDilation: cervicalDilation,
186
+ descentOfHead: descentOfHead,
187
+ });
188
+
189
+ reset();
190
+ onClose();
191
+ };
192
+
193
+ const handleClose = () => {
194
+ reset();
195
+ onClose();
196
+ };
197
+
198
+ const patientLabel = useMemo(
199
+ () => (patient ? `${patient.name}, ${patient.gender}, ${patient.age}` : 'Patient Information'),
200
+ [patient],
201
+ );
202
+
203
+ return (
204
+ <Modal
205
+ open={isOpen}
206
+ onRequestClose={handleClose}
207
+ modalHeading="Cervical Dilation & Descent of Head"
208
+ modalLabel={patientLabel}
209
+ primaryButtonText={t('save', 'Save')}
210
+ secondaryButtonText={t('cancel', 'Cancel')}
211
+ onRequestSubmit={handleSubmit(onSubmitForm)}
212
+ onSecondarySubmit={handleClose}
213
+ size="md">
214
+ <div className={styles.modalContent}>
215
+ <div className={styles.requiredFieldsNote}>
216
+ <p>* All fields are required</p>
217
+ </div>
218
+ <Grid>
219
+ <Column sm={4} md={4} lg={8}>
220
+ <Controller
221
+ name="hour"
222
+ control={control}
223
+ rules={{
224
+ required: 'Hour selection is required',
225
+ }}
226
+ render={({ field, fieldState }) => (
227
+ <Select
228
+ id="hour-select"
229
+ labelText="Hour *"
230
+ value={field.value}
231
+ onChange={(e) => field.onChange((e.target as HTMLSelectElement).value)}
232
+ invalid={!!fieldState.error}
233
+ invalidText={fieldState.error?.message}>
234
+ <SelectItem value="" text="Select hour" />
235
+ {hourOptions.map((option) => (
236
+ <SelectItem key={option.value} value={option.value} text={option.text} disabled={option.disabled} />
237
+ ))}
238
+ </Select>
239
+ )}
240
+ />
241
+ </Column>
242
+ <Column sm={4} md={4} lg={8}>
243
+ <Controller
244
+ name="time"
245
+ control={control}
246
+ rules={{
247
+ required: 'Time selection is required',
248
+ }}
249
+ render={({ field, fieldState }) => (
250
+ <TimePickerDropdown
251
+ id="time-input"
252
+ labelText="Time *"
253
+ value={field.value}
254
+ onChange={(value) => field.onChange(value)}
255
+ invalid={!!fieldState.error}
256
+ invalidText={fieldState.error?.message}
257
+ existingTimeEntries={existingTimeEntries}
258
+ />
259
+ )}
260
+ />
261
+ </Column>
262
+
263
+ <Column sm={4} md={8} lg={16}>
264
+ <Controller
265
+ name="cervicalDilation"
266
+ control={control}
267
+ rules={{
268
+ required: 'Cervical dilation is required',
269
+ validate: {
270
+ isNumber: (value) => !isNaN(parseFloat(value)) || 'Must be a valid number',
271
+ minValue: (value) => {
272
+ const numValue = parseFloat(value);
273
+ return (
274
+ numValue >= validationLimits.cervicalDilationMin ||
275
+ `Cannot be less than previous measurement (${validationLimits.cervicalDilationMin}cm)`
276
+ );
277
+ },
278
+ maxValue: (value) => {
279
+ const numValue = parseFloat(value);
280
+ return numValue <= 10 || 'Cannot exceed 10cm';
281
+ },
282
+ },
283
+ }}
284
+ render={({ field, fieldState }) => (
285
+ <>
286
+ <NumberInput
287
+ id="cervical-dilation-input"
288
+ label="Cervical Dilation (cm) *"
289
+ placeholder={`Enter dilation (min: ${validationLimits.cervicalDilationMin}cm, max: 10cm)`}
290
+ value={field.value || ''}
291
+ onChange={(e, { value }) => field.onChange(String(value))}
292
+ min={validationLimits.cervicalDilationMin}
293
+ max={10}
294
+ step={0.5}
295
+ invalid={!!fieldState.error}
296
+ invalidText={fieldState.error?.message}
297
+ />
298
+ {existingCervixData.length > 0 && (
299
+ <div className={styles.validationHint}>
300
+ Previous highest: {validationLimits.cervicalDilationMin}cm (cannot go below this value)
301
+ </div>
302
+ )}
303
+ </>
304
+ )}
305
+ />
306
+ </Column>
307
+
308
+ <Column sm={4} md={8} lg={16}>
309
+ <Controller
310
+ name="descent"
311
+ control={control}
312
+ rules={{
313
+ required: 'Descent of head is required',
314
+ validate: {
315
+ isNumber: (value) => !isNaN(parseInt(value)) || 'Must be a valid number',
316
+ minValue: (value) => {
317
+ const numValue = parseInt(value);
318
+ return numValue >= 1 || 'Descent of head cannot be less than 1 (most descended)';
319
+ },
320
+ maxValue: (value) => {
321
+ const numValue = parseInt(value);
322
+ return numValue <= 5 || 'Descent of head cannot exceed 5';
323
+ },
324
+ },
325
+ }}
326
+ render={({ field, fieldState }) => (
327
+ <>
328
+ <NumberInput
329
+ id="descent-input"
330
+ label="Descent of Head *"
331
+ placeholder={
332
+ existingCervixData.length === 0
333
+ ? `Default: 5 (high position), can decrement to lower values`
334
+ : `Enter descent (1=most descended, 5=high position)`
335
+ }
336
+ value={field.value || '5'}
337
+ onChange={(e, { value }) => field.onChange(String(value))}
338
+ min={1}
339
+ max={5}
340
+ step={1}
341
+ invalid={!!fieldState.error}
342
+ invalidText={fieldState.error?.message}
343
+ />
344
+ </>
345
+ )}
346
+ />
347
+ </Column>
348
+ </Grid>
349
+ </div>
350
+ </Modal>
351
+ );
352
+ };
353
+
354
+ export default CervixForm;
@@ -0,0 +1,321 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useForm, Controller } from 'react-hook-form';
4
+ import { Button, Modal, Grid, Column, Dropdown, InlineNotification, TextInput, ButtonSkeleton } from '@carbon/react';
5
+ import { Add } from '@carbon/react/icons';
6
+ import { launchWorkspace } from '@openmrs/esm-framework';
7
+ import { saveDrugOrderData } from '../partography.resource';
8
+ import styles from '../partography-data-form.scss';
9
+ import { ROUTE_OPTIONS, FREQUENCY_OPTIONS } from '../types';
10
+
11
+ type DrugsIVFluidsFormData = {
12
+ drugName: string;
13
+ dosage: string;
14
+ route: string;
15
+ frequency: string;
16
+ };
17
+
18
+ type DrugsIVFluidsFormProps = {
19
+ isOpen: boolean;
20
+ onClose: () => void;
21
+ onSubmit: (data: { drugName: string; dosage: string; route: string; frequency: string }) => void;
22
+ onDataSaved?: () => void;
23
+ patient?: {
24
+ uuid: string;
25
+ name: string;
26
+ gender: string;
27
+ age: string;
28
+ };
29
+ };
30
+
31
+ const DrugsIVFluidsForm: React.FC<DrugsIVFluidsFormProps> = ({ isOpen, onClose, onSubmit, onDataSaved, patient }) => {
32
+ const { t } = useTranslation();
33
+ const [isSaving, setIsSaving] = useState(false);
34
+ const [saveError, setSaveError] = useState<string | null>(null);
35
+ const [saveSuccess, setSaveSuccess] = useState(false);
36
+
37
+ const {
38
+ control,
39
+ handleSubmit,
40
+ reset,
41
+ setError,
42
+ clearErrors,
43
+ formState: { errors },
44
+ } = useForm<DrugsIVFluidsFormData>({
45
+ defaultValues: {
46
+ drugName: '',
47
+ dosage: '',
48
+ route: '',
49
+ frequency: '',
50
+ },
51
+ });
52
+
53
+ const handleLaunchDrugOrderWorkspace = useCallback(() => {
54
+ if (patient?.uuid) {
55
+ launchWorkspace('add-drug-order', {
56
+ patientUuid: patient.uuid,
57
+ workspaceTitle: 'Add Drug Order',
58
+ onOrderSaved: (savedOrder) => {
59
+ if (onDataSaved) {
60
+ onDataSaved();
61
+ }
62
+
63
+ onClose();
64
+ },
65
+ });
66
+ }
67
+ }, [patient?.uuid, onDataSaved, onClose]);
68
+
69
+ const routeOptions = ROUTE_OPTIONS as unknown as { id: string; text: string }[];
70
+ const frequencyOptions = FREQUENCY_OPTIONS as unknown as { id: string; text: string }[];
71
+
72
+ const onSubmitForm = async (data: DrugsIVFluidsFormData) => {
73
+ clearErrors();
74
+ setSaveError(null);
75
+ setSaveSuccess(false);
76
+
77
+ if (!data.drugName || data.drugName === '') {
78
+ setError('drugName', {
79
+ type: 'manual',
80
+ message: t('drugNameRequired', 'Drug name is required'),
81
+ });
82
+ return;
83
+ }
84
+
85
+ if (!data.dosage || data.dosage === '') {
86
+ setError('dosage', {
87
+ type: 'manual',
88
+ message: t('dosageRequired', 'Dosage is required'),
89
+ });
90
+ return;
91
+ }
92
+
93
+ if (!data.route || data.route === '') {
94
+ setError('route', {
95
+ type: 'manual',
96
+ message: t('routeRequired', 'Route is required'),
97
+ });
98
+ return;
99
+ }
100
+
101
+ if (!data.frequency || data.frequency === '') {
102
+ setError('frequency', {
103
+ type: 'manual',
104
+ message: t('frequencyRequired', 'Frequency is required'),
105
+ });
106
+ return;
107
+ }
108
+
109
+ if (patient?.uuid) {
110
+ setIsSaving(true);
111
+ try {
112
+ const result = await saveDrugOrderData(
113
+ patient.uuid,
114
+ {
115
+ drugName: data.drugName,
116
+ dosage: data.dosage,
117
+ route: data.route,
118
+ frequency: data.frequency,
119
+ },
120
+ t,
121
+ );
122
+
123
+ if (result.success) {
124
+ setSaveSuccess(true);
125
+
126
+ if (onDataSaved) {
127
+ onDataSaved();
128
+ }
129
+
130
+ onSubmit({
131
+ drugName: data.drugName,
132
+ dosage: data.dosage,
133
+ route: data.route,
134
+ frequency: data.frequency,
135
+ });
136
+
137
+ reset();
138
+
139
+ setTimeout(() => {
140
+ setSaveSuccess(false);
141
+ onClose();
142
+ }, 1500);
143
+ } else {
144
+ setSaveError(result.message || t('saveError', 'Failed to save data'));
145
+ }
146
+ } catch (error) {
147
+ console.error('Save error details:', error);
148
+ setSaveError(
149
+ error?.message ||
150
+ error?.responseBody?.error?.message ||
151
+ error?.response?.data?.error?.message ||
152
+ t('saveError', 'Failed to save data'),
153
+ );
154
+ } finally {
155
+ setIsSaving(false);
156
+ }
157
+ } else {
158
+ onSubmit({
159
+ drugName: data.drugName,
160
+ dosage: data.dosage,
161
+ route: data.route,
162
+ frequency: data.frequency,
163
+ });
164
+ reset();
165
+ onClose();
166
+ }
167
+ };
168
+
169
+ const handleClose = () => {
170
+ reset();
171
+ clearErrors();
172
+ setSaveError(null);
173
+ setSaveSuccess(false);
174
+ onClose();
175
+ };
176
+
177
+ const patientLabel = patient ? `${patient.name}, ${patient.gender}, ${patient.age}` : 'Patient Information';
178
+
179
+ return (
180
+ <Modal
181
+ open={isOpen}
182
+ onRequestClose={handleClose}
183
+ modalHeading={t('drugsIVFluids', 'Drugs and IV Fluids Given')}
184
+ modalLabel={patientLabel}
185
+ primaryButtonText={isSaving ? t('saving', 'Saving...') : t('save', 'Save')}
186
+ primaryButtonDisabled={isSaving}
187
+ secondaryButtonText={t('cancel', 'Cancel')}
188
+ onRequestSubmit={handleSubmit(onSubmitForm)}
189
+ onSecondarySubmit={handleClose}
190
+ size="md">
191
+ {saveSuccess && (
192
+ <InlineNotification
193
+ kind="success"
194
+ title={t('saveSuccess', 'Data saved successfully')}
195
+ subtitle={t('drugOrderDataSaved', 'Drug order data has been saved to OpenMRS')}
196
+ hideCloseButton
197
+ />
198
+ )}
199
+
200
+ {saveError && (
201
+ <InlineNotification
202
+ kind="error"
203
+ title={t('saveError', 'Error saving data')}
204
+ subtitle={saveError}
205
+ onCloseButtonClick={() => setSaveError(null)}
206
+ />
207
+ )}
208
+
209
+ <div className={styles.modalContent}>
210
+ <Grid>
211
+ <Column sm={4} md={8} lg={16}>
212
+ <div className={styles.workspaceLauncherSection}>
213
+ <h4>{t('selectFromDrugList', 'Select from Drug List')}</h4>
214
+ <p className={styles.helperText}>
215
+ {t(
216
+ 'drugOrderDescription',
217
+ 'Use the drug order workspace to select from the complete list of available drugs with proper dosing and administration details.',
218
+ )}
219
+ </p>
220
+ <Button
221
+ kind="tertiary"
222
+ renderIcon={Add}
223
+ onClick={handleLaunchDrugOrderWorkspace}
224
+ disabled={!patient?.uuid}
225
+ className={styles.workspaceLauncherButton}>
226
+ {t('addDrugOrder', 'Add drug order')}
227
+ </Button>
228
+ </div>
229
+ </Column>
230
+
231
+ <Column sm={4} md={8} lg={16}>
232
+ <div className={styles.manualEntrySection}>
233
+ <h4>{t('manualEntry', 'Manual Entry')}</h4>
234
+ <p className={styles.helperText}>
235
+ {t('manualEntryDescription', 'Or enter drug information manually for quick documentation.')}
236
+ </p>
237
+ </div>
238
+ </Column>
239
+
240
+ <Column sm={4} md={8} lg={16}>
241
+ <Controller
242
+ name="drugName"
243
+ control={control}
244
+ render={({ field, fieldState }) => (
245
+ <TextInput
246
+ id="drug-name-input"
247
+ labelText={t('drugName', 'Drug Name')}
248
+ placeholder={t('enterDrugName', 'Enter drug name...')}
249
+ value={field.value}
250
+ onChange={field.onChange}
251
+ invalid={!!fieldState.error}
252
+ invalidText={fieldState.error?.message}
253
+ />
254
+ )}
255
+ />
256
+ </Column>
257
+
258
+ <Column sm={4} md={4} lg={8}>
259
+ <Controller
260
+ name="dosage"
261
+ control={control}
262
+ render={({ field, fieldState }) => (
263
+ <TextInput
264
+ id="dosage-input"
265
+ labelText={t('dosage', 'Dosage')}
266
+ placeholder={t('enterDosage', 'e.g., 250mg, 500ml')}
267
+ value={field.value}
268
+ onChange={field.onChange}
269
+ invalid={!!fieldState.error}
270
+ invalidText={fieldState.error?.message}
271
+ />
272
+ )}
273
+ />
274
+ </Column>
275
+
276
+ <Column sm={4} md={4} lg={8}>
277
+ <Controller
278
+ name="route"
279
+ control={control}
280
+ render={({ field, fieldState }) => (
281
+ <Dropdown
282
+ id="route-dropdown"
283
+ titleText={t('route', 'Route')}
284
+ label={t('selectRoute', 'Select route')}
285
+ items={routeOptions}
286
+ itemToString={(item) => (item ? item.text : '')}
287
+ selectedItem={field.value ? routeOptions.find((opt) => opt.id === field.value) : null}
288
+ onChange={({ selectedItem }) => field.onChange(selectedItem?.id || '')}
289
+ invalid={!!fieldState.error}
290
+ invalidText={fieldState.error?.message}
291
+ />
292
+ )}
293
+ />
294
+ </Column>
295
+
296
+ <Column sm={4} md={8} lg={16}>
297
+ <Controller
298
+ name="frequency"
299
+ control={control}
300
+ render={({ field, fieldState }) => (
301
+ <Dropdown
302
+ id="frequency-dropdown"
303
+ titleText={t('frequency', 'Frequency')}
304
+ label={t('selectFrequency', 'Select frequency')}
305
+ items={frequencyOptions}
306
+ itemToString={(item) => (item ? item.text : '')}
307
+ selectedItem={field.value ? frequencyOptions.find((opt) => opt.id === field.value) : null}
308
+ onChange={({ selectedItem }) => field.onChange(selectedItem?.id || '')}
309
+ invalid={!!fieldState.error}
310
+ invalidText={fieldState.error?.message}
311
+ />
312
+ )}
313
+ />
314
+ </Column>
315
+ </Grid>
316
+ </div>
317
+ </Modal>
318
+ );
319
+ };
320
+
321
+ export default DrugsIVFluidsForm;