@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.
- package/.turbo/turbo-build.log +4 -4
- package/dist/805.js +1 -0
- package/dist/805.js.map +1 -0
- package/dist/kenyaemr-esm-patient-clinical-view-app.js +2 -2
- package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +27 -27
- 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/dist/397.js +0 -1
- package/dist/397.js.map +0 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { useForm, Controller } from 'react-hook-form';
|
|
4
|
+
import { Modal, Grid, Column, NumberInput, Select, SelectItem, InlineNotification } from '@carbon/react';
|
|
5
|
+
import { saveFetalHeartRateData } from '../resources/fetal-heart-rate.resource';
|
|
6
|
+
import TimePickerDropdown from './time-picker-dropdown.component';
|
|
7
|
+
import styles from '../partography-data-form.scss';
|
|
8
|
+
|
|
9
|
+
type FetalHeartRateFormData = {
|
|
10
|
+
hour: string;
|
|
11
|
+
time: string;
|
|
12
|
+
fetalHeartRate: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type FetalHeartRateFormProps = {
|
|
16
|
+
isOpen: boolean;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
onSubmit: (data: { hour: number; time: string; fetalHeartRate: number }) => void;
|
|
19
|
+
onDataSaved?: () => void;
|
|
20
|
+
selectedHours?: number[];
|
|
21
|
+
existingTimeEntries?: Array<{ hour: number; time: string }>;
|
|
22
|
+
patient?: {
|
|
23
|
+
uuid: string;
|
|
24
|
+
name: string;
|
|
25
|
+
gender: string;
|
|
26
|
+
age: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const FetalHeartRateForm: React.FC<FetalHeartRateFormProps> = ({
|
|
31
|
+
isOpen,
|
|
32
|
+
onClose,
|
|
33
|
+
onSubmit,
|
|
34
|
+
onDataSaved,
|
|
35
|
+
selectedHours = [],
|
|
36
|
+
existingTimeEntries = [],
|
|
37
|
+
patient,
|
|
38
|
+
}) => {
|
|
39
|
+
const { t } = useTranslation();
|
|
40
|
+
const [isSaving, setIsSaving] = React.useState(false);
|
|
41
|
+
const [saveError, setSaveError] = React.useState<string | null>(null);
|
|
42
|
+
const [saveSuccess, setSaveSuccess] = React.useState(false);
|
|
43
|
+
|
|
44
|
+
const {
|
|
45
|
+
control,
|
|
46
|
+
handleSubmit,
|
|
47
|
+
reset,
|
|
48
|
+
setError,
|
|
49
|
+
clearErrors,
|
|
50
|
+
formState: { errors },
|
|
51
|
+
} = useForm<FetalHeartRateFormData>({
|
|
52
|
+
defaultValues: {
|
|
53
|
+
hour: '',
|
|
54
|
+
time: '',
|
|
55
|
+
fetalHeartRate: '',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const generateHourOptions = () => {
|
|
59
|
+
const options = [];
|
|
60
|
+
for (let i = 0; i <= 24; i++) {
|
|
61
|
+
if (i === 0) {
|
|
62
|
+
options.push({ value: '0.5', label: '30min' });
|
|
63
|
+
} else {
|
|
64
|
+
options.push({ value: i.toString(), label: `${i}hr` });
|
|
65
|
+
}
|
|
66
|
+
if (i < 24) {
|
|
67
|
+
const halfHourValue = (i + 0.5).toString();
|
|
68
|
+
const halfHourLabel = i === 0 ? '1hr' : `${i}hr 30min`;
|
|
69
|
+
options.push({ value: halfHourValue, label: halfHourLabel });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return options;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const hourOptions = generateHourOptions();
|
|
76
|
+
|
|
77
|
+
const handleFormSubmit = async (data: FetalHeartRateFormData) => {
|
|
78
|
+
const hourValue = parseFloat(data.hour);
|
|
79
|
+
const fetalHeartRateValue = parseInt(data.fetalHeartRate);
|
|
80
|
+
|
|
81
|
+
clearErrors();
|
|
82
|
+
setSaveError(null);
|
|
83
|
+
setSaveSuccess(false);
|
|
84
|
+
|
|
85
|
+
if (!data.hour || isNaN(hourValue) || hourValue < 0 || hourValue > 24) {
|
|
86
|
+
setError('hour', { type: 'manual', message: t('hourRequired', 'Please select a valid hour') });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!data.time || data.time.trim() === '') {
|
|
90
|
+
setError('time', { type: 'manual', message: t('timeRequired', 'Time is required') });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!data.fetalHeartRate || isNaN(fetalHeartRateValue)) {
|
|
94
|
+
setError('fetalHeartRate', {
|
|
95
|
+
type: 'manual',
|
|
96
|
+
message: t('fetalHeartRateRequired', 'Fetal heart rate is required'),
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (fetalHeartRateValue < 80) {
|
|
101
|
+
setError('fetalHeartRate', {
|
|
102
|
+
type: 'manual',
|
|
103
|
+
message: t('fetalHeartRateTooLow', 'Fetal heart rate is too low. Minimum is 80 bpm'),
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (fetalHeartRateValue > 200) {
|
|
109
|
+
setError('fetalHeartRate', {
|
|
110
|
+
type: 'manual',
|
|
111
|
+
message: t('fetalHeartRateTooHigh', 'Fetal heart rate is too high. Maximum is 200 bpm'),
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (patient?.uuid) {
|
|
117
|
+
setIsSaving(true);
|
|
118
|
+
try {
|
|
119
|
+
const result = await saveFetalHeartRateData(
|
|
120
|
+
patient.uuid,
|
|
121
|
+
{
|
|
122
|
+
hour: hourValue,
|
|
123
|
+
time: data.time,
|
|
124
|
+
fetalHeartRate: fetalHeartRateValue,
|
|
125
|
+
},
|
|
126
|
+
t,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (result.success) {
|
|
130
|
+
setSaveSuccess(true);
|
|
131
|
+
if (onDataSaved) {
|
|
132
|
+
onDataSaved();
|
|
133
|
+
}
|
|
134
|
+
onSubmit({
|
|
135
|
+
hour: hourValue,
|
|
136
|
+
time: data.time,
|
|
137
|
+
fetalHeartRate: fetalHeartRateValue,
|
|
138
|
+
});
|
|
139
|
+
reset();
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
setSaveSuccess(false);
|
|
142
|
+
onClose();
|
|
143
|
+
}, 1500);
|
|
144
|
+
} else {
|
|
145
|
+
setSaveError(result.message || t('saveError', 'Failed to save data'));
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
setSaveError(error?.message || t('saveError', 'Failed to save data'));
|
|
149
|
+
} finally {
|
|
150
|
+
setIsSaving(false);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
onSubmit({
|
|
154
|
+
hour: hourValue,
|
|
155
|
+
time: data.time,
|
|
156
|
+
fetalHeartRate: fetalHeartRateValue,
|
|
157
|
+
});
|
|
158
|
+
reset();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleClose = () => {
|
|
163
|
+
reset();
|
|
164
|
+
clearErrors();
|
|
165
|
+
setSaveError(null);
|
|
166
|
+
setSaveSuccess(false);
|
|
167
|
+
onClose();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<Modal
|
|
172
|
+
open={isOpen}
|
|
173
|
+
onRequestClose={handleClose}
|
|
174
|
+
modalHeading={t('fetalHeartRateData', 'Foetal Heart Rate Data')}
|
|
175
|
+
modalLabel=""
|
|
176
|
+
primaryButtonText={isSaving ? t('saving', 'Saving...') : t('save', 'Save')}
|
|
177
|
+
primaryButtonDisabled={isSaving}
|
|
178
|
+
secondaryButtonText={t('cancel', 'Cancel')}
|
|
179
|
+
onRequestSubmit={handleSubmit(handleFormSubmit)}
|
|
180
|
+
onSecondarySubmit={handleClose}
|
|
181
|
+
className={styles.cervixModal}
|
|
182
|
+
size="md">
|
|
183
|
+
{saveSuccess && (
|
|
184
|
+
<InlineNotification
|
|
185
|
+
kind="success"
|
|
186
|
+
title={t('saveSuccess', 'Data saved successfully')}
|
|
187
|
+
subtitle={t('fetalHeartRateDataSaved', 'Fetal heart rate data has been saved')}
|
|
188
|
+
hideCloseButton
|
|
189
|
+
/>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
{saveError && (
|
|
193
|
+
<InlineNotification
|
|
194
|
+
kind="error"
|
|
195
|
+
title={t('saveError', 'Error saving data')}
|
|
196
|
+
subtitle={saveError}
|
|
197
|
+
onCloseButtonClick={() => setSaveError(null)}
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
<Grid className={styles.formGrid}>
|
|
202
|
+
<Column lg={8} md={4} sm={2}>
|
|
203
|
+
<div className={styles.formField}>
|
|
204
|
+
<Controller
|
|
205
|
+
name="hour"
|
|
206
|
+
control={control}
|
|
207
|
+
render={({ field, fieldState }) => (
|
|
208
|
+
<Select
|
|
209
|
+
id="hour-select"
|
|
210
|
+
labelText={t('hour', 'Hour')}
|
|
211
|
+
invalid={!!fieldState.error}
|
|
212
|
+
invalidText={fieldState.error?.message}
|
|
213
|
+
value={field.value}
|
|
214
|
+
onChange={(e) => field.onChange(e.target.value)}>
|
|
215
|
+
<SelectItem value="" text={t('admissionTime', 'Admission')} />
|
|
216
|
+
{hourOptions.map((option) => (
|
|
217
|
+
<SelectItem key={option.value} value={option.value} text={option.label} />
|
|
218
|
+
))}
|
|
219
|
+
</Select>
|
|
220
|
+
)}
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
223
|
+
</Column>
|
|
224
|
+
|
|
225
|
+
<Column lg={8} md={4} sm={2}>
|
|
226
|
+
<div className={styles.formField}>
|
|
227
|
+
<Controller
|
|
228
|
+
name="time"
|
|
229
|
+
control={control}
|
|
230
|
+
render={({ field, fieldState }) => (
|
|
231
|
+
<div>
|
|
232
|
+
<label className={styles.fieldLabel}>{t('time', 'Time')}</label>
|
|
233
|
+
<TimePickerDropdown
|
|
234
|
+
id="time-picker"
|
|
235
|
+
labelText=""
|
|
236
|
+
value={field.value}
|
|
237
|
+
onChange={field.onChange}
|
|
238
|
+
existingTimeEntries={existingTimeEntries}
|
|
239
|
+
invalid={!!fieldState.error}
|
|
240
|
+
invalidText={fieldState.error?.message}
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
246
|
+
</Column>
|
|
247
|
+
|
|
248
|
+
<Column lg={16} md={8} sm={4}>
|
|
249
|
+
<div className={styles.formField}>
|
|
250
|
+
<Controller
|
|
251
|
+
name="fetalHeartRate"
|
|
252
|
+
control={control}
|
|
253
|
+
render={({ field, fieldState }) => (
|
|
254
|
+
<NumberInput
|
|
255
|
+
id="fetal-heart-rate-input"
|
|
256
|
+
label={t('fetalHeartRate', 'Foetal Heart Rate')}
|
|
257
|
+
min={80}
|
|
258
|
+
max={200}
|
|
259
|
+
step={1}
|
|
260
|
+
value={field.value}
|
|
261
|
+
onChange={(_, state) => field.onChange(state.value)}
|
|
262
|
+
invalid={!!fieldState.error}
|
|
263
|
+
invalidText={fieldState.error?.message}
|
|
264
|
+
size="lg"
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
/>
|
|
268
|
+
</div>
|
|
269
|
+
</Column>
|
|
270
|
+
</Grid>
|
|
271
|
+
</Modal>
|
|
272
|
+
);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export default FetalHeartRateForm;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { default as CervixForm } from './cervix-form.component';
|
|
2
|
+
export { default as FetalHeartRateForm } from './fetal-heart-rate-form.component';
|
|
3
|
+
export { default as MembraneAmnioticFluidForm } from './membrane-amniotic-fluid-form.component';
|
|
4
|
+
export { default as CervicalContractionsForm } from './cervical-contractions-form.component';
|
|
5
|
+
export { default as OxytocinForm } from './oxytocin-form.component';
|
|
6
|
+
export { default as DrugsIVFluidsForm } from './drugs-iv-fluids-form.component';
|
|
7
|
+
export { default as PulseBPForm } from './pulse-bp-form.component';
|
|
8
|
+
export { default as TemperatureForm } from './temperature-form.component';
|
|
9
|
+
export { default as UrineTestForm } from './urine-test-form.component';
|
package/src/maternal-and-child-health/partography/forms/membrane-amniotic-fluid-form.component.tsx
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
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, Select, SelectItem, TimePicker } from '@carbon/react';
|
|
5
|
+
import TimePickerDropdown from './time-picker-dropdown.component';
|
|
6
|
+
import styles from '../partography-data-form.scss';
|
|
7
|
+
import { MEMBRANE_TIME_SLOT_OPTIONS } from '../types';
|
|
8
|
+
import {
|
|
9
|
+
AMNIOTIC_MEMBRANE_INTACT_CONCEPT,
|
|
10
|
+
AMNIOTIC_CLEAR_LIQUOR_CONCEPT,
|
|
11
|
+
AMNIOTIC_MECONIUM_STAINED_CONCEPT,
|
|
12
|
+
AMNIOTIC_ABSENT_CONCEPT,
|
|
13
|
+
AMNIOTIC_BLOOD_STAINED_CONCEPT,
|
|
14
|
+
MOULDING_NONE_CONCEPT,
|
|
15
|
+
MOULDING_SLIGHT_CONCEPT,
|
|
16
|
+
MOULDING_MODERATE_CONCEPT,
|
|
17
|
+
MOULDING_SEVERE_CONCEPT,
|
|
18
|
+
} from '../../../config-schema';
|
|
19
|
+
|
|
20
|
+
type MembraneAmnioticFluidFormData = {
|
|
21
|
+
timeSlot: string;
|
|
22
|
+
exactTime: string;
|
|
23
|
+
amnioticFluid: string;
|
|
24
|
+
moulding: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type MembraneAmnioticFluidFormProps = {
|
|
28
|
+
isOpen: boolean;
|
|
29
|
+
onClose: () => void;
|
|
30
|
+
onSubmit: (data: { timeSlot: string; exactTime: string; amnioticFluid: string; moulding: string }) => void;
|
|
31
|
+
onDataSaved?: () => void;
|
|
32
|
+
existingTimeEntries?: Array<{ timeSlot: string; exactTime: string }>;
|
|
33
|
+
patient?: {
|
|
34
|
+
uuid: string;
|
|
35
|
+
name: string;
|
|
36
|
+
gender: string;
|
|
37
|
+
age: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const MembraneAmnioticFluidForm: React.FC<MembraneAmnioticFluidFormProps> = ({
|
|
42
|
+
isOpen,
|
|
43
|
+
onClose,
|
|
44
|
+
onSubmit,
|
|
45
|
+
onDataSaved,
|
|
46
|
+
existingTimeEntries = [],
|
|
47
|
+
patient,
|
|
48
|
+
}) => {
|
|
49
|
+
const { t } = useTranslation();
|
|
50
|
+
|
|
51
|
+
const {
|
|
52
|
+
control,
|
|
53
|
+
handleSubmit,
|
|
54
|
+
reset,
|
|
55
|
+
setError,
|
|
56
|
+
clearErrors,
|
|
57
|
+
formState: { errors },
|
|
58
|
+
} = useForm<MembraneAmnioticFluidFormData>({
|
|
59
|
+
defaultValues: {
|
|
60
|
+
timeSlot: '',
|
|
61
|
+
exactTime: '',
|
|
62
|
+
amnioticFluid: '',
|
|
63
|
+
moulding: '',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const timeSlotOptions = useMemo(
|
|
68
|
+
() => MEMBRANE_TIME_SLOT_OPTIONS as unknown as { value: string; label: string }[],
|
|
69
|
+
[],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const latestUsedTimeSlot = React.useMemo(() => {
|
|
73
|
+
if (!existingTimeEntries || existingTimeEntries.length === 0) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const sortedTimeSlots = existingTimeEntries
|
|
77
|
+
.map((entry) => entry.timeSlot)
|
|
78
|
+
.slice()
|
|
79
|
+
.sort((a, b) => {
|
|
80
|
+
const getMinutes = (time: string) => {
|
|
81
|
+
const [hours, minutes] = time.split(':').map(Number);
|
|
82
|
+
|
|
83
|
+
const adjustedHours = hours <= 5 ? hours + 24 : hours;
|
|
84
|
+
return adjustedHours * 60 + minutes;
|
|
85
|
+
};
|
|
86
|
+
return getMinutes(a) - getMinutes(b);
|
|
87
|
+
});
|
|
88
|
+
return sortedTimeSlots[sortedTimeSlots.length - 1];
|
|
89
|
+
}, [existingTimeEntries]);
|
|
90
|
+
|
|
91
|
+
const isTimeSlotDisabled = (timeSlot: string) => {
|
|
92
|
+
if (!latestUsedTimeSlot) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const getMinutes = (time: string) => {
|
|
97
|
+
const [hours, minutes] = time.split(':').map(Number);
|
|
98
|
+
|
|
99
|
+
const adjustedHours = hours <= 5 ? hours + 24 : hours;
|
|
100
|
+
return adjustedHours * 60 + minutes;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return getMinutes(timeSlot) <= getMinutes(latestUsedTimeSlot);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const amnioticFluidOptions = useMemo(
|
|
107
|
+
() => [
|
|
108
|
+
{
|
|
109
|
+
value: 'Membrane intact',
|
|
110
|
+
label: t('membraneIntact', 'Membrane intact'),
|
|
111
|
+
concept: AMNIOTIC_MEMBRANE_INTACT_CONCEPT,
|
|
112
|
+
},
|
|
113
|
+
{ value: 'Clear liquor', label: t('clearLiquor', 'Clear liquor'), concept: AMNIOTIC_CLEAR_LIQUOR_CONCEPT },
|
|
114
|
+
{
|
|
115
|
+
value: 'Meconium Stained',
|
|
116
|
+
label: t('meconiumStained', 'Meconium Stained'),
|
|
117
|
+
concept: AMNIOTIC_MECONIUM_STAINED_CONCEPT,
|
|
118
|
+
},
|
|
119
|
+
{ value: 'Absent', label: t('absent', 'Absent'), concept: AMNIOTIC_ABSENT_CONCEPT },
|
|
120
|
+
{
|
|
121
|
+
value: 'Blood Stained',
|
|
122
|
+
label: t('bloodStained', 'Blood Stained'),
|
|
123
|
+
concept: AMNIOTIC_BLOOD_STAINED_CONCEPT,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
[t],
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const mouldingOptions = useMemo(
|
|
130
|
+
() => [
|
|
131
|
+
{ value: '0', label: '0', concept: MOULDING_NONE_CONCEPT },
|
|
132
|
+
{ value: '+', label: '+', concept: MOULDING_SLIGHT_CONCEPT },
|
|
133
|
+
{ value: '++', label: '++', concept: MOULDING_MODERATE_CONCEPT },
|
|
134
|
+
{ value: '+++', label: '+++', concept: MOULDING_SEVERE_CONCEPT },
|
|
135
|
+
],
|
|
136
|
+
[],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const handleFormSubmit = (data: MembraneAmnioticFluidFormData) => {
|
|
140
|
+
clearErrors();
|
|
141
|
+
|
|
142
|
+
let hasErrors = false;
|
|
143
|
+
|
|
144
|
+
if (!data.timeSlot || data.timeSlot.trim() === '') {
|
|
145
|
+
setError('timeSlot', { type: 'manual', message: t('timeSlotRequired', 'Please select a time slot') });
|
|
146
|
+
hasErrors = true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!data.exactTime || data.exactTime.trim() === '') {
|
|
150
|
+
setError('exactTime', { type: 'manual', message: t('exactTimeRequired', 'Exact time is required') });
|
|
151
|
+
hasErrors = true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!data.amnioticFluid || data.amnioticFluid.trim() === '') {
|
|
155
|
+
setError('amnioticFluid', {
|
|
156
|
+
type: 'manual',
|
|
157
|
+
message: t('amnioticFluidRequired', 'Please select amniotic fluid status'),
|
|
158
|
+
});
|
|
159
|
+
hasErrors = true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!data.moulding || data.moulding.trim() === '') {
|
|
163
|
+
setError('moulding', {
|
|
164
|
+
type: 'manual',
|
|
165
|
+
message: t('mouldingRequired', 'Please select moulding status'),
|
|
166
|
+
});
|
|
167
|
+
hasErrors = true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (hasErrors) {
|
|
171
|
+
alert(t('formValidationError', 'Please fill in all required fields before submitting.'));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (isTimeSlotDisabled(data.timeSlot)) {
|
|
176
|
+
setError('timeSlot', {
|
|
177
|
+
type: 'manual',
|
|
178
|
+
message: t('timeSlotDisabled', 'Selected time slot is not available. Please select a later time.'),
|
|
179
|
+
});
|
|
180
|
+
alert(t('timeSlotValidationError', 'Please select a valid time slot that comes after the previous entry.'));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
onSubmit({
|
|
185
|
+
timeSlot: data.timeSlot,
|
|
186
|
+
exactTime: data.exactTime,
|
|
187
|
+
amnioticFluid: data.amnioticFluid,
|
|
188
|
+
moulding: data.moulding,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
reset();
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const handleClose = () => {
|
|
195
|
+
reset();
|
|
196
|
+
clearErrors();
|
|
197
|
+
onClose();
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<Modal
|
|
202
|
+
open={isOpen}
|
|
203
|
+
onRequestClose={handleClose}
|
|
204
|
+
modalHeading={t('membraneAmnioticFluidData', 'Membrane Amniotic Fluid & Moulding')}
|
|
205
|
+
modalLabel={patient ? `${patient.name}, ${patient.gender}, ${patient.age}` : ''}
|
|
206
|
+
primaryButtonText={t('save', 'Save')}
|
|
207
|
+
secondaryButtonText={t('cancel', 'Cancel')}
|
|
208
|
+
onRequestSubmit={handleSubmit(handleFormSubmit)}
|
|
209
|
+
onSecondarySubmit={handleClose}
|
|
210
|
+
className={styles.cervixModal}
|
|
211
|
+
size="md">
|
|
212
|
+
<Grid className={styles.formGrid}>
|
|
213
|
+
<Column lg={8} md={4} sm={2}>
|
|
214
|
+
<div className={styles.formField}>
|
|
215
|
+
<Controller
|
|
216
|
+
name="timeSlot"
|
|
217
|
+
control={control}
|
|
218
|
+
rules={{ required: t('timeSlotRequired', 'Please select a time slot') }}
|
|
219
|
+
render={({ field, fieldState }) => (
|
|
220
|
+
<Select
|
|
221
|
+
id="time-slot-select"
|
|
222
|
+
labelText={t('timeSlot', 'Time Slot')}
|
|
223
|
+
invalid={!!fieldState.error}
|
|
224
|
+
invalidText={fieldState.error?.message}
|
|
225
|
+
helperText={
|
|
226
|
+
latestUsedTimeSlot
|
|
227
|
+
? t('timeSlotInfo', `Select a time after ${latestUsedTimeSlot}`)
|
|
228
|
+
: t('timeSlotInfoInitial', 'Select a time slot')
|
|
229
|
+
}
|
|
230
|
+
value={field.value}
|
|
231
|
+
onChange={(e) => field.onChange(e.target.value)}>
|
|
232
|
+
<SelectItem value="" text={t('chooseAnOption', 'Choose an option')} />
|
|
233
|
+
{timeSlotOptions.map((option) => {
|
|
234
|
+
const isDisabled = isTimeSlotDisabled(option.value);
|
|
235
|
+
return (
|
|
236
|
+
<SelectItem
|
|
237
|
+
key={option.value}
|
|
238
|
+
value={option.value}
|
|
239
|
+
text={option.label}
|
|
240
|
+
disabled={isDisabled}
|
|
241
|
+
className={isDisabled ? styles.disabledOption : undefined}
|
|
242
|
+
/>
|
|
243
|
+
);
|
|
244
|
+
})}
|
|
245
|
+
</Select>
|
|
246
|
+
)}
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
</Column>
|
|
250
|
+
|
|
251
|
+
<Column lg={8} md={4} sm={2}>
|
|
252
|
+
<div className={styles.formField}>
|
|
253
|
+
<Controller
|
|
254
|
+
name="exactTime"
|
|
255
|
+
control={control}
|
|
256
|
+
rules={{ required: t('exactTimeRequired', 'Exact time is required') }}
|
|
257
|
+
render={({ field, fieldState }) => (
|
|
258
|
+
<div>
|
|
259
|
+
<label className={styles.fieldLabel}>{t('exactTime', 'Exact Time')}</label>
|
|
260
|
+
<TimePickerDropdown
|
|
261
|
+
id="exact-time-picker"
|
|
262
|
+
labelText=""
|
|
263
|
+
value={field.value}
|
|
264
|
+
onChange={field.onChange}
|
|
265
|
+
existingTimeEntries={existingTimeEntries.map((e) => ({
|
|
266
|
+
hour: parseInt((e.exactTime || '00:00').split(':')[0] || '0', 10) || 0,
|
|
267
|
+
time: e.exactTime || '',
|
|
268
|
+
}))}
|
|
269
|
+
invalid={!!fieldState.error}
|
|
270
|
+
invalidText={fieldState.error?.message}
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
</Column>
|
|
277
|
+
|
|
278
|
+
<Column lg={8} md={4} sm={2}>
|
|
279
|
+
<div className={styles.formField}>
|
|
280
|
+
<Controller
|
|
281
|
+
name="amnioticFluid"
|
|
282
|
+
control={control}
|
|
283
|
+
rules={{ required: t('amnioticFluidRequired', 'Please select amniotic fluid status') }}
|
|
284
|
+
render={({ field, fieldState }) => (
|
|
285
|
+
<Select
|
|
286
|
+
id="amniotic-fluid-select"
|
|
287
|
+
labelText={t('amnioticFluid', 'Amniotic Fluid')}
|
|
288
|
+
invalid={!!fieldState.error}
|
|
289
|
+
invalidText={fieldState.error?.message}
|
|
290
|
+
value={field.value}
|
|
291
|
+
onChange={(e) => field.onChange(e.target.value)}>
|
|
292
|
+
<SelectItem value="" text={t('chooseAnOption', 'Choose an option')} />
|
|
293
|
+
{amnioticFluidOptions.map((option) => (
|
|
294
|
+
<SelectItem key={option.value} value={option.value} text={option.label} />
|
|
295
|
+
))}
|
|
296
|
+
</Select>
|
|
297
|
+
)}
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
</Column>
|
|
301
|
+
|
|
302
|
+
<Column lg={8} md={4} sm={2}>
|
|
303
|
+
<div className={styles.formField}>
|
|
304
|
+
<Controller
|
|
305
|
+
name="moulding"
|
|
306
|
+
control={control}
|
|
307
|
+
rules={{ required: t('mouldingRequired', 'Please select moulding status') }}
|
|
308
|
+
render={({ field, fieldState }) => (
|
|
309
|
+
<Select
|
|
310
|
+
id="moulding-select"
|
|
311
|
+
labelText={t('moulding', 'Moulding')}
|
|
312
|
+
invalid={!!fieldState.error}
|
|
313
|
+
invalidText={fieldState.error?.message}
|
|
314
|
+
value={field.value}
|
|
315
|
+
onChange={(e) => field.onChange(e.target.value)}>
|
|
316
|
+
<SelectItem value="" text={t('chooseAnOption', 'Choose an option')} />
|
|
317
|
+
{mouldingOptions.map((option) => (
|
|
318
|
+
<SelectItem key={option.value} value={option.value} text={option.label} />
|
|
319
|
+
))}
|
|
320
|
+
</Select>
|
|
321
|
+
)}
|
|
322
|
+
/>
|
|
323
|
+
</div>
|
|
324
|
+
</Column>
|
|
325
|
+
</Grid>
|
|
326
|
+
</Modal>
|
|
327
|
+
);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
export default MembraneAmnioticFluidForm;
|