@kenyaemr/esm-patient-clinical-view-app 5.4.2-pre.2763 → 5.4.2-pre.2765
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/{805.js → 189.js} +1 -1
- package/dist/189.js.map +1 -0
- package/dist/40.js +1 -1
- 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 +3 -3
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/config-schema.ts +58 -36
- package/src/maternal-and-child-health/partography/components/temperature-graph.component.tsx +0 -4
- package/src/maternal-and-child-health/partography/components/uterine-contractions-graph.component.tsx +33 -11
- package/src/maternal-and-child-health/partography/forms/cervical-contractions-form.component.tsx +124 -136
- package/src/maternal-and-child-health/partography/forms/cervix-form.component.tsx +23 -14
- package/src/maternal-and-child-health/partography/forms/drugs-iv-fluids-form.component.tsx +6 -10
- package/src/maternal-and-child-health/partography/forms/fetal-heart-rate-form.component.tsx +36 -13
- package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.component.tsx +27 -46
- package/src/maternal-and-child-health/partography/forms/useCervixData.ts +2 -2
- package/src/maternal-and-child-health/partography/graphs/cervix-graph.component.tsx +56 -18
- package/src/maternal-and-child-health/partography/graphs/fetal-heart-rate-graph.component.tsx +36 -23
- package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph.component.tsx +10 -5
- package/src/maternal-and-child-health/partography/partograph.component.tsx +315 -371
- package/src/maternal-and-child-health/partography/partography.resource.ts +788 -230
- package/src/maternal-and-child-health/partography/partography.scss +68 -40
- package/src/maternal-and-child-health/partography/resources/cervix.resource.ts +79 -76
- package/src/maternal-and-child-health/partography/resources/uterine-contractions.resource.ts +33 -12
- package/src/maternal-and-child-health/partography/types/index.ts +94 -0
- package/translations/am.json +0 -8
- package/translations/en.json +0 -8
- package/translations/sw.json +0 -8
- package/dist/805.js.map +0 -1
|
@@ -45,7 +45,7 @@ const CervixForm: React.FC<CervixFormProps> = ({
|
|
|
45
45
|
hour: '',
|
|
46
46
|
time: '',
|
|
47
47
|
cervicalDilation: '',
|
|
48
|
-
descent: '
|
|
48
|
+
descent: '',
|
|
49
49
|
},
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -66,24 +66,25 @@ const CervixForm: React.FC<CervixFormProps> = ({
|
|
|
66
66
|
};
|
|
67
67
|
}, [existingCervixData]);
|
|
68
68
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
const usedHours = useMemo(() => {
|
|
70
|
+
if (!selectedHours || selectedHours.length === 0) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
return selectedHours;
|
|
74
|
+
}, [selectedHours]);
|
|
73
75
|
|
|
74
76
|
const hourOptions = useMemo(() => {
|
|
75
77
|
return Array.from({ length: 24 }, (_, i) => {
|
|
76
78
|
const hourValue = String(i).padStart(2, '0');
|
|
77
|
-
const isDisabled = i
|
|
79
|
+
const isDisabled = usedHours.includes(i);
|
|
78
80
|
const displayText = isDisabled ? `${hourValue} (used)` : hourValue;
|
|
79
|
-
|
|
80
81
|
return {
|
|
81
82
|
value: hourValue,
|
|
82
83
|
text: displayText,
|
|
83
84
|
disabled: isDisabled,
|
|
84
85
|
};
|
|
85
86
|
});
|
|
86
|
-
}, [
|
|
87
|
+
}, [usedHours]);
|
|
87
88
|
|
|
88
89
|
const onSubmitForm = async (data: CervixFormData) => {
|
|
89
90
|
if (!data.hour || data.hour === '') {
|
|
@@ -99,6 +100,7 @@ const CervixForm: React.FC<CervixFormProps> = ({
|
|
|
99
100
|
type: 'manual',
|
|
100
101
|
message: 'Time selection is required',
|
|
101
102
|
});
|
|
103
|
+
console.warn('[CervixForm] Attempted to submit with empty time value:', data);
|
|
102
104
|
return;
|
|
103
105
|
}
|
|
104
106
|
|
|
@@ -178,7 +180,10 @@ const CervixForm: React.FC<CervixFormProps> = ({
|
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
clearErrors();
|
|
181
|
-
|
|
183
|
+
if (!data.time || data.time.trim() === '') {
|
|
184
|
+
alert('Time cannot be empty. Please select a valid time.');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
182
187
|
onSubmit({
|
|
183
188
|
hour: hourValue,
|
|
184
189
|
time: data.time,
|
|
@@ -286,12 +291,16 @@ const CervixForm: React.FC<CervixFormProps> = ({
|
|
|
286
291
|
<NumberInput
|
|
287
292
|
id="cervical-dilation-input"
|
|
288
293
|
label="Cervical Dilation (cm) *"
|
|
289
|
-
placeholder=
|
|
294
|
+
placeholder="Enter dilation (min: 4cm, max: 10cm)"
|
|
290
295
|
value={field.value || ''}
|
|
291
|
-
onChange={(e, { value }) =>
|
|
292
|
-
|
|
296
|
+
onChange={(e, { value }) => {
|
|
297
|
+
// Only allow whole numbers
|
|
298
|
+
const intValue = String(value).replace(/[^\d]/g, '');
|
|
299
|
+
field.onChange(intValue);
|
|
300
|
+
}}
|
|
301
|
+
min={4}
|
|
293
302
|
max={10}
|
|
294
|
-
step={
|
|
303
|
+
step={1}
|
|
295
304
|
invalid={!!fieldState.error}
|
|
296
305
|
invalidText={fieldState.error?.message}
|
|
297
306
|
/>
|
|
@@ -333,7 +342,7 @@ const CervixForm: React.FC<CervixFormProps> = ({
|
|
|
333
342
|
? `Default: 5 (high position), can decrement to lower values`
|
|
334
343
|
: `Enter descent (1=most descended, 5=high position)`
|
|
335
344
|
}
|
|
336
|
-
value={field.value || '
|
|
345
|
+
value={field.value || ''}
|
|
337
346
|
onChange={(e, { value }) => field.onChange(String(value))}
|
|
338
347
|
min={1}
|
|
339
348
|
max={5}
|
|
@@ -109,16 +109,12 @@ const DrugsIVFluidsForm: React.FC<DrugsIVFluidsFormProps> = ({ isOpen, onClose,
|
|
|
109
109
|
if (patient?.uuid) {
|
|
110
110
|
setIsSaving(true);
|
|
111
111
|
try {
|
|
112
|
-
const result = await saveDrugOrderData(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
frequency: data.frequency,
|
|
119
|
-
},
|
|
120
|
-
t,
|
|
121
|
-
);
|
|
112
|
+
const result = await saveDrugOrderData(patient.uuid, {
|
|
113
|
+
drugName: data.drugName,
|
|
114
|
+
dosage: data.dosage,
|
|
115
|
+
route: data.route,
|
|
116
|
+
frequency: data.frequency,
|
|
117
|
+
});
|
|
122
118
|
|
|
123
119
|
if (result.success) {
|
|
124
120
|
setSaveSuccess(true);
|
|
@@ -57,22 +57,35 @@ const FetalHeartRateForm: React.FC<FetalHeartRateFormProps> = ({
|
|
|
57
57
|
});
|
|
58
58
|
const generateHourOptions = () => {
|
|
59
59
|
const options = [];
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
options.push({ value: i.toString(), label: `${i}hr` });
|
|
65
|
-
}
|
|
60
|
+
options.push({ value: '0', label: '00' });
|
|
61
|
+
options.push({ value: '0.5', label: '30min' });
|
|
62
|
+
for (let i = 1; i <= 24; i++) {
|
|
63
|
+
options.push({ value: i.toString(), label: `${i}hr` });
|
|
66
64
|
if (i < 24) {
|
|
67
|
-
|
|
68
|
-
const halfHourLabel = i === 0 ? '1hr' : `${i}hr 30min`;
|
|
69
|
-
options.push({ value: halfHourValue, label: halfHourLabel });
|
|
65
|
+
options.push({ value: (i + 0.5).toString(), label: `${i}hr 30min` });
|
|
70
66
|
}
|
|
71
67
|
}
|
|
72
68
|
return options;
|
|
73
69
|
};
|
|
74
70
|
|
|
75
|
-
const
|
|
71
|
+
const latestHour = React.useMemo(() => {
|
|
72
|
+
if (!existingTimeEntries || existingTimeEntries.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
// Find the max hour value from existingTimeEntries (hour is already a number)
|
|
76
|
+
return Math.max(...existingTimeEntries.map((e) => e.hour));
|
|
77
|
+
}, [existingTimeEntries]);
|
|
78
|
+
|
|
79
|
+
// Generate hour options, disabling those before the latest entered hour (float comparison)
|
|
80
|
+
const hourOptionsWithDisabled = React.useMemo(() => {
|
|
81
|
+
return generateHourOptions().map((option) => {
|
|
82
|
+
const hourValue = parseFloat(option.value);
|
|
83
|
+
return {
|
|
84
|
+
...option,
|
|
85
|
+
disabled: latestHour !== null && hourValue <= latestHour,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}, [latestHour]);
|
|
76
89
|
|
|
77
90
|
const handleFormSubmit = async (data: FetalHeartRateFormData) => {
|
|
78
91
|
const hourValue = parseFloat(data.hour);
|
|
@@ -213,8 +226,13 @@ const FetalHeartRateForm: React.FC<FetalHeartRateFormProps> = ({
|
|
|
213
226
|
value={field.value}
|
|
214
227
|
onChange={(e) => field.onChange(e.target.value)}>
|
|
215
228
|
<SelectItem value="" text={t('admissionTime', 'Admission')} />
|
|
216
|
-
{
|
|
217
|
-
<SelectItem
|
|
229
|
+
{hourOptionsWithDisabled.map((option) => (
|
|
230
|
+
<SelectItem
|
|
231
|
+
key={option.value}
|
|
232
|
+
value={option.value}
|
|
233
|
+
text={option.label}
|
|
234
|
+
disabled={option.disabled}
|
|
235
|
+
/>
|
|
218
236
|
))}
|
|
219
237
|
</Select>
|
|
220
238
|
)}
|
|
@@ -258,10 +276,15 @@ const FetalHeartRateForm: React.FC<FetalHeartRateFormProps> = ({
|
|
|
258
276
|
max={200}
|
|
259
277
|
step={1}
|
|
260
278
|
value={field.value}
|
|
261
|
-
onChange={(
|
|
279
|
+
onChange={(e) => {
|
|
280
|
+
let val = (e.target as HTMLInputElement).value;
|
|
281
|
+
val = val.replace(/[^\d]/g, '');
|
|
282
|
+
field.onChange(val);
|
|
283
|
+
}}
|
|
262
284
|
invalid={!!fieldState.error}
|
|
263
285
|
invalidText={fieldState.error?.message}
|
|
264
286
|
size="lg"
|
|
287
|
+
allowEmpty
|
|
265
288
|
/>
|
|
266
289
|
)}
|
|
267
290
|
/>
|
|
@@ -22,8 +22,6 @@ const TimePickerDropdown: React.FC<TimePickerDropdownProps> = ({
|
|
|
22
22
|
existingTimeEntries = [],
|
|
23
23
|
}) => {
|
|
24
24
|
const [hours, minutes] = value && value.includes(':') ? value.split(':') : ['', ''];
|
|
25
|
-
|
|
26
|
-
// Memoize sorted entries and latest entry to avoid re-sorting on every render
|
|
27
25
|
const latestEntry = useMemo(() => {
|
|
28
26
|
if (!existingTimeEntries || existingTimeEntries.length === 0) {
|
|
29
27
|
return null;
|
|
@@ -50,18 +48,24 @@ const TimePickerDropdown: React.FC<TimePickerDropdownProps> = ({
|
|
|
50
48
|
return currentTimeInMinutes <= latestTimeInMinutes;
|
|
51
49
|
};
|
|
52
50
|
|
|
53
|
-
// Memoize options generation so we don't recreate arrays on every render
|
|
54
51
|
const hourOptions = useMemo(() => {
|
|
52
|
+
if (!latestEntry) {
|
|
53
|
+
return Array.from({ length: 24 }, (_, i) => {
|
|
54
|
+
const hour = i.toString().padStart(2, '0');
|
|
55
|
+
return { value: hour, text: hour, disabled: false, reason: '' };
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const [latestHour, latestMinute] = latestEntry.time.split(':').map(Number);
|
|
55
59
|
return Array.from({ length: 24 }, (_, i) => {
|
|
56
60
|
const hour = i.toString().padStart(2, '0');
|
|
57
61
|
let isDisabled = false;
|
|
58
62
|
let disableReason = '';
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
if (i < latestHour) {
|
|
64
|
+
isDisabled = true;
|
|
65
|
+
disableReason = 'before latest entry';
|
|
66
|
+
} else if (i === latestHour && latestMinute >= 55) {
|
|
67
|
+
isDisabled = true;
|
|
68
|
+
disableReason = 'latest entry full hour';
|
|
65
69
|
}
|
|
66
70
|
const displayText = isDisabled ? `${hour} ${disableReason}` : hour;
|
|
67
71
|
return {
|
|
@@ -74,18 +78,24 @@ const TimePickerDropdown: React.FC<TimePickerDropdownProps> = ({
|
|
|
74
78
|
}, [existingTimeEntries, latestEntry]);
|
|
75
79
|
|
|
76
80
|
const minuteOptions = useMemo(() => {
|
|
81
|
+
if (!latestEntry || !hours) {
|
|
82
|
+
return Array.from({ length: 12 }, (_, i) => {
|
|
83
|
+
const minute = (i * 5).toString().padStart(2, '0');
|
|
84
|
+
return { value: minute, text: minute, disabled: false };
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const [latestHour, latestMinute] = latestEntry.time.split(':').map(Number);
|
|
88
|
+
const currentHour = parseInt(hours);
|
|
77
89
|
return Array.from({ length: 12 }, (_, i) => {
|
|
78
90
|
const minute = (i * 5).toString().padStart(2, '0');
|
|
79
91
|
let isDisabled = false;
|
|
80
92
|
let disableReason = '';
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
disableReason = `≤ ${latestMinute} min`;
|
|
88
|
-
}
|
|
93
|
+
if (currentHour < latestHour) {
|
|
94
|
+
isDisabled = true;
|
|
95
|
+
disableReason = 'before latest entry';
|
|
96
|
+
} else if (currentHour === latestHour && i * 5 <= latestMinute) {
|
|
97
|
+
isDisabled = true;
|
|
98
|
+
disableReason = `≤ ${latestMinute} min`;
|
|
89
99
|
}
|
|
90
100
|
const displayText = isDisabled ? `${minute} ${disableReason}` : minute;
|
|
91
101
|
return { value: minute, text: displayText, disabled: isDisabled };
|
|
@@ -181,35 +191,6 @@ const TimePickerDropdown: React.FC<TimePickerDropdownProps> = ({
|
|
|
181
191
|
</div>
|
|
182
192
|
|
|
183
193
|
{invalid && invalidText && <div className={styles.errorText}>{invalidText}</div>}
|
|
184
|
-
|
|
185
|
-
{latestEntry && (
|
|
186
|
-
<div className={styles.timeConstraint}>
|
|
187
|
-
Latest entry: <strong>{latestEntry.time}</strong> - Next measurement must be after this time
|
|
188
|
-
</div>
|
|
189
|
-
)}
|
|
190
|
-
|
|
191
|
-
{existingTimeEntries.length > 0 && (
|
|
192
|
-
<div className={styles.usedTimesIndicator}>
|
|
193
|
-
<strong>Time Restrictions:</strong>
|
|
194
|
-
<br />
|
|
195
|
-
<strong>Used Times:</strong> {existingTimeEntries.map((entry) => entry.time).join(', ')}
|
|
196
|
-
<br />
|
|
197
|
-
<strong>Blocked Hours:</strong>{' '}
|
|
198
|
-
{hourOptions
|
|
199
|
-
.filter((opt) => opt.disabled)
|
|
200
|
-
.map((opt) => opt.value)
|
|
201
|
-
.join(', ')}{' '}
|
|
202
|
-
(grayed out)
|
|
203
|
-
<br />
|
|
204
|
-
<small>You can only select times after the latest entry</small>
|
|
205
|
-
</div>
|
|
206
|
-
)}
|
|
207
|
-
|
|
208
|
-
{value && (
|
|
209
|
-
<div className={styles.timePreview}>
|
|
210
|
-
Selected Time: <strong>{value}</strong>
|
|
211
|
-
</div>
|
|
212
|
-
)}
|
|
213
194
|
</FormGroup>
|
|
214
195
|
</div>
|
|
215
196
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useCervixData } from '../resources/cervix.resource';
|
|
2
|
-
export function useCervixFormData(patientUuid: string) {
|
|
1
|
+
import { useCervixData, UseCervixDataResult } from '../resources/cervix.resource';
|
|
2
|
+
export function useCervixFormData(patientUuid: string): UseCervixDataResult {
|
|
3
3
|
return useCervixData(patientUuid);
|
|
4
4
|
}
|
|
5
5
|
export type {
|
|
@@ -194,6 +194,7 @@ const CervixGraph: React.FC<CervixGraphProps> = ({
|
|
|
194
194
|
});
|
|
195
195
|
}
|
|
196
196
|
useEffect(() => {
|
|
197
|
+
let observer: MutationObserver | null = null;
|
|
197
198
|
const applyChartStyling = () => {
|
|
198
199
|
const chartContainer = document.querySelector(`[data-chart-id="cervix"]`);
|
|
199
200
|
if (chartContainer) {
|
|
@@ -217,36 +218,60 @@ const CervixGraph: React.FC<CervixGraphProps> = ({
|
|
|
217
218
|
svgCircles.forEach((circle) => {
|
|
218
219
|
const circleElement = circle as SVGCircleElement;
|
|
219
220
|
const parentGroup = circleElement.closest('g');
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
221
|
+
let isCervicalDilation = false;
|
|
222
|
+
if (circleElement.hasAttribute('data-group')) {
|
|
223
|
+
isCervicalDilation = circleElement.getAttribute('data-group')?.toLowerCase().includes('cervical dilation');
|
|
224
|
+
}
|
|
225
|
+
if (!isCervicalDilation && parentGroup) {
|
|
226
|
+
const groupLabel = parentGroup.getAttribute('aria-label') || parentGroup.getAttribute('data-name') || '';
|
|
227
|
+
if (groupLabel.toLowerCase().includes('cervical dilation')) {
|
|
228
|
+
isCervicalDilation = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!isCervicalDilation) {
|
|
232
|
+
const stroke = (circleElement.getAttribute('stroke') || circleElement.style.stroke || '').toLowerCase();
|
|
233
|
+
const fill = (circleElement.getAttribute('fill') || circleElement.style.fill || '').toLowerCase();
|
|
234
|
+
isCervicalDilation =
|
|
235
|
+
stroke.includes('green') ||
|
|
236
|
+
fill.includes('green') ||
|
|
237
|
+
stroke.includes('#42be65') ||
|
|
238
|
+
fill.includes('#42be65') ||
|
|
239
|
+
stroke.includes('#24a148') ||
|
|
240
|
+
fill.includes('#24a148') ||
|
|
241
|
+
stroke.includes('rgb(36, 161, 72)') ||
|
|
242
|
+
fill.includes('rgb(36, 161, 72)') ||
|
|
243
|
+
stroke.includes('rgb(66, 190, 101)') ||
|
|
244
|
+
fill.includes('rgb(66, 190, 101)');
|
|
245
|
+
}
|
|
246
|
+
if (isCervicalDilation && parentGroup) {
|
|
247
|
+
circleElement.style.display = 'none';
|
|
248
|
+
const cx = parseFloat(circleElement.getAttribute('cx') || '0');
|
|
249
|
+
const cy = parseFloat(circleElement.getAttribute('cy') || '0');
|
|
250
|
+
const size = 12;
|
|
251
|
+
const xColor = '#8a3ffc';
|
|
252
|
+
const svg = circleElement.ownerSVGElement;
|
|
253
|
+
if (svg) {
|
|
254
|
+
// Prevent duplicate Xs
|
|
255
|
+
if (!parentGroup.querySelector('line[data-x-marker]')) {
|
|
233
256
|
const line1 = document.createElementNS(SVG_NAMESPACE, 'line');
|
|
234
257
|
line1.setAttribute('x1', (cx - size).toString());
|
|
235
258
|
line1.setAttribute('y1', (cy - size).toString());
|
|
236
259
|
line1.setAttribute('x2', (cx + size).toString());
|
|
237
260
|
line1.setAttribute('y2', (cy + size).toString());
|
|
238
|
-
line1.setAttribute('stroke',
|
|
239
|
-
line1.setAttribute('stroke-width', '
|
|
261
|
+
line1.setAttribute('stroke', xColor);
|
|
262
|
+
line1.setAttribute('stroke-width', '5');
|
|
240
263
|
line1.setAttribute('stroke-linecap', 'round');
|
|
264
|
+
line1.setAttribute('data-x-marker', 'true');
|
|
241
265
|
|
|
242
266
|
const line2 = document.createElementNS(SVG_NAMESPACE, 'line');
|
|
243
267
|
line2.setAttribute('x1', (cx + size).toString());
|
|
244
268
|
line2.setAttribute('y1', (cy - size).toString());
|
|
245
269
|
line2.setAttribute('x2', (cx - size).toString());
|
|
246
270
|
line2.setAttribute('y2', (cy + size).toString());
|
|
247
|
-
line2.setAttribute('stroke',
|
|
248
|
-
line2.setAttribute('stroke-width', '
|
|
271
|
+
line2.setAttribute('stroke', xColor);
|
|
272
|
+
line2.setAttribute('stroke-width', '5');
|
|
249
273
|
line2.setAttribute('stroke-linecap', 'round');
|
|
274
|
+
line2.setAttribute('data-x-marker', 'true');
|
|
250
275
|
|
|
251
276
|
parentGroup.appendChild(line1);
|
|
252
277
|
parentGroup.appendChild(line2);
|
|
@@ -258,7 +283,20 @@ const CervixGraph: React.FC<CervixGraphProps> = ({
|
|
|
258
283
|
};
|
|
259
284
|
|
|
260
285
|
const timer = setTimeout(applyChartStyling, 100);
|
|
261
|
-
|
|
286
|
+
const chartContainer = document.querySelector(`[data-chart-id="cervix"]`);
|
|
287
|
+
if (chartContainer && window.MutationObserver) {
|
|
288
|
+
observer = new MutationObserver(() => {
|
|
289
|
+
applyChartStyling();
|
|
290
|
+
});
|
|
291
|
+
observer.observe(chartContainer, { childList: true, subtree: true });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return () => {
|
|
295
|
+
clearTimeout(timer);
|
|
296
|
+
if (observer) {
|
|
297
|
+
observer.disconnect();
|
|
298
|
+
}
|
|
299
|
+
};
|
|
262
300
|
}, [cervixFormData, isLoading]);
|
|
263
301
|
|
|
264
302
|
const shouldRenderChart = finalChartData.length > 0;
|
package/src/maternal-and-child-health/partography/graphs/fetal-heart-rate-graph.component.tsx
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { Add, ChartColumn, Table as TableIcon } from '@carbon/react/icons';
|
|
17
17
|
import { LineChart } from '@carbon/charts-react';
|
|
18
18
|
import styles from '../partography.scss';
|
|
19
|
-
import { getColorForGraph, generateRange } from '../types';
|
|
19
|
+
import { getColorForGraph, generateRange, defaultFetalHeartRateChartData } from '../types';
|
|
20
20
|
import { usePaginationInfo } from '@openmrs/esm-patient-common-lib';
|
|
21
21
|
|
|
22
22
|
enum ScaleTypes {
|
|
@@ -78,9 +78,9 @@ const FetalHeartRateGraph: React.FC<FetalHeartRateGraphProps> = ({
|
|
|
78
78
|
);
|
|
79
79
|
const getFetalHeartRateStatus = (value: string): { type: string; text: string; color: string } => {
|
|
80
80
|
const numValue = parseInt(value.replace(' bpm', ''));
|
|
81
|
-
if (numValue <
|
|
82
|
-
return { type: '
|
|
83
|
-
} else if (numValue >=
|
|
81
|
+
if (numValue < 110) {
|
|
82
|
+
return { type: 'red', text: 'Low', color: getColorForGraph('red') };
|
|
83
|
+
} else if (numValue >= 110 && numValue <= 160) {
|
|
84
84
|
return { type: 'green', text: 'Normal', color: getColorForGraph('green') };
|
|
85
85
|
} else {
|
|
86
86
|
return { type: 'red', text: 'High', color: getColorForGraph('red') };
|
|
@@ -94,9 +94,9 @@ const FetalHeartRateGraph: React.FC<FetalHeartRateGraphProps> = ({
|
|
|
94
94
|
{ key: 'status', header: t('status', 'Status') },
|
|
95
95
|
];
|
|
96
96
|
const getFetalHeartRateColor = (value: number): string => {
|
|
97
|
-
if (value <
|
|
98
|
-
return getColorForGraph('
|
|
99
|
-
} else if (value >=
|
|
97
|
+
if (value < 110) {
|
|
98
|
+
return getColorForGraph('red');
|
|
99
|
+
} else if (value >= 110 && value <= 160) {
|
|
100
100
|
return getColorForGraph('green');
|
|
101
101
|
} else {
|
|
102
102
|
return getColorForGraph('red');
|
|
@@ -112,15 +112,7 @@ const FetalHeartRateGraph: React.FC<FetalHeartRateGraphProps> = ({
|
|
|
112
112
|
}));
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
return
|
|
116
|
-
{ hour: 0, value: 140, group: 'Fetal Heart Rate', time: '0', color: getColorForGraph('green') },
|
|
117
|
-
{ hour: 10, value: 140, group: 'Fetal Heart Rate', time: '10', color: getColorForGraph('green') },
|
|
118
|
-
{ hour: 20, value: 140, group: 'Fetal Heart Rate', time: '20', color: getColorForGraph('green') },
|
|
119
|
-
{ hour: 30, value: 140, group: 'Fetal Heart Rate', time: '30', color: getColorForGraph('green') },
|
|
120
|
-
{ hour: 40, value: 140, group: 'Fetal Heart Rate', time: '40', color: getColorForGraph('green') },
|
|
121
|
-
{ hour: 50, value: 140, group: 'Fetal Heart Rate', time: '50', color: getColorForGraph('green') },
|
|
122
|
-
{ hour: 60, value: 140, group: 'Fetal Heart Rate', time: '60', color: getColorForGraph('green') },
|
|
123
|
-
];
|
|
115
|
+
return defaultFetalHeartRateChartData;
|
|
124
116
|
}, [data]);
|
|
125
117
|
|
|
126
118
|
const chartData = enhancedChartData;
|
|
@@ -179,6 +171,28 @@ const FetalHeartRateGraph: React.FC<FetalHeartRateGraphProps> = ({
|
|
|
179
171
|
enabled: true,
|
|
180
172
|
},
|
|
181
173
|
},
|
|
174
|
+
referenceLines: [
|
|
175
|
+
{
|
|
176
|
+
axis: 'left',
|
|
177
|
+
value: 110,
|
|
178
|
+
style: {
|
|
179
|
+
stroke: '#111',
|
|
180
|
+
strokeWidth: 4,
|
|
181
|
+
strokeDasharray: '0',
|
|
182
|
+
},
|
|
183
|
+
label: '110',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
axis: 'left',
|
|
187
|
+
value: 160,
|
|
188
|
+
style: {
|
|
189
|
+
stroke: '#111',
|
|
190
|
+
strokeWidth: 4,
|
|
191
|
+
strokeDasharray: '0',
|
|
192
|
+
},
|
|
193
|
+
label: '160',
|
|
194
|
+
},
|
|
195
|
+
],
|
|
182
196
|
color: {
|
|
183
197
|
scale:
|
|
184
198
|
data && data.length > 0
|
|
@@ -204,13 +218,13 @@ const FetalHeartRateGraph: React.FC<FetalHeartRateGraphProps> = ({
|
|
|
204
218
|
<h3 className={styles.fetalHeartRateTitle}>Fetal Heart Rate</h3>
|
|
205
219
|
<div className={styles.fetalHeartRateControls}>
|
|
206
220
|
<Tag type="green" title="Normal Range">
|
|
207
|
-
Normal (
|
|
221
|
+
Normal (110-160)
|
|
208
222
|
</Tag>
|
|
209
223
|
<Tag type="red" title="Abnormal Range">
|
|
210
|
-
Abnormal (>
|
|
224
|
+
Abnormal (>160)
|
|
211
225
|
</Tag>
|
|
212
|
-
<Tag type="
|
|
213
|
-
Low (<
|
|
226
|
+
<Tag type="red" title="Low Range">
|
|
227
|
+
Low (<110)
|
|
214
228
|
</Tag>
|
|
215
229
|
</div>
|
|
216
230
|
</div>
|
|
@@ -290,15 +304,14 @@ const FetalHeartRateGraph: React.FC<FetalHeartRateGraphProps> = ({
|
|
|
290
304
|
: cell.info.header === 'value'
|
|
291
305
|
? (() => {
|
|
292
306
|
const numValue = parseInt(cell.value.replace(' bpm', ''));
|
|
293
|
-
|
|
294
|
-
if (numValue < 100) {
|
|
307
|
+
if (numValue < 110) {
|
|
295
308
|
return (
|
|
296
309
|
<span className={`${styles.fetalHeartRateValue} ${styles.low}`}>
|
|
297
310
|
<span className={styles.arrow}>↓</span>
|
|
298
311
|
{cell.value}
|
|
299
312
|
</span>
|
|
300
313
|
);
|
|
301
|
-
} else if (numValue >
|
|
314
|
+
} else if (numValue > 160) {
|
|
302
315
|
return (
|
|
303
316
|
<span className={`${styles.fetalHeartRateValue} ${styles.high}`}>
|
|
304
317
|
<span className={styles.arrow}>↑</span>
|
|
@@ -58,7 +58,7 @@ const PULSE_BP_CHART_OPTIONS = {
|
|
|
58
58
|
filled: true,
|
|
59
59
|
},
|
|
60
60
|
curve: 'curveLinear',
|
|
61
|
-
height: '
|
|
61
|
+
height: '600px',
|
|
62
62
|
theme: 'white',
|
|
63
63
|
toolbar: {
|
|
64
64
|
enabled: false,
|
|
@@ -74,7 +74,7 @@ const PULSE_BP_CHART_OPTIONS = {
|
|
|
74
74
|
},
|
|
75
75
|
y: {
|
|
76
76
|
enabled: true,
|
|
77
|
-
numberOfTicks:
|
|
77
|
+
numberOfTicks: 7, // Fewer ticks for larger row spacing
|
|
78
78
|
},
|
|
79
79
|
},
|
|
80
80
|
zoomBar: {
|
|
@@ -174,6 +174,9 @@ const PulseBPGraph: React.FC<PulseBPGraphProps> = ({ data }) => {
|
|
|
174
174
|
const systolicYPosition = chartMarginTop + ((180 - item.systolicBP) / (180 - 60)) * chartHeight;
|
|
175
175
|
const diastolicYPosition = chartMarginTop + ((180 - item.diastolicBP) / (180 - 60)) * chartHeight;
|
|
176
176
|
|
|
177
|
+
const greenArrowStartY = Math.min(pulseYPosition, diastolicYPosition);
|
|
178
|
+
const greenArrowEndY = Math.max(pulseYPosition, diastolicYPosition);
|
|
179
|
+
|
|
177
180
|
return (
|
|
178
181
|
<g key={index}>
|
|
179
182
|
<defs>
|
|
@@ -195,7 +198,8 @@ const PulseBPGraph: React.FC<PulseBPGraphProps> = ({ data }) => {
|
|
|
195
198
|
refY="5"
|
|
196
199
|
orient="auto"
|
|
197
200
|
markerUnits="strokeWidth">
|
|
198
|
-
|
|
201
|
+
{/* Downward arrow for green (diastolic) */}
|
|
202
|
+
<polygon points="0,10 10,5 0,0 3,5" fill={getColorForGraph('green')} />
|
|
199
203
|
</marker>
|
|
200
204
|
</defs>
|
|
201
205
|
|
|
@@ -209,11 +213,12 @@ const PulseBPGraph: React.FC<PulseBPGraphProps> = ({ data }) => {
|
|
|
209
213
|
markerEnd={`url(#systolic-arrow-${index})`}
|
|
210
214
|
/>
|
|
211
215
|
|
|
216
|
+
{/* Always draw green arrow down, regardless of value */}
|
|
212
217
|
<line
|
|
213
218
|
x1={`${bpXPosition}%`}
|
|
214
|
-
y1={`${
|
|
219
|
+
y1={`${greenArrowStartY}%`}
|
|
215
220
|
x2={`${bpXPosition}%`}
|
|
216
|
-
y2={`${
|
|
221
|
+
y2={`${greenArrowEndY}%`}
|
|
217
222
|
stroke={getColorForGraph('green')}
|
|
218
223
|
strokeWidth="2"
|
|
219
224
|
markerEnd={`url(#diastolic-arrow-${index})`}
|