@kenyaemr/esm-patient-clinical-view-app 5.4.2-pre.2716 → 5.4.2-pre.2724
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 +4 -4
- package/dist/127.js +1 -1
- package/dist/40.js +1 -1
- package/dist/805.js +1 -0
- package/dist/805.js.map +1 -0
- package/dist/916.js +1 -1
- package/dist/kenyaemr-esm-patient-clinical-view-app.js +2 -2
- package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +36 -36
- package/dist/main.js +27 -27
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/config-schema.ts +97 -0
- package/src/contact-list/contact-tracing-history.component.tsx +18 -15
- package/src/maternal-and-child-health/partography/components/pulse-bp-graph.component.tsx +1 -0
- package/src/maternal-and-child-health/partography/components/temperature-graph.component.tsx +218 -0
- package/src/maternal-and-child-health/partography/components/uterine-contractions-graph.component.tsx +209 -0
- package/src/maternal-and-child-health/partography/forms/cervical-contractions-form.component.tsx +211 -0
- package/src/maternal-and-child-health/partography/forms/cervix-form.component.tsx +354 -0
- package/src/maternal-and-child-health/partography/forms/drugs-iv-fluids-form.component.tsx +321 -0
- package/src/maternal-and-child-health/partography/forms/fetal-heart-rate-form.component.tsx +275 -0
- package/src/maternal-and-child-health/partography/forms/index.ts +9 -0
- package/src/maternal-and-child-health/partography/forms/membrane-amniotic-fluid-form.component.tsx +330 -0
- package/src/maternal-and-child-health/partography/forms/oxytocin-form.component.tsx +207 -0
- package/src/maternal-and-child-health/partography/forms/pulse-bp-form.component.tsx +174 -0
- package/src/maternal-and-child-health/partography/forms/temperature-form.component.tsx +210 -0
- package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.component.tsx +218 -0
- package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.scss +107 -0
- package/src/maternal-and-child-health/partography/forms/time-picker-with-clock.component.tsx +174 -0
- package/src/maternal-and-child-health/partography/forms/time-picker-with-clock.scss +178 -0
- package/src/maternal-and-child-health/partography/forms/urine-test-form.component.tsx +255 -0
- package/src/maternal-and-child-health/partography/forms/useCervixData.ts +16 -0
- package/src/maternal-and-child-health/partography/graphs/cervical-contractions-graph.component.tsx +266 -0
- package/src/maternal-and-child-health/partography/graphs/cervix-graph.component.tsx +429 -0
- package/src/maternal-and-child-health/partography/graphs/drugs-iv-fluids-graph-wrapper.component.tsx +163 -0
- package/src/maternal-and-child-health/partography/graphs/drugs-iv-fluids-graph.component.tsx +82 -0
- package/src/maternal-and-child-health/partography/graphs/fetal-heart-rate-graph.component.tsx +359 -0
- package/src/maternal-and-child-health/partography/graphs/index.ts +10 -0
- package/src/maternal-and-child-health/partography/graphs/membrane-amniotic-fluid-graph.component.tsx +266 -0
- package/src/maternal-and-child-health/partography/graphs/oxytocin-graph-wrapper.component.tsx +190 -0
- package/src/maternal-and-child-health/partography/graphs/oxytocin-graph.component.tsx +126 -0
- package/src/maternal-and-child-health/partography/graphs/partograph-graph.component.tsx +266 -0
- package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph-wrapper.component.tsx +298 -0
- package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph.component.tsx +267 -0
- package/src/maternal-and-child-health/partography/graphs/temperature-graph.component.tsx +242 -0
- package/src/maternal-and-child-health/partography/graphs/urine-test-graph.component.tsx +246 -0
- package/src/maternal-and-child-health/partography/partograph.component.tsx +2141 -118
- package/src/maternal-and-child-health/partography/partography-dashboard.meta.ts +8 -0
- package/src/maternal-and-child-health/partography/partography-data-form.scss +163 -0
- package/src/maternal-and-child-health/partography/partography.resource.ts +233 -326
- package/src/maternal-and-child-health/partography/partography.scss +1341 -3
- package/src/maternal-and-child-health/partography/resources/blood-pressure.resource.ts +96 -0
- package/src/maternal-and-child-health/partography/resources/cervical-dilation.resource.ts +109 -0
- package/src/maternal-and-child-health/partography/resources/cervix.resource.ts +362 -0
- package/src/maternal-and-child-health/partography/resources/descent-of-head.resource.ts +101 -0
- package/src/maternal-and-child-health/partography/resources/drugs-fluids.resource.ts +88 -0
- package/src/maternal-and-child-health/partography/resources/fetal-heart-rate.resource.ts +122 -0
- package/src/maternal-and-child-health/partography/resources/maternal-pulse.resource.ts +77 -0
- package/src/maternal-and-child-health/partography/resources/membrane-amniotic-fluid.resource.ts +108 -0
- package/src/maternal-and-child-health/partography/resources/oxytocin.resource.ts +159 -0
- package/src/maternal-and-child-health/partography/resources/progress-events.resource.ts +6 -0
- package/src/maternal-and-child-health/partography/resources/pulse-bp-combined.resource.ts +53 -0
- package/src/maternal-and-child-health/partography/resources/temperature.resource.ts +84 -0
- package/src/maternal-and-child-health/partography/resources/uterine-contractions.resource.ts +173 -0
- package/src/maternal-and-child-health/partography/table/temperature-table.component.tsx +99 -0
- package/src/maternal-and-child-health/partography/table/uterine-contractions-table.component.tsx +86 -0
- package/src/maternal-and-child-health/partography/types/index.ts +319 -101
- package/translations/am.json +121 -1
- package/translations/en.json +121 -1
- package/translations/sw.json +121 -1
- package/dist/397.js +0 -1
- 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;
|