@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
|
@@ -1,66 +1,707 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { LineChart } from '@carbon/charts-react';
|
|
2
|
+
import '@carbon/charts/styles.css';
|
|
3
3
|
import {
|
|
4
|
-
Layer,
|
|
5
|
-
Grid,
|
|
6
|
-
Column,
|
|
7
|
-
Tag,
|
|
8
|
-
Tabs,
|
|
9
|
-
TabList,
|
|
10
|
-
Tab,
|
|
11
|
-
TabPanels,
|
|
12
|
-
TabPanel,
|
|
13
4
|
Button,
|
|
5
|
+
Column,
|
|
14
6
|
DataTable,
|
|
15
|
-
|
|
7
|
+
Grid,
|
|
8
|
+
Layer,
|
|
9
|
+
Pagination,
|
|
16
10
|
Table,
|
|
17
|
-
TableHead,
|
|
18
|
-
TableRow,
|
|
19
|
-
TableHeader,
|
|
20
11
|
TableBody,
|
|
21
12
|
TableCell,
|
|
22
|
-
|
|
13
|
+
TableContainer,
|
|
14
|
+
TableHead,
|
|
15
|
+
TableHeader,
|
|
16
|
+
TableRow,
|
|
17
|
+
Tag,
|
|
23
18
|
} from '@carbon/react';
|
|
24
19
|
import { Add, ChartColumn, Table as TableIcon } from '@carbon/react/icons';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import '
|
|
28
|
-
import
|
|
20
|
+
import { openmrsFetch, useLayoutType, useSession } from '@openmrs/esm-framework';
|
|
21
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
22
|
+
import { useTranslation } from 'react-i18next';
|
|
23
|
+
import {
|
|
24
|
+
CervicalContractionsForm,
|
|
25
|
+
CervixForm,
|
|
26
|
+
FetalHeartRateForm,
|
|
27
|
+
OxytocinForm,
|
|
28
|
+
TemperatureForm,
|
|
29
|
+
UrineTestForm,
|
|
30
|
+
} from './forms';
|
|
31
|
+
import MembraneAmnioticFluidForm from './forms/membrane-amniotic-fluid-form.component';
|
|
32
|
+
import { saveCervixFormData, useCervixFormData } from './forms/useCervixData';
|
|
33
|
+
import {
|
|
34
|
+
CervicalContractionsGraph,
|
|
35
|
+
DrugsIVFluidsGraph,
|
|
36
|
+
FetalHeartRateGraph,
|
|
37
|
+
MembraneAmnioticFluidGraph,
|
|
38
|
+
OxytocinGraph,
|
|
39
|
+
PulseBPGraph,
|
|
40
|
+
TemperatureGraph,
|
|
41
|
+
UrineTestGraph,
|
|
42
|
+
} from './graphs';
|
|
29
43
|
import PartographyDataForm from './partography-data-form.component';
|
|
30
44
|
import {
|
|
31
|
-
usePartographyData,
|
|
32
45
|
createPartographyEncounter,
|
|
46
|
+
saveMembraneAmnioticFluidData,
|
|
33
47
|
transformEncounterToChartData,
|
|
34
48
|
transformEncounterToTableData,
|
|
49
|
+
useDrugOrders,
|
|
50
|
+
useFetalHeartRateData,
|
|
51
|
+
usePartographyData,
|
|
35
52
|
} from './partography.resource';
|
|
36
|
-
import
|
|
53
|
+
import styles from './partography.scss';
|
|
54
|
+
import { useMembraneAmnioticFluidData } from './resources/membrane-amniotic-fluid.resource';
|
|
55
|
+
import { saveOxytocinFormData, useOxytocinData } from './resources/oxytocin.resource';
|
|
56
|
+
import {
|
|
57
|
+
getColorForGraph,
|
|
58
|
+
getPartographyTableHeaders,
|
|
59
|
+
getTranslatedPartographyGraphs,
|
|
60
|
+
PARTOGRAPHY_CONCEPTS,
|
|
61
|
+
} from './types/index';
|
|
37
62
|
|
|
38
63
|
enum ScaleTypes {
|
|
39
64
|
LABELS = 'labels',
|
|
40
65
|
LINEAR = 'linear',
|
|
41
66
|
}
|
|
42
67
|
|
|
68
|
+
// --- INLINE TYPE DEFINITIONS ADDED FOR CONTEXT ---
|
|
69
|
+
type GraphDefinition = {
|
|
70
|
+
id: string;
|
|
71
|
+
title: string;
|
|
72
|
+
color: string;
|
|
73
|
+
yAxisLabel: string;
|
|
74
|
+
yMin: number;
|
|
75
|
+
yMax: number;
|
|
76
|
+
normalRange: string;
|
|
77
|
+
description: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type ChartDataPoint = {
|
|
81
|
+
hour: number;
|
|
82
|
+
time?: string;
|
|
83
|
+
group: string;
|
|
84
|
+
value: number;
|
|
85
|
+
};
|
|
86
|
+
// --- END INLINE TYPE DEFINITIONS ---
|
|
87
|
+
|
|
88
|
+
// --- CERVIX CHART OPTIONS: MEDICAL PARTOGRAPH STYLING ---
|
|
89
|
+
const CERVIX_CHART_OPTIONS = {
|
|
90
|
+
axes: {
|
|
91
|
+
bottom: {
|
|
92
|
+
title: '', // Remove Hours label completely
|
|
93
|
+
mapsTo: 'hour',
|
|
94
|
+
scaleType: ScaleTypes.LINEAR,
|
|
95
|
+
domain: [0, 10],
|
|
96
|
+
tick: {
|
|
97
|
+
count: 21, // Force exactly 21 ticks for all 30-min intervals
|
|
98
|
+
rotation: 0,
|
|
99
|
+
values: [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 9.5, 10], // Explicit 30-min intervals
|
|
100
|
+
formatter: (hour) => {
|
|
101
|
+
// Format as hours with 30-minute intervals
|
|
102
|
+
if (hour === 0) {
|
|
103
|
+
return '0';
|
|
104
|
+
} else if (hour === 0.5) {
|
|
105
|
+
return '0.30';
|
|
106
|
+
} else if (hour % 1 === 0) {
|
|
107
|
+
return `${hour}`;
|
|
108
|
+
} else if (hour % 1 === 0.5) {
|
|
109
|
+
return `${Math.floor(hour)}.30`;
|
|
110
|
+
} else {
|
|
111
|
+
return `${hour}`;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
grid: {
|
|
116
|
+
enabled: true,
|
|
117
|
+
strokeWidth: 1,
|
|
118
|
+
strokeDasharray: '2,2',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
left: {
|
|
122
|
+
title: 'Cervical Dilation (cm) / Descent of Head (5=high → 1=descended)',
|
|
123
|
+
mapsTo: 'value',
|
|
124
|
+
domain: [0, 10],
|
|
125
|
+
ticks: {
|
|
126
|
+
values: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
127
|
+
formatter: (value) => {
|
|
128
|
+
// Show both cervical dilation and descent of head values
|
|
129
|
+
// Direct mapping: 5=high position, 1=most descended
|
|
130
|
+
if (value >= 1 && value <= 5) {
|
|
131
|
+
return `${value}cm / D${value}`;
|
|
132
|
+
} else if (value === 0) {
|
|
133
|
+
return '0cm';
|
|
134
|
+
} else {
|
|
135
|
+
return `${value}cm`;
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
scaleType: ScaleTypes.LINEAR,
|
|
140
|
+
grid: {
|
|
141
|
+
enabled: true,
|
|
142
|
+
strokeWidth: 1,
|
|
143
|
+
strokeDasharray: '2,2',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
points: {
|
|
148
|
+
enabled: true,
|
|
149
|
+
radius: 6,
|
|
150
|
+
strokeWidth: 2,
|
|
151
|
+
fill: true,
|
|
152
|
+
},
|
|
153
|
+
curve: 'curveLinear',
|
|
154
|
+
height: '500px',
|
|
155
|
+
grid: {
|
|
156
|
+
x: {
|
|
157
|
+
enabled: true,
|
|
158
|
+
strokeWidth: 1,
|
|
159
|
+
strokeDasharray: '2,2',
|
|
160
|
+
},
|
|
161
|
+
y: {
|
|
162
|
+
enabled: true,
|
|
163
|
+
strokeWidth: 1,
|
|
164
|
+
strokeDasharray: '2,2',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
theme: 'white',
|
|
168
|
+
toolbar: {
|
|
169
|
+
enabled: false,
|
|
170
|
+
},
|
|
171
|
+
legend: {
|
|
172
|
+
position: 'top',
|
|
173
|
+
clickable: false,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
// --- END CERVIX CHART OPTIONS ---
|
|
177
|
+
|
|
43
178
|
type PartographyProps = {
|
|
44
179
|
patientUuid: string;
|
|
45
180
|
};
|
|
46
181
|
|
|
182
|
+
// Skeleton Components for Loading States with inline animation
|
|
183
|
+
const GraphSkeleton: React.FC = () => {
|
|
184
|
+
const skeletonStyle = {
|
|
185
|
+
background: 'linear-gradient(90deg, #f4f4f4 25%, #e0e0e0 50%, #f4f4f4 75%)',
|
|
186
|
+
backgroundSize: '200% 100%',
|
|
187
|
+
animation: 'skeletonShimmer 1.5s infinite ease-in-out',
|
|
188
|
+
borderRadius: '4px',
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Add keyframes to head if not already added
|
|
192
|
+
React.useEffect(() => {
|
|
193
|
+
if (!document.querySelector('#skeleton-keyframes')) {
|
|
194
|
+
const style = document.createElement('style');
|
|
195
|
+
style.id = 'skeleton-keyframes';
|
|
196
|
+
style.textContent = `
|
|
197
|
+
@keyframes skeletonShimmer {
|
|
198
|
+
0% { background-position: -200% 0; }
|
|
199
|
+
100% { background-position: 200% 0; }
|
|
200
|
+
}
|
|
201
|
+
`;
|
|
202
|
+
document.head.appendChild(style);
|
|
203
|
+
}
|
|
204
|
+
}, []);
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div
|
|
208
|
+
style={{
|
|
209
|
+
padding: '1rem',
|
|
210
|
+
backgroundColor: '#ffffff',
|
|
211
|
+
borderRadius: '4px',
|
|
212
|
+
border: '1px solid #e0e0e0',
|
|
213
|
+
}}>
|
|
214
|
+
<div
|
|
215
|
+
style={{
|
|
216
|
+
height: '500px',
|
|
217
|
+
...skeletonStyle,
|
|
218
|
+
marginBottom: '1rem',
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
{/* Skeleton for custom time labels (cervix specific) */}
|
|
222
|
+
<div style={{ marginTop: '1rem', borderTop: '1px solid #e0e0e0', paddingTop: '0.5rem' }}>
|
|
223
|
+
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.25rem' }}>
|
|
224
|
+
{Array.from({ length: 11 }).map((_, index) => (
|
|
225
|
+
<div
|
|
226
|
+
key={index}
|
|
227
|
+
style={{
|
|
228
|
+
width: index === 0 ? '60px' : '60px',
|
|
229
|
+
height: '20px',
|
|
230
|
+
flex: index === 0 ? 'none' : '1',
|
|
231
|
+
...skeletonStyle,
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
234
|
+
))}
|
|
235
|
+
</div>
|
|
236
|
+
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
237
|
+
{Array.from({ length: 11 }).map((_, index) => (
|
|
238
|
+
<div
|
|
239
|
+
key={index}
|
|
240
|
+
style={{
|
|
241
|
+
width: index === 0 ? '60px' : '60px',
|
|
242
|
+
height: '20px',
|
|
243
|
+
flex: index === 0 ? 'none' : '1',
|
|
244
|
+
...skeletonStyle,
|
|
245
|
+
}}
|
|
246
|
+
/>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
<div style={{ display: 'flex', gap: '2rem', marginTop: '1rem' }}>
|
|
251
|
+
<div style={{ width: '100px', height: '24px', ...skeletonStyle }} />
|
|
252
|
+
<div style={{ width: '120px', height: '24px', ...skeletonStyle }} />
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const TableSkeleton: React.FC = () => {
|
|
259
|
+
const skeletonStyle = {
|
|
260
|
+
background: 'linear-gradient(90deg, #f4f4f4 25%, #e0e0e0 50%, #f4f4f4 75%)',
|
|
261
|
+
backgroundSize: '200% 100%',
|
|
262
|
+
animation: 'skeletonShimmer 1.5s infinite ease-in-out',
|
|
263
|
+
borderRadius: '4px',
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div
|
|
268
|
+
style={{
|
|
269
|
+
padding: '1rem',
|
|
270
|
+
backgroundColor: '#ffffff',
|
|
271
|
+
borderRadius: '4px',
|
|
272
|
+
border: '1px solid #e0e0e0',
|
|
273
|
+
}}>
|
|
274
|
+
<div style={{ width: '200px', height: '24px', marginBottom: '1rem', ...skeletonStyle }} />
|
|
275
|
+
{Array.from({ length: 5 }).map((_, index) => (
|
|
276
|
+
<div key={index} style={{ display: 'flex', gap: '2rem', marginBottom: '0.5rem' }}>
|
|
277
|
+
<div style={{ width: '150px', height: '20px', ...skeletonStyle }} />
|
|
278
|
+
<div style={{ width: '80px', height: '20px', ...skeletonStyle }} />
|
|
279
|
+
<div style={{ width: '60px', height: '20px', ...skeletonStyle }} />
|
|
280
|
+
</div>
|
|
281
|
+
))}
|
|
282
|
+
<div style={{ width: '180px', height: '20px', marginTop: '1rem', ...skeletonStyle }} />
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
};
|
|
286
|
+
|
|
47
287
|
const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
48
288
|
const { t } = useTranslation();
|
|
49
289
|
|
|
290
|
+
// Development flag to enable dummy data (set to false for production)
|
|
291
|
+
const ENABLE_DUMMY_DATA = false;
|
|
292
|
+
|
|
293
|
+
// Local state for regular partography data (not saved to OpenMRS yet)
|
|
294
|
+
const [localPartographyData, setLocalPartographyData] = useState<Record<string, any[]>>({});
|
|
295
|
+
|
|
296
|
+
// Local state for fetal heart rate data
|
|
297
|
+
const [localFetalHeartRateData, setLocalFetalHeartRateData] = useState<
|
|
298
|
+
Array<{
|
|
299
|
+
hour: number;
|
|
300
|
+
value: number;
|
|
301
|
+
group: string;
|
|
302
|
+
time?: string;
|
|
303
|
+
}>
|
|
304
|
+
>([]);
|
|
305
|
+
|
|
306
|
+
// Backend membrane amniotic fluid data
|
|
307
|
+
const {
|
|
308
|
+
membraneAmnioticFluidEntries,
|
|
309
|
+
isLoading: isMembraneAmnioticFluidLoading,
|
|
310
|
+
error: membraneAmnioticFluidError,
|
|
311
|
+
mutate: mutateMembraneAmnioticFluidData,
|
|
312
|
+
} = useMembraneAmnioticFluidData(patientUuid || '');
|
|
313
|
+
|
|
314
|
+
// Cervical contractions backend data
|
|
315
|
+
const {
|
|
316
|
+
data: cervicalContractionsData,
|
|
317
|
+
isLoading: isCervicalContractionsLoading,
|
|
318
|
+
mutate: mutateCervicalContractions,
|
|
319
|
+
} = usePartographyData(patientUuid || '', 'uterine-contractions');
|
|
320
|
+
|
|
321
|
+
// Oxytocin backend data
|
|
322
|
+
const {
|
|
323
|
+
oxytocinData: loadedOxytocinData,
|
|
324
|
+
existingOxytocinEntries,
|
|
325
|
+
isLoading: isOxytocinDataLoading,
|
|
326
|
+
error: oxytocinDataError,
|
|
327
|
+
mutate: mutateOxytocinData,
|
|
328
|
+
} = useOxytocinData(patientUuid || '');
|
|
329
|
+
|
|
330
|
+
// Drugs and IV Fluids state
|
|
331
|
+
const [localDrugsIVFluidsData, setLocalDrugsIVFluidsData] = useState<
|
|
332
|
+
Array<{
|
|
333
|
+
drugName: string;
|
|
334
|
+
dosage: string;
|
|
335
|
+
route?: string;
|
|
336
|
+
frequency?: string;
|
|
337
|
+
date?: string;
|
|
338
|
+
id?: string;
|
|
339
|
+
}>
|
|
340
|
+
>([]);
|
|
341
|
+
|
|
342
|
+
// Pulse and BP backend data
|
|
343
|
+
const {
|
|
344
|
+
data: loadedPulseData,
|
|
345
|
+
isLoading: isPulseDataLoading,
|
|
346
|
+
error: pulseDataError,
|
|
347
|
+
mutate: mutatePulseData,
|
|
348
|
+
} = usePartographyData(patientUuid || '', 'maternal-pulse');
|
|
349
|
+
const {
|
|
350
|
+
data: loadedBPData,
|
|
351
|
+
isLoading: isBPDataLoading,
|
|
352
|
+
error: bpDataError,
|
|
353
|
+
mutate: mutateBPData,
|
|
354
|
+
} = usePartographyData(patientUuid || '', 'blood-pressure');
|
|
355
|
+
|
|
356
|
+
// Temperature backend data
|
|
357
|
+
const {
|
|
358
|
+
data: loadedTemperatureData,
|
|
359
|
+
isLoading: isTemperatureDataLoading,
|
|
360
|
+
error: temperatureDataError,
|
|
361
|
+
mutate: mutateTemperatureData,
|
|
362
|
+
} = usePartographyData(patientUuid || '', 'temperature');
|
|
363
|
+
|
|
364
|
+
// Urine Test backend data
|
|
365
|
+
const {
|
|
366
|
+
data: urineTestEncounters = [],
|
|
367
|
+
isLoading: isUrineTestLoading,
|
|
368
|
+
error: urineTestError,
|
|
369
|
+
mutate: mutateUrineTestData,
|
|
370
|
+
} = usePartographyData(patientUuid || '', 'urine-analysis');
|
|
371
|
+
|
|
372
|
+
// Transform backend urine test data to UrineTestData[]
|
|
373
|
+
const urineTestData = useMemo(() => {
|
|
374
|
+
// Map coded values to +/++/+++ for protein/acetone
|
|
375
|
+
const codeToPlus = (code) => {
|
|
376
|
+
switch (code) {
|
|
377
|
+
case '1107AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA':
|
|
378
|
+
case 'ZERO':
|
|
379
|
+
case '0':
|
|
380
|
+
return '0';
|
|
381
|
+
case '1362AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA':
|
|
382
|
+
case 'ONE PLUS':
|
|
383
|
+
case '+':
|
|
384
|
+
return '+';
|
|
385
|
+
case '1363AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA':
|
|
386
|
+
case 'TWO PLUS':
|
|
387
|
+
case '++':
|
|
388
|
+
return '++';
|
|
389
|
+
case '1364AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA':
|
|
390
|
+
case 'THREE PLUS':
|
|
391
|
+
case '+++':
|
|
392
|
+
return '+++';
|
|
393
|
+
default:
|
|
394
|
+
return code;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
return (urineTestEncounters || []).map((encounter, index) => {
|
|
398
|
+
const obs = encounter.obs || [];
|
|
399
|
+
// Helper to get obs value by concept
|
|
400
|
+
const getObsValue = (conceptUuid) => {
|
|
401
|
+
const found = obs.find((o) => o.concept.uuid === conceptUuid);
|
|
402
|
+
if (!found) {
|
|
403
|
+
return '';
|
|
404
|
+
}
|
|
405
|
+
const v = found.value;
|
|
406
|
+
if (v && typeof v === 'object' && Object.prototype.hasOwnProperty.call(v, 'display')) {
|
|
407
|
+
// @ts-ignore
|
|
408
|
+
return v.display ?? '';
|
|
409
|
+
}
|
|
410
|
+
return v != null ? String(v) : '';
|
|
411
|
+
};
|
|
412
|
+
// Helper to get time from description obs
|
|
413
|
+
const getTime = () => {
|
|
414
|
+
const timeObs = obs.find(
|
|
415
|
+
(o) =>
|
|
416
|
+
o.concept.uuid === PARTOGRAPHY_CONCEPTS['fetal-heart-rate-time'] &&
|
|
417
|
+
typeof o.value === 'string' &&
|
|
418
|
+
o.value.startsWith('Time:'),
|
|
419
|
+
);
|
|
420
|
+
if (timeObs && typeof timeObs.value === 'string') {
|
|
421
|
+
const match = timeObs.value.match(/Time:\s*(.+)/);
|
|
422
|
+
if (match) {
|
|
423
|
+
return match[1].trim();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return '';
|
|
427
|
+
};
|
|
428
|
+
// Helper to get timeSlot from obs (look for concept with 'time-slot' or value with 'TimeSlot:')
|
|
429
|
+
const getTimeSlot = () => {
|
|
430
|
+
// Try concept for time-slot
|
|
431
|
+
const slotObs = obs.find(
|
|
432
|
+
(o) =>
|
|
433
|
+
o.concept.uuid === PARTOGRAPHY_CONCEPTS['time-slot'] ||
|
|
434
|
+
(typeof o.value === 'string' && o.value.startsWith('TimeSlot:')),
|
|
435
|
+
);
|
|
436
|
+
if (slotObs) {
|
|
437
|
+
if (typeof slotObs.value === 'string') {
|
|
438
|
+
const match = slotObs.value.match(/TimeSlot:\s*(.+)/);
|
|
439
|
+
if (match) {
|
|
440
|
+
return match[1].trim();
|
|
441
|
+
}
|
|
442
|
+
return slotObs.value;
|
|
443
|
+
}
|
|
444
|
+
return String(slotObs.value);
|
|
445
|
+
}
|
|
446
|
+
return '';
|
|
447
|
+
};
|
|
448
|
+
// Sample Collected and Result Returned: try to find obs with concept display or uuid containing 'collected' or 'returned'
|
|
449
|
+
const getSampleCollected = () => {
|
|
450
|
+
const found = obs.find(
|
|
451
|
+
(o) =>
|
|
452
|
+
(o.concept.display && o.concept.display.toLowerCase().includes('collected')) ||
|
|
453
|
+
(o.concept.uuid && o.concept.uuid.toLowerCase().includes('collected')),
|
|
454
|
+
);
|
|
455
|
+
if (found) {
|
|
456
|
+
return found.value != null ? String(found.value) : '';
|
|
457
|
+
}
|
|
458
|
+
return '';
|
|
459
|
+
};
|
|
460
|
+
const getResultReturned = () => {
|
|
461
|
+
const found = obs.find(
|
|
462
|
+
(o) =>
|
|
463
|
+
(o.concept.display && o.concept.display.toLowerCase().includes('returned')) ||
|
|
464
|
+
(o.concept.uuid && o.concept.uuid.toLowerCase().includes('returned')),
|
|
465
|
+
);
|
|
466
|
+
if (found) {
|
|
467
|
+
return found.value != null ? String(found.value) : '';
|
|
468
|
+
}
|
|
469
|
+
return '';
|
|
470
|
+
};
|
|
471
|
+
// Get urine volume as string (not code), handle case-insensitive concept id
|
|
472
|
+
const getVolume = () => {
|
|
473
|
+
const found = obs.find(
|
|
474
|
+
(o) =>
|
|
475
|
+
(o.concept.uuid && o.concept.uuid.toLowerCase() === '159660aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') ||
|
|
476
|
+
(o.concept.display && o.concept.display.toLowerCase().includes('volume')),
|
|
477
|
+
);
|
|
478
|
+
if (!found) {
|
|
479
|
+
return '';
|
|
480
|
+
}
|
|
481
|
+
if (typeof found.value === 'number') {
|
|
482
|
+
return String(found.value);
|
|
483
|
+
}
|
|
484
|
+
if (typeof found.value === 'string') {
|
|
485
|
+
return found.value;
|
|
486
|
+
}
|
|
487
|
+
if (
|
|
488
|
+
found.value &&
|
|
489
|
+
typeof found.value === 'object' &&
|
|
490
|
+
'display' in found.value &&
|
|
491
|
+
typeof (found.value as { display?: unknown }).display === 'string'
|
|
492
|
+
) {
|
|
493
|
+
return (found.value as { display: string }).display ?? '';
|
|
494
|
+
}
|
|
495
|
+
return '';
|
|
496
|
+
};
|
|
497
|
+
// Extract 'Time Slot: HH:mm' from event-description obs
|
|
498
|
+
let timeSlot = '';
|
|
499
|
+
const eventDescObs = obs.find(
|
|
500
|
+
(o) =>
|
|
501
|
+
o.concept.uuid === PARTOGRAPHY_CONCEPTS['event-description'] &&
|
|
502
|
+
typeof o.value === 'string' &&
|
|
503
|
+
o.value.startsWith('Time Slot: '),
|
|
504
|
+
);
|
|
505
|
+
if (eventDescObs && typeof eventDescObs.value === 'string') {
|
|
506
|
+
const match = eventDescObs.value.match(/Time Slot: (\d{1,2}:\d{2})/);
|
|
507
|
+
if (match) {
|
|
508
|
+
timeSlot = match[1];
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// Ensure volume is a number if present, otherwise undefined
|
|
512
|
+
let volumeRaw = getVolume();
|
|
513
|
+
let volume: number | undefined = undefined;
|
|
514
|
+
if (typeof volumeRaw === 'number') {
|
|
515
|
+
volume = volumeRaw;
|
|
516
|
+
} else if (typeof volumeRaw === 'string' && volumeRaw.trim() !== '' && !isNaN(Number(volumeRaw))) {
|
|
517
|
+
volume = Number(volumeRaw);
|
|
518
|
+
}
|
|
519
|
+
const sampleCollected = getSampleCollected();
|
|
520
|
+
const resultReturned = getResultReturned();
|
|
521
|
+
// final row prepared
|
|
522
|
+
return {
|
|
523
|
+
id: `urine-test-${index}`,
|
|
524
|
+
date: new Date(encounter.encounterDatetime).toLocaleDateString(),
|
|
525
|
+
timeSlot,
|
|
526
|
+
exactTime: getTime(),
|
|
527
|
+
protein: codeToPlus(getObsValue(PARTOGRAPHY_CONCEPTS['protein-level'])),
|
|
528
|
+
acetone: codeToPlus(getObsValue(PARTOGRAPHY_CONCEPTS['ketone-level'])),
|
|
529
|
+
volume,
|
|
530
|
+
timeSampleCollected: sampleCollected,
|
|
531
|
+
timeResultsReturned: resultReturned,
|
|
532
|
+
};
|
|
533
|
+
});
|
|
534
|
+
}, [urineTestEncounters]);
|
|
535
|
+
|
|
536
|
+
const generateExtendedDummyData = () => {
|
|
537
|
+
const baseTime = new Date();
|
|
538
|
+
baseTime.setHours(8, 0, 0, 0);
|
|
539
|
+
|
|
540
|
+
return Array.from({ length: 10 }, (_, i) => {
|
|
541
|
+
const currentTime = new Date(baseTime);
|
|
542
|
+
currentTime.setHours(currentTime.getHours() + i);
|
|
543
|
+
|
|
544
|
+
const cervicalDilation = Math.min(3 + i * 0.8, 10);
|
|
545
|
+
|
|
546
|
+
const descentOfHead = Math.max(5 - Math.floor(i * 0.5), 1);
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
hour: i,
|
|
550
|
+
time: currentTime.toTimeString().slice(0, 5),
|
|
551
|
+
cervicalDilation: Math.round(cervicalDilation * 10) / 10,
|
|
552
|
+
descentOfHead,
|
|
553
|
+
entryDate: currentTime.toLocaleDateString(),
|
|
554
|
+
entryTime: currentTime.toTimeString(),
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
// Load cervix data from OpenMRS
|
|
560
|
+
const {
|
|
561
|
+
cervixData: loadedCervixData,
|
|
562
|
+
existingTimeEntries,
|
|
563
|
+
existingCervixData,
|
|
564
|
+
selectedHours,
|
|
565
|
+
isLoading: isCervixDataLoading,
|
|
566
|
+
error: cervixDataError,
|
|
567
|
+
mutate: mutateCervixData,
|
|
568
|
+
} = useCervixFormData(patientUuid || '');
|
|
569
|
+
|
|
570
|
+
// Load fetal heart rate data from OpenMRS with null safety
|
|
571
|
+
const {
|
|
572
|
+
fetalHeartRateData: loadedFetalHeartRateData = [],
|
|
573
|
+
isLoading: isFetalHeartRateLoading = false,
|
|
574
|
+
error: fetalHeartRateError = null,
|
|
575
|
+
mutate: mutateFetalHeartRateData = () => {},
|
|
576
|
+
} = useFetalHeartRateData(patientUuid || '');
|
|
577
|
+
|
|
578
|
+
// Fetch actual drug orders from OpenMRS
|
|
579
|
+
const {
|
|
580
|
+
drugOrders: loadedDrugOrders = [],
|
|
581
|
+
isLoading: isDrugOrdersLoading = false,
|
|
582
|
+
error: drugOrdersError = null,
|
|
583
|
+
mutate: mutateDrugOrders = () => {},
|
|
584
|
+
} = useDrugOrders(patientUuid || '');
|
|
585
|
+
|
|
586
|
+
// Debug: Log drug orders data
|
|
587
|
+
useEffect(() => {
|
|
588
|
+
if (patientUuid) {
|
|
589
|
+
// Drug orders status logged in development only
|
|
590
|
+
|
|
591
|
+
if (drugOrdersError) {
|
|
592
|
+
console.error('Drug Orders Error:', drugOrdersError);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}, [patientUuid, loadedDrugOrders, isDrugOrdersLoading, drugOrdersError]);
|
|
596
|
+
|
|
50
597
|
const session = useSession();
|
|
51
598
|
const layout = useLayoutType();
|
|
52
599
|
const isTablet = layout === 'tablet';
|
|
53
600
|
const controlSize = isTablet ? 'md' : 'sm';
|
|
54
|
-
const [selectedTab, setSelectedTab] = useState(0);
|
|
55
601
|
const [isFormOpen, setIsFormOpen] = useState(false);
|
|
602
|
+
const [isCervixFormOpen, setIsCervixFormOpen] = useState(false);
|
|
603
|
+
const [isFetalHeartRateFormOpen, setIsFetalHeartRateFormOpen] = useState(false);
|
|
604
|
+
const [isMembraneAmnioticFluidFormOpen, setIsMembraneAmnioticFluidFormOpen] = useState(false);
|
|
605
|
+
const [isCervicalContractionsFormOpen, setIsCervicalContractionsFormOpen] = useState(false);
|
|
606
|
+
const [isOxytocinFormOpen, setIsOxytocinFormOpen] = useState(false);
|
|
607
|
+
const [isTemperatureFormOpen, setIsTemperatureFormOpen] = useState(false);
|
|
608
|
+
const [temperatureFormInitialTime, setTemperatureFormInitialTime] = useState<string>('');
|
|
609
|
+
const [isUrineTestFormOpen, setIsUrineTestFormOpen] = useState(false);
|
|
56
610
|
const [selectedGraphType, setSelectedGraphType] = useState<string>('');
|
|
57
|
-
const [graphData, setGraphData] = useState<Record<string,
|
|
611
|
+
const [graphData, setGraphData] = useState<Record<string, ChartDataPoint[]>>({});
|
|
58
612
|
const [viewMode, setViewMode] = useState<Record<string, 'graph' | 'table'>>({});
|
|
613
|
+
const [fetalHeartRateViewMode, setFetalHeartRateViewMode] = useState<'graph' | 'table'>('graph');
|
|
614
|
+
const [membraneAmnioticFluidViewMode, setMembraneAmnioticFluidViewMode] = useState<'graph' | 'table'>('graph');
|
|
615
|
+
const [cervicalContractionsViewMode, setCervicalContractionsViewMode] = useState<'graph' | 'table'>('graph');
|
|
616
|
+
const [oxytocinViewMode, setOxytocinViewMode] = useState<'graph' | 'table'>('graph');
|
|
617
|
+
const [drugsIVFluidsViewMode, setDrugsIVFluidsViewMode] = useState<'graph' | 'table'>('graph');
|
|
618
|
+
const [pulseBPViewMode, setPulseBPViewMode] = useState<'graph' | 'table'>('graph');
|
|
619
|
+
const [temperatureViewMode, setTemperatureViewMode] = useState<'graph' | 'table'>('graph');
|
|
620
|
+
const [urineTestViewMode, setUrineTestViewMode] = useState<'graph' | 'table'>('graph');
|
|
621
|
+
const [fetalHeartRateCurrentPage, setFetalHeartRateCurrentPage] = useState(1);
|
|
622
|
+
const [fetalHeartRatePageSize, setFetalHeartRatePageSize] = useState(5);
|
|
623
|
+
const [membraneAmnioticFluidCurrentPage, setMembraneAmnioticFluidCurrentPage] = useState(1);
|
|
624
|
+
const [membraneAmnioticFluidPageSize, setMembraneAmnioticFluidPageSize] = useState(5);
|
|
625
|
+
const [cervicalContractionsCurrentPage, setCervicalContractionsCurrentPage] = useState(1);
|
|
626
|
+
const [cervicalContractionsPageSize, setCervicalContractionsPageSize] = useState(5);
|
|
627
|
+
const [oxytocinCurrentPage, setOxytocinCurrentPage] = useState(1);
|
|
628
|
+
const [oxytocinPageSize, setOxytocinPageSize] = useState(5);
|
|
629
|
+
const [drugsIVFluidsCurrentPage, setDrugsIVFluidsCurrentPage] = useState(1);
|
|
630
|
+
const [pulseBPCurrentPage, setPulseBPCurrentPage] = useState(1);
|
|
631
|
+
const [temperatureCurrentPage, setTemperatureCurrentPage] = useState(1);
|
|
632
|
+
const [temperaturePageSize, setTemperaturePageSize] = useState(5);
|
|
633
|
+
const [urineTestCurrentPage, setUrineTestCurrentPage] = useState(1);
|
|
634
|
+
const [urineTestPageSize, setUrineTestPageSize] = useState(5);
|
|
635
|
+
const [drugsIVFluidsPageSize, setDrugsIVFluidsPageSize] = useState(5);
|
|
636
|
+
const [pulseBPPageSize, setPulseBPPageSize] = useState(5);
|
|
59
637
|
const [currentPage, setCurrentPage] = useState<Record<string, number>>({});
|
|
60
638
|
const [pageSize, setPageSize] = useState<Record<string, number>>({});
|
|
61
639
|
const [isLoading, setIsLoading] = useState<Record<string, boolean>>({});
|
|
62
640
|
|
|
63
|
-
|
|
641
|
+
// Transform cervix data to match the existing format expected by the chart
|
|
642
|
+
const cervixFormData = useMemo(() => {
|
|
643
|
+
// Use OpenMRS data if available
|
|
644
|
+
const dataToUse = loadedCervixData.length > 0 ? loadedCervixData : [];
|
|
645
|
+
|
|
646
|
+
const processedData = dataToUse
|
|
647
|
+
.map((data) => ({
|
|
648
|
+
hour: data.hour || 0,
|
|
649
|
+
time: data.time || '',
|
|
650
|
+
cervicalDilation: data.cervicalDilation || 0,
|
|
651
|
+
descentOfHead: data.descentOfHead || 0,
|
|
652
|
+
entryDate: new Date(data.encounterDatetime).toLocaleDateString(),
|
|
653
|
+
entryTime: new Date(data.encounterDatetime).toLocaleTimeString(),
|
|
654
|
+
}))
|
|
655
|
+
.filter((data) => data.hour > 0 && data.cervicalDilation > 0 && data.descentOfHead > 0);
|
|
656
|
+
|
|
657
|
+
// Return dummy data if no processed data and dummy data is enabled, otherwise return processed data
|
|
658
|
+
return processedData.length > 0 ? processedData : ENABLE_DUMMY_DATA ? generateExtendedDummyData() : [];
|
|
659
|
+
}, [loadedCervixData, ENABLE_DUMMY_DATA]);
|
|
660
|
+
|
|
661
|
+
// Compute existingTimeEntries from both local and OpenMRS data
|
|
662
|
+
const computedExistingTimeEntries = useMemo(() => {
|
|
663
|
+
// Use the existing time entries from OpenMRS only
|
|
664
|
+
return existingTimeEntries;
|
|
665
|
+
}, [existingTimeEntries]);
|
|
666
|
+
|
|
667
|
+
// Compute combined fetal heart rate data from both local and OpenMRS sources
|
|
668
|
+
const computedFetalHeartRateData = useMemo(() => {
|
|
669
|
+
const combined = [...localFetalHeartRateData];
|
|
670
|
+
|
|
671
|
+
// Add OpenMRS data if available and not already in local data
|
|
672
|
+
if (loadedFetalHeartRateData?.length > 0) {
|
|
673
|
+
loadedFetalHeartRateData.forEach((openMrsEntry) => {
|
|
674
|
+
const exists = combined.find(
|
|
675
|
+
(localEntry) => localEntry.hour === openMrsEntry.hour && localEntry.time === openMrsEntry.time,
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
if (!exists) {
|
|
679
|
+
// Transform OpenMRS data to match local data structure
|
|
680
|
+
const transformedEntry = {
|
|
681
|
+
hour: openMrsEntry.hour,
|
|
682
|
+
value: openMrsEntry.fetalHeartRate, // Map fetalHeartRate to value
|
|
683
|
+
group: 'Fetal Heart Rate',
|
|
684
|
+
time: openMrsEntry.time,
|
|
685
|
+
};
|
|
686
|
+
combined.push(transformedEntry);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Sort by hour and time
|
|
692
|
+
const sorted = combined.sort((a, b) => {
|
|
693
|
+
if (a.hour !== b.hour) {
|
|
694
|
+
return a.hour - b.hour;
|
|
695
|
+
}
|
|
696
|
+
return a.time.localeCompare(b.time);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
return sorted;
|
|
700
|
+
}, [localFetalHeartRateData, loadedFetalHeartRateData]);
|
|
701
|
+
const partographGraphs: GraphDefinition[] = useMemo(
|
|
702
|
+
() => getTranslatedPartographyGraphs(t) as GraphDefinition[],
|
|
703
|
+
[t],
|
|
704
|
+
);
|
|
64
705
|
|
|
65
706
|
useEffect(() => {
|
|
66
707
|
const initialViewMode = {};
|
|
@@ -86,7 +727,65 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
|
86
727
|
|
|
87
728
|
useEffect(() => {
|
|
88
729
|
if (!isLoading) {
|
|
89
|
-
|
|
730
|
+
let chartData: ChartDataPoint[] = [];
|
|
731
|
+
|
|
732
|
+
// Special handling for pulse and BP combined encounters
|
|
733
|
+
if (graphType === 'maternal-pulse' || graphType === 'blood-pressure') {
|
|
734
|
+
// Collect all obs for pulse and BP from all encounters
|
|
735
|
+
chartData = [];
|
|
736
|
+
if (Array.isArray(encounters)) {
|
|
737
|
+
encounters.forEach((encounter) => {
|
|
738
|
+
if (Array.isArray(encounter.obs)) {
|
|
739
|
+
const time = new Date(encounter.encounterDatetime).toLocaleTimeString([], {
|
|
740
|
+
hour: '2-digit',
|
|
741
|
+
minute: '2-digit',
|
|
742
|
+
});
|
|
743
|
+
encounter.obs.forEach((obs) => {
|
|
744
|
+
if (graphType === 'maternal-pulse' && obs.concept.uuid === PARTOGRAPHY_CONCEPTS['maternal-pulse']) {
|
|
745
|
+
chartData.push({
|
|
746
|
+
hour: 0,
|
|
747
|
+
group: 'Maternal Pulse',
|
|
748
|
+
time,
|
|
749
|
+
value: typeof obs.value === 'number' ? obs.value : parseFloat(obs.value),
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
if (graphType === 'blood-pressure') {
|
|
753
|
+
if (obs.concept.uuid === PARTOGRAPHY_CONCEPTS['systolic-bp']) {
|
|
754
|
+
chartData.push({
|
|
755
|
+
hour: 0,
|
|
756
|
+
group: 'Systolic',
|
|
757
|
+
time,
|
|
758
|
+
value: typeof obs.value === 'number' ? obs.value : parseFloat(obs.value),
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
if (obs.concept.uuid === PARTOGRAPHY_CONCEPTS['diastolic-bp']) {
|
|
762
|
+
chartData.push({
|
|
763
|
+
hour: 0,
|
|
764
|
+
group: 'Diastolic',
|
|
765
|
+
time,
|
|
766
|
+
value: typeof obs.value === 'number' ? obs.value : parseFloat(obs.value),
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
} else if (localPartographyData[graphType] && localPartographyData[graphType].length > 0) {
|
|
775
|
+
// Transform local data to chart format
|
|
776
|
+
chartData = localPartographyData[graphType].map((item, index) => ({
|
|
777
|
+
hour: index + 1, // Use index as hour for now
|
|
778
|
+
group: graphType,
|
|
779
|
+
time: item.time || `Point ${index + 1}`,
|
|
780
|
+
value: item.value || item.measurementValue || 0,
|
|
781
|
+
}));
|
|
782
|
+
} else {
|
|
783
|
+
chartData = transformEncounterToChartData(encounters, graphType);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (chartData.length === 0 && ENABLE_DUMMY_DATA) {
|
|
787
|
+
chartData = generateDummyDataForGraph(graphType);
|
|
788
|
+
}
|
|
90
789
|
|
|
91
790
|
setGraphData((prevData) => ({
|
|
92
791
|
...prevData,
|
|
@@ -103,30 +802,173 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
|
103
802
|
return { encounters, isLoading, mutate };
|
|
104
803
|
};
|
|
105
804
|
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
805
|
+
// Function to generate dummy data for different graph types
|
|
806
|
+
const generateDummyDataForGraph = (graphType: string): ChartDataPoint[] => {
|
|
807
|
+
const baseTimeEntries = [
|
|
808
|
+
{ hour: 0, time: '08:00' },
|
|
809
|
+
{ hour: 1, time: '09:00' },
|
|
810
|
+
{ hour: 2, time: '10:00' },
|
|
811
|
+
{ hour: 3, time: '11:00' },
|
|
812
|
+
{ hour: 4, time: '12:00' },
|
|
813
|
+
{ hour: 5, time: '13:00' },
|
|
814
|
+
{ hour: 6, time: '14:00' },
|
|
815
|
+
];
|
|
816
|
+
|
|
817
|
+
switch (graphType) {
|
|
818
|
+
case 'fetal-heart-rate':
|
|
819
|
+
return baseTimeEntries.map((entry) => ({
|
|
820
|
+
hour: entry.hour,
|
|
821
|
+
time: entry.time,
|
|
822
|
+
group: 'Fetal Heart Rate',
|
|
823
|
+
value: 140 + Math.random() * 20, // Normal range 120-160
|
|
824
|
+
}));
|
|
825
|
+
|
|
826
|
+
case 'maternal-pulse':
|
|
827
|
+
return baseTimeEntries.map((entry) => ({
|
|
828
|
+
hour: entry.hour,
|
|
829
|
+
time: entry.time,
|
|
830
|
+
group: 'Maternal Pulse',
|
|
831
|
+
value: 75 + Math.random() * 15, // Normal range 60-100
|
|
832
|
+
}));
|
|
833
|
+
|
|
834
|
+
case 'blood-pressure':
|
|
835
|
+
return [
|
|
836
|
+
...baseTimeEntries.map((entry) => ({
|
|
837
|
+
hour: entry.hour,
|
|
838
|
+
time: entry.time,
|
|
839
|
+
group: 'Systolic',
|
|
840
|
+
value: 115 + Math.random() * 10, // Normal systolic
|
|
841
|
+
})),
|
|
842
|
+
...baseTimeEntries.map((entry) => ({
|
|
843
|
+
hour: entry.hour,
|
|
844
|
+
time: entry.time,
|
|
845
|
+
group: 'Diastolic',
|
|
846
|
+
value: 75 + Math.random() * 5, // Normal diastolic
|
|
847
|
+
})),
|
|
848
|
+
];
|
|
849
|
+
|
|
850
|
+
case 'temperature':
|
|
851
|
+
return baseTimeEntries.map((entry) => ({
|
|
852
|
+
hour: entry.hour,
|
|
853
|
+
time: entry.time,
|
|
854
|
+
group: 'Temperature',
|
|
855
|
+
value: 36.5 + Math.random() * 0.8, // Normal range 36.5-37.3
|
|
856
|
+
}));
|
|
857
|
+
|
|
858
|
+
case 'uterine-contractions':
|
|
859
|
+
return baseTimeEntries.map((entry) => ({
|
|
860
|
+
hour: entry.hour,
|
|
861
|
+
time: entry.time,
|
|
862
|
+
group: 'Contractions per 10 minutes',
|
|
863
|
+
value: Math.floor(Math.random() * 5) + 2, // 2-6 contractions
|
|
864
|
+
}));
|
|
865
|
+
|
|
866
|
+
default:
|
|
867
|
+
return [];
|
|
868
|
+
}
|
|
128
869
|
};
|
|
129
870
|
|
|
871
|
+
const cervixData = useGraphData('cervix');
|
|
872
|
+
|
|
873
|
+
// Enable some data hooks for development/demo purposes
|
|
874
|
+
// Remove duplicate declaration and broken object syntax
|
|
875
|
+
|
|
876
|
+
// Apply custom styling for cervix chart lines after render
|
|
877
|
+
useEffect(() => {
|
|
878
|
+
const applyChartStyling = () => {
|
|
879
|
+
const chartContainer = document.querySelector(`[data-chart-id="cervix"]`);
|
|
880
|
+
if (chartContainer) {
|
|
881
|
+
// Style the Alert and Action lines
|
|
882
|
+
const svgPaths = chartContainer.querySelectorAll('svg path');
|
|
883
|
+
svgPaths.forEach((path, index) => {
|
|
884
|
+
const pathElement = path as SVGPathElement;
|
|
885
|
+
// Check if this path represents Alert or Action line by checking its data
|
|
886
|
+
const pathData = pathElement.getAttribute('d');
|
|
887
|
+
if (pathData) {
|
|
888
|
+
// Alert Line should start at hour 0
|
|
889
|
+
if (pathData.includes('M0') || pathData.includes('L0')) {
|
|
890
|
+
pathElement.style.stroke = '#FFD700'; // Yellow
|
|
891
|
+
pathElement.style.strokeWidth = '3px';
|
|
892
|
+
pathElement.style.strokeDasharray = '8,4';
|
|
893
|
+
}
|
|
894
|
+
// Action Line should start at hour 4
|
|
895
|
+
else if (pathData.includes('M4') || pathData.includes('L4')) {
|
|
896
|
+
pathElement.style.stroke = '#FF0000'; // Red
|
|
897
|
+
pathElement.style.strokeWidth = '3px';
|
|
898
|
+
pathElement.style.strokeDasharray = '8,4';
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// Style cervical dilation points as X marks
|
|
904
|
+
const svgCircles = chartContainer.querySelectorAll('svg circle');
|
|
905
|
+
svgCircles.forEach((circle) => {
|
|
906
|
+
const circleElement = circle as SVGCircleElement;
|
|
907
|
+
// Check if this circle belongs to cervical dilation data
|
|
908
|
+
const parentGroup = circleElement.closest('g');
|
|
909
|
+
if (parentGroup) {
|
|
910
|
+
// Look for cervical dilation color or class indicators
|
|
911
|
+
const stroke = circleElement.getAttribute('stroke') || circleElement.style.stroke;
|
|
912
|
+
const fill = circleElement.getAttribute('fill') || circleElement.style.fill;
|
|
913
|
+
|
|
914
|
+
// If this is a cervical dilation point (green color), convert to X
|
|
915
|
+
if (stroke === '#22C55E' || fill === '#22C55E') {
|
|
916
|
+
// Hide the original circle
|
|
917
|
+
circleElement.style.display = 'none';
|
|
918
|
+
|
|
919
|
+
// Create X mark using two crossing lines
|
|
920
|
+
const cx = parseFloat(circleElement.getAttribute('cx') || '0');
|
|
921
|
+
const cy = parseFloat(circleElement.getAttribute('cy') || '0');
|
|
922
|
+
const size = 6; // Size of the X mark
|
|
923
|
+
|
|
924
|
+
const svg = circleElement.ownerSVGElement;
|
|
925
|
+
if (svg) {
|
|
926
|
+
// Create first line of X (top-left to bottom-right)
|
|
927
|
+
const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
928
|
+
line1.setAttribute('x1', (cx - size).toString());
|
|
929
|
+
line1.setAttribute('y1', (cy - size).toString());
|
|
930
|
+
line1.setAttribute('x2', (cx + size).toString());
|
|
931
|
+
line1.setAttribute('y2', (cy + size).toString());
|
|
932
|
+
line1.setAttribute('stroke', '#22C55E');
|
|
933
|
+
line1.setAttribute('stroke-width', '3');
|
|
934
|
+
line1.setAttribute('stroke-linecap', 'round');
|
|
935
|
+
|
|
936
|
+
// Create second line of X (top-right to bottom-left)
|
|
937
|
+
const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
938
|
+
line2.setAttribute('x1', (cx + size).toString());
|
|
939
|
+
line2.setAttribute('y1', (cy - size).toString());
|
|
940
|
+
line2.setAttribute('x2', (cx - size).toString());
|
|
941
|
+
line2.setAttribute('y2', (cy + size).toString());
|
|
942
|
+
line2.setAttribute('stroke', '#22C55E');
|
|
943
|
+
line2.setAttribute('stroke-width', '3');
|
|
944
|
+
line2.setAttribute('stroke-linecap', 'round');
|
|
945
|
+
|
|
946
|
+
// Insert the X lines after the circle
|
|
947
|
+
parentGroup.appendChild(line1);
|
|
948
|
+
parentGroup.appendChild(line2);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
const timer = setTimeout(applyChartStyling, 100);
|
|
957
|
+
return () => clearTimeout(timer);
|
|
958
|
+
}, [cervixFormData, isLoading, isCervixDataLoading]);
|
|
959
|
+
|
|
960
|
+
// Patch: Add fetch/post logging for OpenMRS API
|
|
961
|
+
|
|
962
|
+
// Wrap openmrsFetch to log all requests and responses
|
|
963
|
+
function openmrsFetchWithLogging(url, options) {
|
|
964
|
+
return openmrsFetch(url, options)
|
|
965
|
+
.then((response) => response)
|
|
966
|
+
.catch((error) => {
|
|
967
|
+
throw error;
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// If patientUuid is not available, show a message to select a patient
|
|
130
972
|
if (!patientUuid) {
|
|
131
973
|
return (
|
|
132
974
|
<div className={styles.partographyContainer}>
|
|
@@ -144,9 +986,19 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
|
144
986
|
);
|
|
145
987
|
}
|
|
146
988
|
|
|
989
|
+
// Show error if cervix data failed to load
|
|
990
|
+
if (cervixDataError) {
|
|
991
|
+
console.error('Error loading cervix data:', cervixDataError);
|
|
992
|
+
// Continue rendering but show warning - don't block the entire UI
|
|
993
|
+
}
|
|
994
|
+
|
|
147
995
|
const handleAddDataPoint = (graphId: string) => {
|
|
148
|
-
|
|
149
|
-
|
|
996
|
+
if (graphId === 'cervix') {
|
|
997
|
+
setIsCervixFormOpen(true);
|
|
998
|
+
} else {
|
|
999
|
+
setSelectedGraphType(graphId);
|
|
1000
|
+
setIsFormOpen(true);
|
|
1001
|
+
}
|
|
150
1002
|
};
|
|
151
1003
|
|
|
152
1004
|
const handleFormSubmit = async (formData: any) => {
|
|
@@ -156,32 +1008,22 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
|
156
1008
|
[formData.graphType]: true,
|
|
157
1009
|
}));
|
|
158
1010
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
formData
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
t,
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
if (!encounterResult.success) {
|
|
169
|
-
throw new Error(encounterResult.message);
|
|
170
|
-
}
|
|
1011
|
+
// Add to local state instead of saving to OpenMRS
|
|
1012
|
+
const newEntry = {
|
|
1013
|
+
...formData,
|
|
1014
|
+
timestamp: new Date(),
|
|
1015
|
+
id: Date.now(), // Simple ID for local data
|
|
1016
|
+
};
|
|
171
1017
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
1018
|
+
setLocalPartographyData((prev) => ({
|
|
1019
|
+
...prev,
|
|
1020
|
+
[formData.graphType]: [...(prev[formData.graphType] || []), newEntry],
|
|
1021
|
+
}));
|
|
176
1022
|
|
|
177
1023
|
setIsFormOpen(false);
|
|
178
1024
|
setSelectedGraphType('');
|
|
179
1025
|
} catch (error) {
|
|
180
|
-
|
|
181
|
-
...prev,
|
|
182
|
-
[formData.graphType]: false,
|
|
183
|
-
}));
|
|
184
|
-
alert(`Failed to save partography data: ${error.message}. Please try again.`);
|
|
1026
|
+
alert(`Failed to add partography data: ${error.message}. Please try again.`);
|
|
185
1027
|
} finally {
|
|
186
1028
|
setIsLoading((prev) => ({
|
|
187
1029
|
...prev,
|
|
@@ -195,6 +1037,600 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
|
195
1037
|
setSelectedGraphType('');
|
|
196
1038
|
};
|
|
197
1039
|
|
|
1040
|
+
const handleCervixFormClose = () => {
|
|
1041
|
+
setIsCervixFormOpen(false);
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
const handleCervixFormSubmit = async (formData: {
|
|
1045
|
+
hour: number;
|
|
1046
|
+
time: string;
|
|
1047
|
+
cervicalDilation: number;
|
|
1048
|
+
descentOfHead: number;
|
|
1049
|
+
}) => {
|
|
1050
|
+
// Validate data before adding to prevent NaN values
|
|
1051
|
+
if (
|
|
1052
|
+
isNaN(formData.hour) ||
|
|
1053
|
+
isNaN(formData.cervicalDilation) ||
|
|
1054
|
+
isNaN(formData.descentOfHead) ||
|
|
1055
|
+
!formData.time ||
|
|
1056
|
+
formData.time.trim() === ''
|
|
1057
|
+
) {
|
|
1058
|
+
alert('Invalid data detected. Please ensure all fields are properly filled.');
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (
|
|
1063
|
+
formData.hour < 0 ||
|
|
1064
|
+
formData.hour > 23 ||
|
|
1065
|
+
formData.cervicalDilation < 0 ||
|
|
1066
|
+
formData.cervicalDilation > 10 ||
|
|
1067
|
+
formData.descentOfHead < 1 ||
|
|
1068
|
+
formData.descentOfHead > 5
|
|
1069
|
+
) {
|
|
1070
|
+
alert('Values are outside acceptable medical ranges. Please check your inputs.');
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const saveResult = await saveCervixFormData(
|
|
1075
|
+
patientUuid,
|
|
1076
|
+
{
|
|
1077
|
+
hour: String(formData.hour),
|
|
1078
|
+
time: formData.time,
|
|
1079
|
+
cervicalDilation: String(formData.cervicalDilation),
|
|
1080
|
+
descent: String(formData.descentOfHead), // direct numeric value
|
|
1081
|
+
},
|
|
1082
|
+
t,
|
|
1083
|
+
session?.currentProvider?.uuid,
|
|
1084
|
+
session?.sessionLocation?.uuid,
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
if (saveResult.success) {
|
|
1088
|
+
// Optionally refresh data from backend here
|
|
1089
|
+
if (typeof mutateCervixData === 'function') {
|
|
1090
|
+
mutateCervixData();
|
|
1091
|
+
}
|
|
1092
|
+
setIsCervixFormOpen(false);
|
|
1093
|
+
} else {
|
|
1094
|
+
alert('Failed to save data: ' + saveResult.message);
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
const handleCervixDataSaved = () => {
|
|
1099
|
+
mutateCervixData(); // Refresh the cervix data from OpenMRS
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
// Fetal Heart Rate handlers
|
|
1103
|
+
const handleFetalHeartRateFormSubmit = (formData: { hour: number; time: string; fetalHeartRate: number }) => {
|
|
1104
|
+
// Validate data before adding
|
|
1105
|
+
if (isNaN(formData.hour) || isNaN(formData.fetalHeartRate) || !formData.time || formData.time.trim() === '') {
|
|
1106
|
+
alert('Invalid data detected. Please ensure all fields are properly filled.');
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Additional validation for reasonable values
|
|
1111
|
+
if (formData.hour < 0 || formData.hour > 24 || formData.fetalHeartRate < 80 || formData.fetalHeartRate > 200) {
|
|
1112
|
+
alert('Values are outside acceptable medical ranges. Please check your inputs.');
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Add to local state for immediate UI update
|
|
1117
|
+
const newEntry = {
|
|
1118
|
+
hour: formData.hour,
|
|
1119
|
+
value: formData.fetalHeartRate,
|
|
1120
|
+
group: 'Fetal Heart Rate',
|
|
1121
|
+
time: formData.time,
|
|
1122
|
+
date: new Date().toLocaleDateString(),
|
|
1123
|
+
id: `fhr-${Date.now()}`,
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
setLocalFetalHeartRateData((prev) => [...prev, newEntry]);
|
|
1127
|
+
setIsFetalHeartRateFormOpen(false);
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
// Callback for when fetal heart rate data is saved to OpenMRS
|
|
1131
|
+
const handleFetalHeartRateDataSaved = () => {
|
|
1132
|
+
mutateFetalHeartRateData(); // Refresh the fetal heart rate data from OpenMRS
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
const handleFetalHeartRateFormClose = () => {
|
|
1136
|
+
setIsFetalHeartRateFormOpen(false);
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// Membrane Amniotic Fluid handlers
|
|
1140
|
+
const handleMembraneAmnioticFluidFormSubmit = async (formData: {
|
|
1141
|
+
timeSlot: string;
|
|
1142
|
+
exactTime: string;
|
|
1143
|
+
amnioticFluid: string;
|
|
1144
|
+
moulding: string;
|
|
1145
|
+
}) => {
|
|
1146
|
+
if (
|
|
1147
|
+
!formData.timeSlot ||
|
|
1148
|
+
!formData.exactTime ||
|
|
1149
|
+
!formData.amnioticFluid ||
|
|
1150
|
+
!formData.moulding ||
|
|
1151
|
+
formData.timeSlot.trim() === '' ||
|
|
1152
|
+
formData.exactTime.trim() === '' ||
|
|
1153
|
+
formData.amnioticFluid.trim() === '' ||
|
|
1154
|
+
formData.moulding.trim() === ''
|
|
1155
|
+
) {
|
|
1156
|
+
alert('Invalid data detected. Please ensure all fields are properly filled.');
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const saveResult = await saveMembraneAmnioticFluidData(
|
|
1161
|
+
patientUuid,
|
|
1162
|
+
{
|
|
1163
|
+
amnioticFluid: formData.amnioticFluid,
|
|
1164
|
+
moulding: formData.moulding,
|
|
1165
|
+
time: formData.exactTime,
|
|
1166
|
+
},
|
|
1167
|
+
t,
|
|
1168
|
+
session?.sessionLocation?.uuid,
|
|
1169
|
+
session?.currentProvider?.uuid,
|
|
1170
|
+
);
|
|
1171
|
+
await mutateMembraneAmnioticFluidData();
|
|
1172
|
+
setIsMembraneAmnioticFluidFormOpen(false);
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
const handleMembraneAmnioticFluidFormClose = () => {
|
|
1176
|
+
setIsMembraneAmnioticFluidFormOpen(false);
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
const handleCervicalContractionsFormSubmit = async (formData: {
|
|
1180
|
+
contractionLevel: string;
|
|
1181
|
+
contractionCount: string;
|
|
1182
|
+
timeSlot: string;
|
|
1183
|
+
}) => {
|
|
1184
|
+
const contractionLevelUuidMap: Record<string, string> = {
|
|
1185
|
+
none: '1107AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1186
|
+
mild: '1498AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1187
|
+
moderate: '1499AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1188
|
+
strong: '166788AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1189
|
+
};
|
|
1190
|
+
const contractionLevel = formData.contractionLevel || 'none';
|
|
1191
|
+
const contractionLevelValue = contractionLevelUuidMap[contractionLevel];
|
|
1192
|
+
const contractionLevelUuid = contractionLevelValue; // For coded obs, concept and value are the same
|
|
1193
|
+
const contractionCountConcept = '159682AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
|
1194
|
+
// Always send contractionCount as a number
|
|
1195
|
+
const contractionCount = parseInt(formData.contractionCount || '1', 10);
|
|
1196
|
+
const timeSlot = formData.timeSlot || new Date().toISOString().substring(11, 16);
|
|
1197
|
+
|
|
1198
|
+
// Debug info removed for linting
|
|
1199
|
+
|
|
1200
|
+
// Always send both obs: count (numeric) and level (coded)
|
|
1201
|
+
const encounterFormData = {
|
|
1202
|
+
contractionLevelValue, // UUID for level (none, mild, moderate, strong)
|
|
1203
|
+
contractionLevelUuid,
|
|
1204
|
+
contractionCount,
|
|
1205
|
+
contractionCountConcept,
|
|
1206
|
+
timeSlot,
|
|
1207
|
+
};
|
|
1208
|
+
// Payload prepared for submission
|
|
1209
|
+
try {
|
|
1210
|
+
const saveResult = await createPartographyEncounter(patientUuid, 'uterine-contractions', encounterFormData);
|
|
1211
|
+
if (saveResult.success) {
|
|
1212
|
+
if (typeof mutateCervicalContractions === 'function') {
|
|
1213
|
+
await mutateCervicalContractions();
|
|
1214
|
+
}
|
|
1215
|
+
setIsCervicalContractionsFormOpen(false);
|
|
1216
|
+
} else {
|
|
1217
|
+
console.error('Failed to save cervical contractions:', saveResult.message);
|
|
1218
|
+
}
|
|
1219
|
+
} catch (err) {
|
|
1220
|
+
console.error('Error during cervical contractions save:', err);
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
const handleCervicalContractionsFormClose = () => {
|
|
1225
|
+
setIsCervicalContractionsFormOpen(false);
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
// Oxytocin handlers
|
|
1229
|
+
const handleOxytocinFormSubmit = async (formData: {
|
|
1230
|
+
oxytocinUsed: 'yes' | 'no';
|
|
1231
|
+
dropsPerMinute: number;
|
|
1232
|
+
timeSlot: string;
|
|
1233
|
+
}) => {
|
|
1234
|
+
// Validate data before saving
|
|
1235
|
+
if (!formData.timeSlot || formData.timeSlot.trim() === '') {
|
|
1236
|
+
alert('Time is required.');
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (
|
|
1240
|
+
formData.oxytocinUsed === 'yes' &&
|
|
1241
|
+
(isNaN(formData.dropsPerMinute) || formData.dropsPerMinute < 0 || formData.dropsPerMinute > 60)
|
|
1242
|
+
) {
|
|
1243
|
+
alert('Drops per minute must be between 0 and 60 when oxytocin is used.');
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (formData.oxytocinUsed === 'yes') {
|
|
1248
|
+
try {
|
|
1249
|
+
const saveResult = await saveOxytocinFormData(
|
|
1250
|
+
patientUuid,
|
|
1251
|
+
{
|
|
1252
|
+
time: formData.timeSlot,
|
|
1253
|
+
dropsPerMinute: String(formData.dropsPerMinute),
|
|
1254
|
+
},
|
|
1255
|
+
t,
|
|
1256
|
+
session?.currentProvider?.uuid,
|
|
1257
|
+
session?.sessionLocation?.uuid,
|
|
1258
|
+
);
|
|
1259
|
+
|
|
1260
|
+
if (saveResult.success) {
|
|
1261
|
+
if (typeof mutateOxytocinData === 'function') {
|
|
1262
|
+
mutateOxytocinData();
|
|
1263
|
+
}
|
|
1264
|
+
setIsOxytocinFormOpen(false);
|
|
1265
|
+
} else {
|
|
1266
|
+
alert('Failed to save oxytocin data: ' + saveResult.message);
|
|
1267
|
+
}
|
|
1268
|
+
} catch (err) {
|
|
1269
|
+
console.error('Error saving oxytocin data:', err);
|
|
1270
|
+
alert('Failed to save oxytocin data.');
|
|
1271
|
+
}
|
|
1272
|
+
} else {
|
|
1273
|
+
setIsOxytocinFormOpen(false);
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
const handleOxytocinFormClose = () => {
|
|
1278
|
+
setIsOxytocinFormOpen(false);
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
// Drugs and IV Fluids handlers
|
|
1282
|
+
const handleDrugsIVFluidsFormSubmit = (formData: {
|
|
1283
|
+
drugName: string;
|
|
1284
|
+
dosage: string;
|
|
1285
|
+
route: string;
|
|
1286
|
+
frequency: string;
|
|
1287
|
+
}) => {
|
|
1288
|
+
// Validate data before adding
|
|
1289
|
+
if (!formData.drugName || !formData.dosage || formData.drugName.trim() === '' || formData.dosage.trim() === '') {
|
|
1290
|
+
alert('Invalid data detected. Please ensure all fields are properly filled.');
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// Add to local state
|
|
1295
|
+
const newEntry = {
|
|
1296
|
+
drugName: formData.drugName,
|
|
1297
|
+
dosage: formData.dosage,
|
|
1298
|
+
route: formData.route,
|
|
1299
|
+
frequency: formData.frequency,
|
|
1300
|
+
date: new Date().toLocaleDateString(),
|
|
1301
|
+
id: `drugs-${Date.now()}`,
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1304
|
+
setLocalDrugsIVFluidsData((prev) => [...prev, newEntry]);
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
const handleDrugOrderDataSaved = () => {
|
|
1308
|
+
// Drug order data saved, refreshing drug orders...
|
|
1309
|
+
// Force refresh the drug orders data from OpenMRS
|
|
1310
|
+
mutateDrugOrders();
|
|
1311
|
+
// Also trigger a revalidation after a short delay
|
|
1312
|
+
setTimeout(() => {
|
|
1313
|
+
// Triggering second refresh...
|
|
1314
|
+
mutateDrugOrders();
|
|
1315
|
+
}, 2000);
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
// Pulse and BP handlers
|
|
1319
|
+
const handlePulseBPFormSubmit = async (formData: { pulse: number; systolicBP: number; diastolicBP: number }) => {
|
|
1320
|
+
if (!formData.pulse || !formData.systolicBP || !formData.diastolicBP) {
|
|
1321
|
+
alert('Invalid data detected. Please ensure all fields are properly filled.');
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
try {
|
|
1325
|
+
// Save both pulse and BP in a single encounter
|
|
1326
|
+
await createPartographyEncounter(patientUuid, 'pulse-bp-combined', {
|
|
1327
|
+
pulse: formData.pulse,
|
|
1328
|
+
systolic: formData.systolicBP,
|
|
1329
|
+
diastolic: formData.diastolicBP,
|
|
1330
|
+
});
|
|
1331
|
+
await mutatePulseData();
|
|
1332
|
+
await mutateBPData();
|
|
1333
|
+
// Force refresh of graph/table by toggling view mode
|
|
1334
|
+
setPulseBPViewMode((prev) => (prev === 'table' ? 'graph' : 'table'));
|
|
1335
|
+
setTimeout(() => setPulseBPViewMode('graph'), 0);
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
alert('Failed to save Pulse & BP data.');
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
// Temperature backend save handler
|
|
1342
|
+
const handleTemperatureFormSubmit = async (formData: {
|
|
1343
|
+
timeSlot: string;
|
|
1344
|
+
exactTime: string;
|
|
1345
|
+
temperature: number;
|
|
1346
|
+
}) => {
|
|
1347
|
+
if (!formData.timeSlot || !formData.exactTime || !formData.temperature) {
|
|
1348
|
+
alert('Invalid data detected. Please ensure all fields are properly filled.');
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
// Save to backend
|
|
1352
|
+
try {
|
|
1353
|
+
await createPartographyEncounter(patientUuid, 'temperature', {
|
|
1354
|
+
value: formData.temperature,
|
|
1355
|
+
time: formData.exactTime,
|
|
1356
|
+
});
|
|
1357
|
+
await mutateTemperatureData();
|
|
1358
|
+
setIsTemperatureFormOpen(false);
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
alert('Failed to save temperature data.');
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
const handleUrineTestFormSubmit = async (formData: {
|
|
1365
|
+
timeSlot: string;
|
|
1366
|
+
exactTime: string;
|
|
1367
|
+
protein: string;
|
|
1368
|
+
acetone: string;
|
|
1369
|
+
volume: number;
|
|
1370
|
+
timeSampleCollected: string;
|
|
1371
|
+
timeResultsReturned: string;
|
|
1372
|
+
}) => {
|
|
1373
|
+
// Validate data before adding
|
|
1374
|
+
if (
|
|
1375
|
+
!formData.timeSlot ||
|
|
1376
|
+
!formData.exactTime ||
|
|
1377
|
+
!formData.protein ||
|
|
1378
|
+
!formData.acetone ||
|
|
1379
|
+
!formData.volume ||
|
|
1380
|
+
!formData.timeSampleCollected ||
|
|
1381
|
+
!formData.timeResultsReturned
|
|
1382
|
+
) {
|
|
1383
|
+
alert('Invalid data detected. Please ensure all fields are properly filled.');
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Map label to UUID for protein and acetone/ketone
|
|
1388
|
+
const labelToUuid = (label: string) => {
|
|
1389
|
+
switch (label) {
|
|
1390
|
+
case '0':
|
|
1391
|
+
return '1107AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
|
1392
|
+
case '+':
|
|
1393
|
+
return '1362AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
|
1394
|
+
case '++':
|
|
1395
|
+
return '1363AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
|
1396
|
+
case '+++':
|
|
1397
|
+
return '1364AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
|
1398
|
+
default:
|
|
1399
|
+
return label;
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
try {
|
|
1404
|
+
// Get current time in HH:mm format
|
|
1405
|
+
const now = new Date();
|
|
1406
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
1407
|
+
const currentTime = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
1408
|
+
await createPartographyEncounter(patientUuid, 'urine-analysis', {
|
|
1409
|
+
proteinLevel: labelToUuid(formData.protein),
|
|
1410
|
+
ketoneLevel: labelToUuid(formData.acetone),
|
|
1411
|
+
volume: formData.volume,
|
|
1412
|
+
timeSampleCollected: formData.timeSampleCollected,
|
|
1413
|
+
timeResultsReturned: formData.timeResultsReturned,
|
|
1414
|
+
eventDescription: `Time Slot: ${currentTime}`,
|
|
1415
|
+
});
|
|
1416
|
+
setIsUrineTestFormOpen(false);
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
alert('Failed to save urine test data.');
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1422
|
+
// Generate table data for membrane amniotic fluid from backend only
|
|
1423
|
+
const getMembraneAmnioticFluidTableData = () => {
|
|
1424
|
+
return membraneAmnioticFluidEntries.map((data, index) => ({
|
|
1425
|
+
id: data.id || `maf-${index}`,
|
|
1426
|
+
date: data.date,
|
|
1427
|
+
timeSlot: data.timeSlot || '',
|
|
1428
|
+
exactTime: data.time || '',
|
|
1429
|
+
amnioticFluid: data.amnioticFluid,
|
|
1430
|
+
moulding: data.moulding,
|
|
1431
|
+
}));
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
// Generate table data for fetal heart rate
|
|
1435
|
+
const getFetalHeartRateTableData = () => {
|
|
1436
|
+
return computedFetalHeartRateData.map((data, index) => {
|
|
1437
|
+
const getStatus = (value: number) => {
|
|
1438
|
+
if (value < 100) {
|
|
1439
|
+
return 'Low';
|
|
1440
|
+
}
|
|
1441
|
+
if (value >= 100 && value <= 180) {
|
|
1442
|
+
return 'Normal';
|
|
1443
|
+
}
|
|
1444
|
+
return 'High';
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
return {
|
|
1448
|
+
id: `fhr-${index}`,
|
|
1449
|
+
date: new Date().toLocaleDateString(),
|
|
1450
|
+
time: data.time || 'N/A',
|
|
1451
|
+
hour: `${data.hour}${data.hour % 1 === 0.5 ? '.5' : ''}hr`,
|
|
1452
|
+
value: `${data.value} bpm`,
|
|
1453
|
+
status: getStatus(data.value),
|
|
1454
|
+
};
|
|
1455
|
+
});
|
|
1456
|
+
};
|
|
1457
|
+
|
|
1458
|
+
// Generate table data for cervical contractions
|
|
1459
|
+
const getCervicalContractionsTableData = () => {
|
|
1460
|
+
return cervicalContractionsData.map((encounter, index) => {
|
|
1461
|
+
// Find relevant obs for timeSlot, contractionCount, contractionLevel
|
|
1462
|
+
let timeSlot = '';
|
|
1463
|
+
let contractionCount = '';
|
|
1464
|
+
let contractionLevel = '';
|
|
1465
|
+
if (Array.isArray(encounter.obs)) {
|
|
1466
|
+
for (const obs of encounter.obs) {
|
|
1467
|
+
// TimeSlot is stored as a string value starting with 'Time:'
|
|
1468
|
+
if (
|
|
1469
|
+
obs.concept.uuid === '160632AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' &&
|
|
1470
|
+
typeof obs.value === 'string' &&
|
|
1471
|
+
obs.value.startsWith('Time:')
|
|
1472
|
+
) {
|
|
1473
|
+
const match = obs.value.match(/Time:\s*(.+)/);
|
|
1474
|
+
if (match) {
|
|
1475
|
+
timeSlot = match[1].trim();
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
// Contraction count concept
|
|
1479
|
+
if (obs.concept.uuid === '159682AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') {
|
|
1480
|
+
contractionCount = String(obs.value);
|
|
1481
|
+
}
|
|
1482
|
+
// Contraction level concepts (none, mild, moderate, strong)
|
|
1483
|
+
if (
|
|
1484
|
+
obs.concept.uuid === '1107AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' || // none
|
|
1485
|
+
obs.concept.uuid === '1498AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' || // mild
|
|
1486
|
+
obs.concept.uuid === '1499AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' || // moderate
|
|
1487
|
+
obs.concept.uuid === '166788AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' // strong
|
|
1488
|
+
) {
|
|
1489
|
+
// Map UUID to label
|
|
1490
|
+
if (obs.concept.uuid === '1107AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') {
|
|
1491
|
+
contractionLevel = 'none';
|
|
1492
|
+
}
|
|
1493
|
+
if (obs.concept.uuid === '1498AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') {
|
|
1494
|
+
contractionLevel = 'mild';
|
|
1495
|
+
}
|
|
1496
|
+
if (obs.concept.uuid === '1499AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') {
|
|
1497
|
+
contractionLevel = 'moderate';
|
|
1498
|
+
}
|
|
1499
|
+
if (obs.concept.uuid === '166788AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') {
|
|
1500
|
+
contractionLevel = 'strong';
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return {
|
|
1506
|
+
id: `cc-${index}`,
|
|
1507
|
+
date: new Date(encounter.encounterDatetime).toLocaleDateString(),
|
|
1508
|
+
timeSlot,
|
|
1509
|
+
contractionCount,
|
|
1510
|
+
contractionLevel,
|
|
1511
|
+
};
|
|
1512
|
+
});
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
// Generate table data for oxytocin (use backend data only)
|
|
1516
|
+
const getOxytocinTableData = () => {
|
|
1517
|
+
// Only map oxytocin-specific fields, do not include any event-description or urine test fields
|
|
1518
|
+
return loadedOxytocinData.map((data, index) => {
|
|
1519
|
+
// Defensive: Only use fields that are expected for oxytocin
|
|
1520
|
+
// If backend returns extra obs, ignore them
|
|
1521
|
+
return {
|
|
1522
|
+
id: `oxy-${index}`,
|
|
1523
|
+
date: data.encounterDatetime ? new Date(data.encounterDatetime).toLocaleDateString() : '',
|
|
1524
|
+
time: data.time || '',
|
|
1525
|
+
dropsPerMinute:
|
|
1526
|
+
data.dropsPerMinute !== null && data.dropsPerMinute !== undefined
|
|
1527
|
+
? `${data.dropsPerMinute} drops/min`
|
|
1528
|
+
: 'N/A',
|
|
1529
|
+
};
|
|
1530
|
+
});
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
// Generate table data for drugs and IV fluids
|
|
1534
|
+
const getDrugsIVFluidsTableData = () => {
|
|
1535
|
+
// Combine loaded drug orders with local manual entries
|
|
1536
|
+
const drugOrdersData = loadedDrugOrders.map((order) => ({
|
|
1537
|
+
id: order.id,
|
|
1538
|
+
date: order.date,
|
|
1539
|
+
drugName: order.drugName,
|
|
1540
|
+
dosage: order.dosage,
|
|
1541
|
+
route: order.route,
|
|
1542
|
+
frequency: order.frequency,
|
|
1543
|
+
source: 'order', // Mark as coming from drug orders
|
|
1544
|
+
}));
|
|
1545
|
+
|
|
1546
|
+
const manualEntriesData = localDrugsIVFluidsData.map((data, index) => ({
|
|
1547
|
+
id: `manual-${index}`,
|
|
1548
|
+
date: new Date().toLocaleDateString(),
|
|
1549
|
+
drugName: data.drugName,
|
|
1550
|
+
dosage: data.dosage,
|
|
1551
|
+
route: data.route || '',
|
|
1552
|
+
frequency: data.frequency || '',
|
|
1553
|
+
source: 'manual', // Mark as manual entry
|
|
1554
|
+
}));
|
|
1555
|
+
|
|
1556
|
+
const combinedData = [...drugOrdersData, ...manualEntriesData];
|
|
1557
|
+
|
|
1558
|
+
return combinedData;
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
// Generate table data for pulse and BP from backend
|
|
1562
|
+
const getPulseBPTableData = () => {
|
|
1563
|
+
// Map encounters to a combined row by encounterDatetime
|
|
1564
|
+
const bpMap = (loadedBPData || []).reduce((acc, encounter) => {
|
|
1565
|
+
const systolicObs = encounter.obs.find((obs) => obs.concept.uuid === PARTOGRAPHY_CONCEPTS['systolic-bp']);
|
|
1566
|
+
const diastolicObs = encounter.obs.find((obs) => obs.concept.uuid === PARTOGRAPHY_CONCEPTS['diastolic-bp']);
|
|
1567
|
+
if (systolicObs && diastolicObs) {
|
|
1568
|
+
acc[encounter.encounterDatetime] = {
|
|
1569
|
+
systolicBP: typeof systolicObs.value === 'number' ? systolicObs.value : parseFloat(systolicObs.value),
|
|
1570
|
+
diastolicBP: typeof diastolicObs.value === 'number' ? diastolicObs.value : parseFloat(diastolicObs.value),
|
|
1571
|
+
date: new Date(encounter.encounterDatetime).toLocaleDateString(),
|
|
1572
|
+
time: new Date(encounter.encounterDatetime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
return acc;
|
|
1576
|
+
}, {} as Record<string, any>);
|
|
1577
|
+
|
|
1578
|
+
// For each pulse, try to find matching BP by encounterDatetime
|
|
1579
|
+
return (loadedPulseData || []).map((encounter, index) => {
|
|
1580
|
+
const pulseObs = encounter.obs.find((obs) => obs.concept.uuid === PARTOGRAPHY_CONCEPTS['maternal-pulse']);
|
|
1581
|
+
const pulse = pulseObs
|
|
1582
|
+
? typeof pulseObs.value === 'number'
|
|
1583
|
+
? pulseObs.value
|
|
1584
|
+
: parseFloat(pulseObs.value)
|
|
1585
|
+
: null;
|
|
1586
|
+
const date = new Date(encounter.encounterDatetime).toLocaleDateString();
|
|
1587
|
+
const time = new Date(encounter.encounterDatetime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1588
|
+
const bp = bpMap[encounter.encounterDatetime] || {};
|
|
1589
|
+
return {
|
|
1590
|
+
id: `pulse-bp-${index}`,
|
|
1591
|
+
pulse,
|
|
1592
|
+
systolicBP: bp.systolicBP ?? '',
|
|
1593
|
+
diastolicBP: bp.diastolicBP ?? '',
|
|
1594
|
+
date,
|
|
1595
|
+
time,
|
|
1596
|
+
};
|
|
1597
|
+
});
|
|
1598
|
+
};
|
|
1599
|
+
|
|
1600
|
+
// Generate table data for temperature from backend only
|
|
1601
|
+
const getTemperatureTableData = () => {
|
|
1602
|
+
return loadedTemperatureData.map((encounter, index) => {
|
|
1603
|
+
const tempObs = encounter.obs.find((obs) => obs.concept.uuid === '5088AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
|
|
1604
|
+
const timeObs = encounter.obs.find(
|
|
1605
|
+
(obs) =>
|
|
1606
|
+
obs.concept.uuid === '160632AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' &&
|
|
1607
|
+
typeof obs.value === 'string' &&
|
|
1608
|
+
obs.value.startsWith('Time:'),
|
|
1609
|
+
);
|
|
1610
|
+
let time = '';
|
|
1611
|
+
if (timeObs && typeof timeObs.value === 'string') {
|
|
1612
|
+
const timeMatch = timeObs.value.match(/Time:\s*(.+)/);
|
|
1613
|
+
if (timeMatch) {
|
|
1614
|
+
time = timeMatch[1].trim();
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
let temperature = tempObs?.value ?? null;
|
|
1618
|
+
if (typeof temperature === 'string') {
|
|
1619
|
+
const parsed = parseFloat(temperature);
|
|
1620
|
+
temperature = isNaN(parsed) ? null : parsed;
|
|
1621
|
+
}
|
|
1622
|
+
return {
|
|
1623
|
+
id: `temperature-${index}`,
|
|
1624
|
+
date: new Date(encounter.encounterDatetime).toLocaleDateString(),
|
|
1625
|
+
timeSlot: '',
|
|
1626
|
+
exactTime: time,
|
|
1627
|
+
temperature,
|
|
1628
|
+
};
|
|
1629
|
+
});
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
const getUrineTestTableData = () => urineTestData;
|
|
1633
|
+
|
|
198
1634
|
const handleViewModeChange = (graphId: string, mode: 'graph' | 'table') => {
|
|
199
1635
|
setViewMode((prev) => ({
|
|
200
1636
|
...prev,
|
|
@@ -221,15 +1657,130 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
|
221
1657
|
};
|
|
222
1658
|
|
|
223
1659
|
const getTableData = (graph) => {
|
|
224
|
-
|
|
1660
|
+
// Special handling for cervix graph to use form data
|
|
1661
|
+
if (graph.id === 'cervix') {
|
|
1662
|
+
return loadedCervixData
|
|
1663
|
+
.filter((data) => {
|
|
1664
|
+
// Filter out any data with NaN values
|
|
1665
|
+
return (
|
|
1666
|
+
!isNaN(data.hour) &&
|
|
1667
|
+
!isNaN(data.cervicalDilation) &&
|
|
1668
|
+
!isNaN(data.descentOfHead) &&
|
|
1669
|
+
data.time &&
|
|
1670
|
+
data.time.trim() !== ''
|
|
1671
|
+
);
|
|
1672
|
+
})
|
|
1673
|
+
.map((data, index) => ({
|
|
1674
|
+
id: `cervix-${index}`,
|
|
1675
|
+
date: new Date(data.encounterDatetime).toLocaleDateString() || 'N/A',
|
|
1676
|
+
actualTime: new Date(data.encounterDatetime).toLocaleTimeString() || 'N/A',
|
|
1677
|
+
cervicalDilation: `${data.cervicalDilation} cm`,
|
|
1678
|
+
descentOfHead: `${data.descentOfHead}`,
|
|
1679
|
+
hourInput: `${data.hour} hr`,
|
|
1680
|
+
formTime: data.time || 'N/A',
|
|
1681
|
+
}));
|
|
1682
|
+
}
|
|
225
1683
|
|
|
226
|
-
|
|
227
|
-
|
|
1684
|
+
// Membrane amniotic fluid: use backend data only
|
|
1685
|
+
if (graph.id === 'membrane-amniotic-fluid') {
|
|
1686
|
+
return membraneAmnioticFluidEntries.map((data, index) => ({
|
|
1687
|
+
id: data.id || `maf-${index}`,
|
|
1688
|
+
date: data.date,
|
|
1689
|
+
timeSlot: data.timeSlot || '',
|
|
1690
|
+
exactTime: data.time || '',
|
|
1691
|
+
amnioticFluid: data.amnioticFluid,
|
|
1692
|
+
moulding: data.moulding,
|
|
1693
|
+
}));
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// Fallback for other graphs: use local state if available
|
|
1697
|
+
if (localPartographyData[graph.id] && localPartographyData[graph.id].length > 0) {
|
|
1698
|
+
return localPartographyData[graph.id].map((item, index) => ({
|
|
1699
|
+
id: `${graph.id}-${index}`,
|
|
1700
|
+
time: item.time || 'N/A',
|
|
1701
|
+
value: item.value || item.measurementValue || 'N/A',
|
|
1702
|
+
date: new Date(item.timestamp).toLocaleDateString() || 'N/A',
|
|
1703
|
+
...item, // Include any additional fields
|
|
1704
|
+
}));
|
|
228
1705
|
}
|
|
229
1706
|
|
|
230
|
-
|
|
1707
|
+
// Otherwise, return empty or dummy data
|
|
1708
|
+
return ENABLE_DUMMY_DATA ? generateDummyTableData(graph.id) : [];
|
|
1709
|
+
};
|
|
1710
|
+
|
|
1711
|
+
// Function to generate dummy table data
|
|
1712
|
+
const generateDummyTableData = (graphId: string) => {
|
|
1713
|
+
const baseEntries = [
|
|
1714
|
+
{ time: '08:00', date: new Date().toLocaleDateString() },
|
|
1715
|
+
{ time: '09:00', date: new Date().toLocaleDateString() },
|
|
1716
|
+
{ time: '10:00', date: new Date().toLocaleDateString() },
|
|
1717
|
+
{ time: '11:00', date: new Date().toLocaleDateString() },
|
|
1718
|
+
{ time: '12:00', date: new Date().toLocaleDateString() },
|
|
1719
|
+
];
|
|
231
1720
|
|
|
232
|
-
|
|
1721
|
+
switch (graphId) {
|
|
1722
|
+
case 'fetal-heart-rate':
|
|
1723
|
+
return baseEntries.map((entry, index) => ({
|
|
1724
|
+
id: `fhr-${index}`,
|
|
1725
|
+
date: entry.date,
|
|
1726
|
+
time: entry.time,
|
|
1727
|
+
value: `${140 + Math.floor(Math.random() * 20)} bpm`,
|
|
1728
|
+
}));
|
|
1729
|
+
|
|
1730
|
+
case 'maternal-pulse':
|
|
1731
|
+
return baseEntries.map((entry, index) => ({
|
|
1732
|
+
id: `pulse-${index}`,
|
|
1733
|
+
date: entry.date,
|
|
1734
|
+
time: entry.time,
|
|
1735
|
+
value: `${75 + Math.floor(Math.random() * 15)} bpm`,
|
|
1736
|
+
}));
|
|
1737
|
+
|
|
1738
|
+
case 'blood-pressure':
|
|
1739
|
+
return baseEntries.map((entry, index) => ({
|
|
1740
|
+
id: `bp-${index}`,
|
|
1741
|
+
date: entry.date,
|
|
1742
|
+
time: entry.time,
|
|
1743
|
+
systolic: `${115 + Math.floor(Math.random() * 10)} mmHg`,
|
|
1744
|
+
diastolic: `${75 + Math.floor(Math.random() * 5)} mmHg`,
|
|
1745
|
+
}));
|
|
1746
|
+
|
|
1747
|
+
case 'temperature':
|
|
1748
|
+
return baseEntries.map((entry, index) => ({
|
|
1749
|
+
id: `temp-${index}`,
|
|
1750
|
+
date: entry.date,
|
|
1751
|
+
time: entry.time,
|
|
1752
|
+
value: `${(36.5 + Math.random() * 0.8).toFixed(1)} °C`,
|
|
1753
|
+
}));
|
|
1754
|
+
|
|
1755
|
+
case 'uterine-contractions':
|
|
1756
|
+
return baseEntries.map((entry, index) => ({
|
|
1757
|
+
id: `contractions-${index}`,
|
|
1758
|
+
date: entry.date,
|
|
1759
|
+
time: entry.time,
|
|
1760
|
+
value: `${Math.floor(Math.random() * 4) + 2} per 10 min`,
|
|
1761
|
+
duration: `${Math.floor(Math.random() * 20) + 30} sec`,
|
|
1762
|
+
}));
|
|
1763
|
+
|
|
1764
|
+
default:
|
|
1765
|
+
return [];
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
const getTableHeaders = (graph) => {
|
|
1770
|
+
// Special headers for cervix graph
|
|
1771
|
+
if (graph.id === 'cervix') {
|
|
1772
|
+
return [
|
|
1773
|
+
{ key: 'date', header: t('date', 'Date') },
|
|
1774
|
+
{ key: 'actualTime', header: t('actualTime', 'Actual Time') },
|
|
1775
|
+
{ key: 'cervicalDilation', header: t('cervicalDilation', 'Cervical Dilation') },
|
|
1776
|
+
{ key: 'descentOfHead', header: t('descentOfHead', 'Descent of Head') },
|
|
1777
|
+
{ key: 'hourInput', header: t('hourInput', 'Hour Input') },
|
|
1778
|
+
{ key: 'formTime', header: t('formTime', 'Form Time') },
|
|
1779
|
+
];
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// Default headers for other graphs
|
|
1783
|
+
return getPartographyTableHeaders(t);
|
|
233
1784
|
};
|
|
234
1785
|
|
|
235
1786
|
const getValueStatus = (value: number, graph) => {
|
|
@@ -258,8 +1809,8 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
|
258
1809
|
return 'normal';
|
|
259
1810
|
};
|
|
260
1811
|
|
|
261
|
-
const renderGraph = (graph) => {
|
|
262
|
-
const
|
|
1812
|
+
const renderGraph = (graph: GraphDefinition, index: number, totalGraphs: number) => {
|
|
1813
|
+
const patientChartData: ChartDataPoint[] = graphData[graph.id] || [];
|
|
263
1814
|
const currentViewMode = viewMode[graph.id] || 'graph';
|
|
264
1815
|
const tableData = getTableData(graph);
|
|
265
1816
|
const currentPageNum = currentPage[graph.id] || 1;
|
|
@@ -271,44 +1822,171 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
|
271
1822
|
const endIndex = startIndex + currentPageSize;
|
|
272
1823
|
const paginatedData = tableData.slice(startIndex, endIndex);
|
|
273
1824
|
|
|
274
|
-
|
|
1825
|
+
// Default chart data - declare once here
|
|
1826
|
+
let finalChartData: ChartDataPoint[] = patientChartData;
|
|
1827
|
+
let zeroTime: Date | undefined;
|
|
1828
|
+
let maxChartTime: Date | undefined;
|
|
1829
|
+
|
|
1830
|
+
// --- Data for custom time labels ---
|
|
1831
|
+
let timeLabelsData: { hours: string; time: string; span: number }[] = [];
|
|
1832
|
+
|
|
1833
|
+
// Default chart options with medical partograph styling
|
|
1834
|
+
let chartOptions: any = {
|
|
275
1835
|
title: graph.title,
|
|
276
1836
|
axes: {
|
|
277
1837
|
bottom: {
|
|
278
1838
|
title: t('time', 'Time'),
|
|
279
1839
|
mapsTo: 'time',
|
|
280
1840
|
scaleType: ScaleTypes.LABELS,
|
|
1841
|
+
grid: {
|
|
1842
|
+
enabled: true,
|
|
1843
|
+
strokeWidth: 1,
|
|
1844
|
+
strokeDasharray: '1,1',
|
|
1845
|
+
},
|
|
281
1846
|
},
|
|
282
1847
|
left: {
|
|
283
1848
|
title: graph.yAxisLabel,
|
|
284
1849
|
mapsTo: 'value',
|
|
285
1850
|
scaleType: ScaleTypes.LINEAR,
|
|
286
1851
|
domain: [graph.yMin, graph.yMax],
|
|
1852
|
+
grid: {
|
|
1853
|
+
enabled: true,
|
|
1854
|
+
strokeWidth: 1,
|
|
1855
|
+
strokeDasharray: '1,1',
|
|
1856
|
+
},
|
|
287
1857
|
},
|
|
288
1858
|
},
|
|
289
|
-
curve: '
|
|
1859
|
+
curve: 'curveLinear',
|
|
290
1860
|
height: '500px',
|
|
291
1861
|
color: {
|
|
292
1862
|
scale: {
|
|
293
|
-
[
|
|
1863
|
+
[patientChartData[0]?.group || graph.id]: getColorForGraph(graph.color),
|
|
294
1864
|
Systolic: '#ff6b6b',
|
|
295
1865
|
Diastolic: '#4ecdc4',
|
|
296
1866
|
},
|
|
297
1867
|
},
|
|
298
1868
|
points: {
|
|
299
1869
|
enabled: true,
|
|
300
|
-
radius:
|
|
1870
|
+
radius: 5,
|
|
1871
|
+
strokeWidth: 2,
|
|
1872
|
+
fill: true,
|
|
301
1873
|
},
|
|
302
1874
|
grid: {
|
|
303
1875
|
x: {
|
|
304
1876
|
enabled: true,
|
|
1877
|
+
strokeWidth: 1,
|
|
1878
|
+
strokeDasharray: '1,1',
|
|
305
1879
|
},
|
|
306
1880
|
y: {
|
|
307
1881
|
enabled: true,
|
|
1882
|
+
strokeWidth: 1,
|
|
1883
|
+
strokeDasharray: '1,1',
|
|
308
1884
|
},
|
|
309
1885
|
},
|
|
1886
|
+
legend: {
|
|
1887
|
+
position: 'bottom',
|
|
1888
|
+
clickable: false,
|
|
1889
|
+
},
|
|
1890
|
+
theme: 'white',
|
|
1891
|
+
toolbar: {
|
|
1892
|
+
enabled: false,
|
|
1893
|
+
},
|
|
310
1894
|
};
|
|
311
1895
|
|
|
1896
|
+
// Hide X-axis title and labels for non-cervix graphs only
|
|
1897
|
+
if (graph.id !== 'cervix') {
|
|
1898
|
+
chartOptions.axes.bottom.title = undefined;
|
|
1899
|
+
chartOptions.axes.bottom.tick = {
|
|
1900
|
+
formatter: () => '', // Hide tick labels for non-cervix graphs
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// --- START LOGIC FOR CERVIX GRAPH ---
|
|
1905
|
+
if (graph.id === 'cervix') {
|
|
1906
|
+
// 1. Apply custom axis and styling options
|
|
1907
|
+
chartOptions = {
|
|
1908
|
+
...chartOptions,
|
|
1909
|
+
...CERVIX_CHART_OPTIONS,
|
|
1910
|
+
title: graph.title,
|
|
1911
|
+
color: {
|
|
1912
|
+
scale: {
|
|
1913
|
+
'Alert Line': '#FFD700', // Yellow for alert line
|
|
1914
|
+
'Action Line': '#FF0000', // Red for action line
|
|
1915
|
+
'Cervical Dilation': '#22C55E', // Green for cervical dilation
|
|
1916
|
+
'Descent of Head': '#2563EB', // Blue for descent of head
|
|
1917
|
+
[graph.id]: getColorForGraph(graph.color),
|
|
1918
|
+
},
|
|
1919
|
+
},
|
|
1920
|
+
legend: {
|
|
1921
|
+
position: 'top', // Move legend to the top for Cervix graph
|
|
1922
|
+
},
|
|
1923
|
+
};
|
|
1924
|
+
|
|
1925
|
+
// 2. Calculate Alert and Action Lines Data Points (using hour scale)
|
|
1926
|
+
const ALERT_START_CM = 4; // Changed from 5cm to 4cm as requested
|
|
1927
|
+
const CERVIX_DILATION_MAX = 10;
|
|
1928
|
+
const ALERT_ACTION_DIFFERENCE_HOURS = 4; // 4 hours difference
|
|
1929
|
+
const EXPECTED_LABOR_DURATION_HOURS = 6; // 6 hours progression from 4cm to 10cm
|
|
1930
|
+
|
|
1931
|
+
const staticLinesData: ChartDataPoint[] = [
|
|
1932
|
+
// Alert Line Points: (Hour 0, 4cm) -> (Hour 6, 10cm)
|
|
1933
|
+
{ hour: 0, value: ALERT_START_CM, group: 'Alert Line' },
|
|
1934
|
+
{ hour: EXPECTED_LABOR_DURATION_HOURS, value: CERVIX_DILATION_MAX, group: 'Alert Line' },
|
|
1935
|
+
|
|
1936
|
+
// Action Line Points: (Hour 4, 4cm) -> (Hour 10, 10cm)
|
|
1937
|
+
{ hour: ALERT_ACTION_DIFFERENCE_HOURS, value: ALERT_START_CM, group: 'Action Line' },
|
|
1938
|
+
{
|
|
1939
|
+
hour: ALERT_ACTION_DIFFERENCE_HOURS + EXPECTED_LABOR_DURATION_HOURS,
|
|
1940
|
+
value: CERVIX_DILATION_MAX,
|
|
1941
|
+
group: 'Action Line',
|
|
1942
|
+
},
|
|
1943
|
+
];
|
|
1944
|
+
|
|
1945
|
+
// 3. Add Cervical Dilation data points from form inputs
|
|
1946
|
+
const cervicalDilationData: ChartDataPoint[] = cervixFormData.map((data) => ({
|
|
1947
|
+
hour: data.hour,
|
|
1948
|
+
value: data.cervicalDilation,
|
|
1949
|
+
group: 'Cervical Dilation',
|
|
1950
|
+
time: data.time,
|
|
1951
|
+
}));
|
|
1952
|
+
|
|
1953
|
+
// 4. Add Descent of Head data points from form inputs
|
|
1954
|
+
// Medical reality: 5=high position → 4 → 3 → 2 → 1=most descended (head coming down)
|
|
1955
|
+
// Chart display: Show medical values directly (5 at top, 1 at bottom)
|
|
1956
|
+
const descentOfHeadData: ChartDataPoint[] = cervixFormData.map((data) => ({
|
|
1957
|
+
hour: data.hour,
|
|
1958
|
+
value: data.descentOfHead, // Direct mapping: medical 5→chart 5 (high), medical 1→chart 1 (low)
|
|
1959
|
+
group: 'Descent of Head',
|
|
1960
|
+
time: data.time,
|
|
1961
|
+
}));
|
|
1962
|
+
|
|
1963
|
+
// 5. Combine all data - Alert/Action lines + patient data + form data
|
|
1964
|
+
finalChartData = [...patientChartData, ...staticLinesData, ...cervicalDilationData, ...descentOfHeadData];
|
|
1965
|
+
|
|
1966
|
+
// 6. Generate dynamic time labels for the custom footer (from form data)
|
|
1967
|
+
timeLabelsData = [];
|
|
1968
|
+
|
|
1969
|
+
// Create time labels based on form submissions or default to 10 columns if no data
|
|
1970
|
+
const maxHours = Math.max(10, Math.max(...cervixFormData.map((d) => d.hour), 0) + 1);
|
|
1971
|
+
|
|
1972
|
+
for (let i = 0; i < Math.min(maxHours, 10); i++) {
|
|
1973
|
+
const hourLabel = i === 0 ? '0' : `${i}hr`;
|
|
1974
|
+
|
|
1975
|
+
// Find corresponding form data for this hour
|
|
1976
|
+
const formDataForHour = cervixFormData.find((data) => data.hour === i);
|
|
1977
|
+
const timeValue = formDataForHour ? formDataForHour.time : '--:--';
|
|
1978
|
+
|
|
1979
|
+
timeLabelsData.push({
|
|
1980
|
+
hours: hourLabel,
|
|
1981
|
+
time: timeValue,
|
|
1982
|
+
span: 1,
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
// --- END LOGIC FOR CERVIX GRAPH ---
|
|
1987
|
+
|
|
1988
|
+
const shouldRenderChart = finalChartData.length > 0;
|
|
1989
|
+
|
|
312
1990
|
return (
|
|
313
1991
|
<div className={styles.graphContainer} key={graph.id}>
|
|
314
1992
|
<div className={styles.graphHeader}>
|
|
@@ -346,38 +2024,111 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
|
346
2024
|
|
|
347
2025
|
{currentViewMode === 'graph' ? (
|
|
348
2026
|
<>
|
|
349
|
-
<div className={styles.chartContainer}>
|
|
2027
|
+
<div className={styles.chartContainer} data-chart-id={graph.id}>
|
|
350
2028
|
{isGraphLoading ? (
|
|
351
|
-
<
|
|
352
|
-
<p>{t('loadingData', 'Loading data...')}</p>
|
|
353
|
-
</div>
|
|
354
|
-
) : chartData.length > 0 ? (
|
|
355
|
-
<LineChart data={chartData} options={chartOptions} />
|
|
2029
|
+
<GraphSkeleton />
|
|
356
2030
|
) : (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
2031
|
+
<>
|
|
2032
|
+
{shouldRenderChart ? (
|
|
2033
|
+
<div className={graph.id === 'cervix' ? 'cervix-chart-wrapper' : ''}>
|
|
2034
|
+
<LineChart data={finalChartData} options={chartOptions} />
|
|
2035
|
+
</div>
|
|
2036
|
+
) : (
|
|
2037
|
+
<LineChart
|
|
2038
|
+
data={[{ group: graph.title, time: t('noData', 'No Data'), value: graph.yMin }]}
|
|
2039
|
+
options={{
|
|
2040
|
+
...chartOptions,
|
|
2041
|
+
axes: {
|
|
2042
|
+
...chartOptions.axes,
|
|
2043
|
+
bottom: {
|
|
2044
|
+
...chartOptions.axes.bottom,
|
|
2045
|
+
mapsTo: 'time',
|
|
2046
|
+
title: undefined,
|
|
2047
|
+
tick: {
|
|
2048
|
+
formatter: () => '',
|
|
2049
|
+
},
|
|
2050
|
+
},
|
|
2051
|
+
},
|
|
2052
|
+
legend: {
|
|
2053
|
+
enabled: false,
|
|
2054
|
+
},
|
|
2055
|
+
points: {
|
|
2056
|
+
enabled: false,
|
|
2057
|
+
},
|
|
2058
|
+
color: {
|
|
2059
|
+
scale: {
|
|
2060
|
+
[graph.title]: '#d0d0d0',
|
|
2061
|
+
},
|
|
2062
|
+
},
|
|
2063
|
+
}}
|
|
2064
|
+
/>
|
|
2065
|
+
)}
|
|
2066
|
+
</>
|
|
367
2067
|
)}
|
|
368
2068
|
</div>
|
|
369
|
-
{
|
|
2069
|
+
{/* --- Custom Time Labels Display for Cervix Graph only --- */}
|
|
2070
|
+
{graph.id === 'cervix' && timeLabelsData.length > 0 && (
|
|
2071
|
+
<div
|
|
2072
|
+
className={styles.customTimeLabelsContainer}
|
|
2073
|
+
style={{ '--visible-columns': Math.min(10, timeLabelsData.length) } as React.CSSProperties}>
|
|
2074
|
+
{/* Hours Row */}
|
|
2075
|
+
<div className={styles.customTimeLabelsRow}>
|
|
2076
|
+
<div className={styles.customTimeLabelHeader}>Hours</div>
|
|
2077
|
+
{timeLabelsData.map((data, index) => (
|
|
2078
|
+
<div
|
|
2079
|
+
key={`hours-${index}`}
|
|
2080
|
+
className={styles.customTimeLabelCell}
|
|
2081
|
+
style={{
|
|
2082
|
+
gridColumnEnd: `span ${data.span}`,
|
|
2083
|
+
backgroundColor: '#f4f4f4',
|
|
2084
|
+
fontWeight: 700,
|
|
2085
|
+
}}>
|
|
2086
|
+
{data.hours}
|
|
2087
|
+
</div>
|
|
2088
|
+
))}
|
|
2089
|
+
</div>
|
|
2090
|
+
{/* Time Row */}
|
|
2091
|
+
<div className={styles.customTimeLabelsRow}>
|
|
2092
|
+
<div className={styles.customTimeLabelHeader}>Time</div>
|
|
2093
|
+
{timeLabelsData.map((data, index) => (
|
|
2094
|
+
<div
|
|
2095
|
+
key={`time-${index}`}
|
|
2096
|
+
className={styles.customTimeLabelCell}
|
|
2097
|
+
style={{ gridColumnEnd: `span ${data.span}` }}>
|
|
2098
|
+
{data.time}
|
|
2099
|
+
</div>
|
|
2100
|
+
))}
|
|
2101
|
+
</div>
|
|
2102
|
+
{/* Scroll indicator if content overflows */}
|
|
2103
|
+
{timeLabelsData.length > 10 && (
|
|
2104
|
+
<div
|
|
2105
|
+
style={{
|
|
2106
|
+
fontSize: '12px',
|
|
2107
|
+
color: '#666',
|
|
2108
|
+
textAlign: 'center',
|
|
2109
|
+
padding: '4px',
|
|
2110
|
+
backgroundColor: '#f0f0f0',
|
|
2111
|
+
borderTop: '1px solid #ddd',
|
|
2112
|
+
}}>
|
|
2113
|
+
← Scroll horizontally to view all {timeLabelsData.length} hours →
|
|
2114
|
+
</div>
|
|
2115
|
+
)}
|
|
2116
|
+
</div>
|
|
2117
|
+
)}
|
|
2118
|
+
{/* --- END Custom Time Labels --- */}
|
|
2119
|
+
|
|
2120
|
+
{patientChartData.length > 0 && !isGraphLoading && (
|
|
370
2121
|
<div className={styles.chartStats}>
|
|
371
2122
|
<div className={styles.statItem}>
|
|
372
2123
|
<span className={styles.statLabel}>{t('latest', 'Latest')}:</span>
|
|
373
2124
|
<span className={styles.statValue}>
|
|
374
|
-
{
|
|
2125
|
+
{patientChartData[patientChartData.length - 1]?.value?.toFixed(1)} {graph.yAxisLabel}
|
|
375
2126
|
</span>
|
|
376
2127
|
</div>
|
|
377
2128
|
<div className={styles.statItem}>
|
|
378
2129
|
<span className={styles.statLabel}>{t('average', 'Average')}:</span>
|
|
379
2130
|
<span className={styles.statValue}>
|
|
380
|
-
{(
|
|
2131
|
+
{(patientChartData.reduce((sum, item) => sum + item.value, 0) / patientChartData.length).toFixed(1)}{' '}
|
|
381
2132
|
{graph.yAxisLabel}
|
|
382
2133
|
</span>
|
|
383
2134
|
</div>
|
|
@@ -387,12 +2138,10 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
|
387
2138
|
) : (
|
|
388
2139
|
<div className={styles.tableContainer}>
|
|
389
2140
|
{isGraphLoading ? (
|
|
390
|
-
<
|
|
391
|
-
<p>{t('loadingData', 'Loading data...')}</p>
|
|
392
|
-
</div>
|
|
2141
|
+
<TableSkeleton />
|
|
393
2142
|
) : paginatedData.length > 0 ? (
|
|
394
2143
|
<>
|
|
395
|
-
<DataTable rows={paginatedData} headers={
|
|
2144
|
+
<DataTable rows={paginatedData} headers={getTableHeaders(graph)}>
|
|
396
2145
|
{({ rows, headers, getTableProps, getHeaderProps, getRowProps }) => (
|
|
397
2146
|
<TableContainer title="" description="">
|
|
398
2147
|
<Table {...getTableProps()} size="sm">
|
|
@@ -411,7 +2160,12 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
|
411
2160
|
{row.cells.map((cell) => {
|
|
412
2161
|
let cellContent = cell.value;
|
|
413
2162
|
|
|
414
|
-
|
|
2163
|
+
// Only apply value status logic for non-cervix graphs
|
|
2164
|
+
if (
|
|
2165
|
+
graph.id !== 'cervix' &&
|
|
2166
|
+
cell.info.header === 'value' &&
|
|
2167
|
+
row.cells.find((c) => c.info.header === 'value')
|
|
2168
|
+
) {
|
|
415
2169
|
const cellValue = cell.value;
|
|
416
2170
|
const status = getValueStatus(parseFloat(cellValue), graph);
|
|
417
2171
|
const statusClass =
|
|
@@ -480,18 +2234,186 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
|
480
2234
|
<Layer>
|
|
481
2235
|
<Grid>
|
|
482
2236
|
<Column lg={16} md={8} sm={4}>
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
2237
|
+
{/* Fetal Heart Rate Graph - Standalone */}
|
|
2238
|
+
<FetalHeartRateGraph
|
|
2239
|
+
data={computedFetalHeartRateData}
|
|
2240
|
+
tableData={getFetalHeartRateTableData()}
|
|
2241
|
+
viewMode={fetalHeartRateViewMode}
|
|
2242
|
+
currentPage={fetalHeartRateCurrentPage}
|
|
2243
|
+
pageSize={fetalHeartRatePageSize}
|
|
2244
|
+
totalItems={getFetalHeartRateTableData().length}
|
|
2245
|
+
controlSize={controlSize}
|
|
2246
|
+
onAddData={() => setIsFetalHeartRateFormOpen(true)}
|
|
2247
|
+
onViewModeChange={setFetalHeartRateViewMode}
|
|
2248
|
+
onPageChange={setFetalHeartRateCurrentPage}
|
|
2249
|
+
onPageSizeChange={setFetalHeartRatePageSize}
|
|
2250
|
+
isAddButtonDisabled={false}
|
|
2251
|
+
/>
|
|
2252
|
+
|
|
2253
|
+
{/* Membrane Amniotic Fluid Graph - Standalone */}
|
|
2254
|
+
<MembraneAmnioticFluidGraph
|
|
2255
|
+
data={membraneAmnioticFluidEntries}
|
|
2256
|
+
tableData={membraneAmnioticFluidEntries}
|
|
2257
|
+
viewMode={membraneAmnioticFluidViewMode}
|
|
2258
|
+
currentPage={membraneAmnioticFluidCurrentPage}
|
|
2259
|
+
pageSize={membraneAmnioticFluidPageSize}
|
|
2260
|
+
totalItems={membraneAmnioticFluidEntries.length}
|
|
2261
|
+
controlSize={controlSize}
|
|
2262
|
+
onAddData={() => setIsMembraneAmnioticFluidFormOpen(true)}
|
|
2263
|
+
onViewModeChange={setMembraneAmnioticFluidViewMode}
|
|
2264
|
+
onPageChange={setMembraneAmnioticFluidCurrentPage}
|
|
2265
|
+
onPageSizeChange={setMembraneAmnioticFluidPageSize}
|
|
2266
|
+
isAddButtonDisabled={false}
|
|
2267
|
+
/>
|
|
2268
|
+
|
|
2269
|
+
{/* Existing Partography Graphs - Contains Cervix Graph */}
|
|
2270
|
+
<div className={styles.partographyGrid}>
|
|
2271
|
+
{partographGraphs.map((graph, index) => renderGraph(graph, index, partographGraphs.length))}
|
|
2272
|
+
</div>
|
|
2273
|
+
|
|
2274
|
+
{/* Cervical Contractions Graph - Positioned below Cervix graph */}
|
|
2275
|
+
<CervicalContractionsGraph
|
|
2276
|
+
data={transformEncounterToTableData(cervicalContractionsData || [], 'uterine-contractions').map(
|
|
2277
|
+
(row, index) => ({
|
|
2278
|
+
id: row.id || `cc-${index}`,
|
|
2279
|
+
date: row.dateTime?.split(' — ')[0] || '',
|
|
2280
|
+
timeSlot: row.timeSlot || '',
|
|
2281
|
+
contractionCount: row.contractionCount || '',
|
|
2282
|
+
contractionLevel: row.contractionLevel || 'none',
|
|
2283
|
+
}),
|
|
2284
|
+
)}
|
|
2285
|
+
tableData={transformEncounterToTableData(cervicalContractionsData || [], 'uterine-contractions')}
|
|
2286
|
+
viewMode={cervicalContractionsViewMode}
|
|
2287
|
+
currentPage={cervicalContractionsCurrentPage}
|
|
2288
|
+
pageSize={cervicalContractionsPageSize}
|
|
2289
|
+
totalItems={transformEncounterToTableData(cervicalContractionsData || [], 'uterine-contractions').length}
|
|
2290
|
+
controlSize={controlSize}
|
|
2291
|
+
onAddData={() => setIsCervicalContractionsFormOpen(true)}
|
|
2292
|
+
onViewModeChange={setCervicalContractionsViewMode}
|
|
2293
|
+
onPageChange={setCervicalContractionsCurrentPage}
|
|
2294
|
+
onPageSizeChange={setCervicalContractionsPageSize}
|
|
2295
|
+
isAddButtonDisabled={false}
|
|
2296
|
+
patient={{
|
|
2297
|
+
uuid: patientUuid,
|
|
2298
|
+
name: 'Patient Name',
|
|
2299
|
+
gender: 'F',
|
|
2300
|
+
age: '28',
|
|
2301
|
+
}}
|
|
2302
|
+
/>
|
|
2303
|
+
|
|
2304
|
+
{/* Oxytocin Graph - Positioned below Cervical Contractions */}
|
|
2305
|
+
<OxytocinGraph
|
|
2306
|
+
data={loadedOxytocinData.map((item) => ({
|
|
2307
|
+
timeSlot: item.time ?? '',
|
|
2308
|
+
oxytocinUsed: typeof item.dropsPerMinute === 'number' && item.dropsPerMinute > 0 ? 'yes' : 'no',
|
|
2309
|
+
dropsPerMinute: typeof item.dropsPerMinute === 'number' ? item.dropsPerMinute : 0,
|
|
2310
|
+
date: item.encounterDatetime ? new Date(item.encounterDatetime).toLocaleDateString() : '',
|
|
2311
|
+
id: item.uuid || undefined,
|
|
2312
|
+
}))}
|
|
2313
|
+
tableData={loadedOxytocinData.map((item, index) => ({
|
|
2314
|
+
id: item.uuid || `oxy-${index}`,
|
|
2315
|
+
date: item.encounterDatetime ? new Date(item.encounterDatetime).toLocaleDateString() : '',
|
|
2316
|
+
timeSlot: item.time ?? '',
|
|
2317
|
+
oxytocinUsed: typeof item.dropsPerMinute === 'number' && item.dropsPerMinute > 0 ? 'yes' : 'no',
|
|
2318
|
+
dropsPerMinute: typeof item.dropsPerMinute === 'number' ? `${item.dropsPerMinute} drops/min` : 'N/A',
|
|
2319
|
+
}))}
|
|
2320
|
+
viewMode={oxytocinViewMode}
|
|
2321
|
+
currentPage={oxytocinCurrentPage}
|
|
2322
|
+
pageSize={oxytocinPageSize}
|
|
2323
|
+
totalItems={loadedOxytocinData.length}
|
|
2324
|
+
controlSize={controlSize}
|
|
2325
|
+
onAddData={() => setIsOxytocinFormOpen(true)}
|
|
2326
|
+
onViewModeChange={setOxytocinViewMode}
|
|
2327
|
+
onPageChange={setOxytocinCurrentPage}
|
|
2328
|
+
onPageSizeChange={setOxytocinPageSize}
|
|
2329
|
+
isAddButtonDisabled={false}
|
|
2330
|
+
/>
|
|
2331
|
+
|
|
2332
|
+
{/* Drugs and IV Fluids Graph - Positioned below Oxytocin */}
|
|
2333
|
+
<DrugsIVFluidsGraph
|
|
2334
|
+
data={getDrugsIVFluidsTableData()}
|
|
2335
|
+
tableData={getDrugsIVFluidsTableData()}
|
|
2336
|
+
viewMode={drugsIVFluidsViewMode}
|
|
2337
|
+
currentPage={drugsIVFluidsCurrentPage}
|
|
2338
|
+
pageSize={drugsIVFluidsPageSize}
|
|
2339
|
+
totalItems={getDrugsIVFluidsTableData().length}
|
|
2340
|
+
controlSize={controlSize}
|
|
2341
|
+
onAddData={() => {}} // Form handling is done by the wrapper component
|
|
2342
|
+
onViewModeChange={setDrugsIVFluidsViewMode}
|
|
2343
|
+
onPageChange={setDrugsIVFluidsCurrentPage}
|
|
2344
|
+
onPageSizeChange={setDrugsIVFluidsPageSize}
|
|
2345
|
+
isAddButtonDisabled={false}
|
|
2346
|
+
patient={{
|
|
2347
|
+
uuid: patientUuid,
|
|
2348
|
+
name: 'Patient Name',
|
|
2349
|
+
gender: 'F',
|
|
2350
|
+
age: '28',
|
|
2351
|
+
}}
|
|
2352
|
+
onDrugsIVFluidsSubmit={handleDrugsIVFluidsFormSubmit}
|
|
2353
|
+
onDataSaved={handleDrugOrderDataSaved}
|
|
2354
|
+
/>
|
|
2355
|
+
|
|
2356
|
+
{/* Pulse and BP Graph - Positioned below Drugs and IV Fluids */}
|
|
2357
|
+
<PulseBPGraph
|
|
2358
|
+
data={getPulseBPTableData()}
|
|
2359
|
+
tableData={getPulseBPTableData()}
|
|
2360
|
+
viewMode={pulseBPViewMode}
|
|
2361
|
+
currentPage={pulseBPCurrentPage}
|
|
2362
|
+
pageSize={pulseBPPageSize}
|
|
2363
|
+
totalItems={getPulseBPTableData().length}
|
|
2364
|
+
controlSize={controlSize}
|
|
2365
|
+
onAddData={() => {}}
|
|
2366
|
+
onViewModeChange={setPulseBPViewMode}
|
|
2367
|
+
onPageChange={setPulseBPCurrentPage}
|
|
2368
|
+
onPageSizeChange={setPulseBPPageSize}
|
|
2369
|
+
isAddButtonDisabled={false}
|
|
2370
|
+
patient={{
|
|
2371
|
+
uuid: patientUuid,
|
|
2372
|
+
name: 'Patient Name',
|
|
2373
|
+
gender: 'F',
|
|
2374
|
+
age: '28',
|
|
2375
|
+
}}
|
|
2376
|
+
onPulseBPSubmit={handlePulseBPFormSubmit}
|
|
2377
|
+
/>
|
|
2378
|
+
|
|
2379
|
+
{/* Temperature Graph - Positioned below Pulse and BP */}
|
|
2380
|
+
<TemperatureGraph
|
|
2381
|
+
data={getTemperatureTableData()}
|
|
2382
|
+
tableData={getTemperatureTableData()}
|
|
2383
|
+
viewMode={temperatureViewMode}
|
|
2384
|
+
currentPage={temperatureCurrentPage}
|
|
2385
|
+
pageSize={temperaturePageSize}
|
|
2386
|
+
totalItems={getTemperatureTableData().length}
|
|
2387
|
+
controlSize={controlSize}
|
|
2388
|
+
onAddData={() => {
|
|
2389
|
+
const now = new Date();
|
|
2390
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
2391
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
2392
|
+
setTemperatureFormInitialTime(`${hh}:${mm}`);
|
|
2393
|
+
setIsTemperatureFormOpen(true);
|
|
2394
|
+
}}
|
|
2395
|
+
onViewModeChange={setTemperatureViewMode}
|
|
2396
|
+
onPageChange={setTemperatureCurrentPage}
|
|
2397
|
+
onPageSizeChange={setTemperaturePageSize}
|
|
2398
|
+
isAddButtonDisabled={false}
|
|
2399
|
+
/>
|
|
2400
|
+
|
|
2401
|
+
{/* Urine Test Graph - Now using backend data */}
|
|
2402
|
+
<UrineTestGraph
|
|
2403
|
+
data={urineTestData}
|
|
2404
|
+
tableData={urineTestData}
|
|
2405
|
+
viewMode={urineTestViewMode}
|
|
2406
|
+
currentPage={urineTestCurrentPage}
|
|
2407
|
+
pageSize={urineTestPageSize}
|
|
2408
|
+
totalItems={urineTestData.length}
|
|
2409
|
+
controlSize={controlSize}
|
|
2410
|
+
onAddData={() => setIsUrineTestFormOpen(true)}
|
|
2411
|
+
onViewModeChange={setUrineTestViewMode}
|
|
2412
|
+
onPageChange={setUrineTestCurrentPage}
|
|
2413
|
+
onPageSizeChange={setUrineTestPageSize}
|
|
2414
|
+
isAddButtonDisabled={false}
|
|
2415
|
+
/>
|
|
2416
|
+
|
|
495
2417
|
{isFormOpen && (
|
|
496
2418
|
<PartographyDataForm
|
|
497
2419
|
isOpen={isFormOpen}
|
|
@@ -501,6 +2423,107 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
|
501
2423
|
graphTitle={partographGraphs.find((g) => g.id === selectedGraphType)?.title || ''}
|
|
502
2424
|
/>
|
|
503
2425
|
)}
|
|
2426
|
+
{isCervixFormOpen && (
|
|
2427
|
+
<CervixForm
|
|
2428
|
+
isOpen={isCervixFormOpen}
|
|
2429
|
+
onClose={handleCervixFormClose}
|
|
2430
|
+
onSubmit={handleCervixFormSubmit}
|
|
2431
|
+
onDataSaved={handleCervixDataSaved}
|
|
2432
|
+
selectedHours={selectedHours}
|
|
2433
|
+
existingTimeEntries={computedExistingTimeEntries}
|
|
2434
|
+
existingCervixData={existingCervixData}
|
|
2435
|
+
patient={{
|
|
2436
|
+
uuid: patientUuid,
|
|
2437
|
+
name: 'Patient',
|
|
2438
|
+
gender: 'Female',
|
|
2439
|
+
age: '40 Years',
|
|
2440
|
+
}}
|
|
2441
|
+
/>
|
|
2442
|
+
)}
|
|
2443
|
+
{isFetalHeartRateFormOpen && (
|
|
2444
|
+
<FetalHeartRateForm
|
|
2445
|
+
isOpen={isFetalHeartRateFormOpen}
|
|
2446
|
+
onClose={handleFetalHeartRateFormClose}
|
|
2447
|
+
onSubmit={handleFetalHeartRateFormSubmit}
|
|
2448
|
+
onDataSaved={handleFetalHeartRateDataSaved}
|
|
2449
|
+
existingTimeEntries={computedExistingTimeEntries}
|
|
2450
|
+
patient={{
|
|
2451
|
+
uuid: patientUuid,
|
|
2452
|
+
name: 'Patient',
|
|
2453
|
+
gender: 'Female',
|
|
2454
|
+
age: '40 Years',
|
|
2455
|
+
}}
|
|
2456
|
+
/>
|
|
2457
|
+
)}
|
|
2458
|
+
{isMembraneAmnioticFluidFormOpen && (
|
|
2459
|
+
<MembraneAmnioticFluidForm
|
|
2460
|
+
isOpen={isMembraneAmnioticFluidFormOpen}
|
|
2461
|
+
onClose={handleMembraneAmnioticFluidFormClose}
|
|
2462
|
+
onSubmit={handleMembraneAmnioticFluidFormSubmit}
|
|
2463
|
+
patient={{
|
|
2464
|
+
uuid: patientUuid,
|
|
2465
|
+
name: 'Patient',
|
|
2466
|
+
gender: 'Female',
|
|
2467
|
+
age: '40 Years',
|
|
2468
|
+
}}
|
|
2469
|
+
/>
|
|
2470
|
+
)}
|
|
2471
|
+
{isCervicalContractionsFormOpen && (
|
|
2472
|
+
<CervicalContractionsForm
|
|
2473
|
+
isOpen={isCervicalContractionsFormOpen}
|
|
2474
|
+
onClose={handleCervicalContractionsFormClose}
|
|
2475
|
+
onSubmit={handleCervicalContractionsFormSubmit}
|
|
2476
|
+
patient={{
|
|
2477
|
+
uuid: patientUuid,
|
|
2478
|
+
name: 'Patient',
|
|
2479
|
+
gender: 'Female',
|
|
2480
|
+
age: '40 Years',
|
|
2481
|
+
}}
|
|
2482
|
+
/>
|
|
2483
|
+
)}
|
|
2484
|
+
{isOxytocinFormOpen && (
|
|
2485
|
+
<OxytocinForm
|
|
2486
|
+
isOpen={isOxytocinFormOpen}
|
|
2487
|
+
onClose={handleOxytocinFormClose}
|
|
2488
|
+
onSubmit={handleOxytocinFormSubmit}
|
|
2489
|
+
existingTimeEntries={existingTimeEntries}
|
|
2490
|
+
patient={{
|
|
2491
|
+
uuid: patientUuid,
|
|
2492
|
+
name: 'Patient',
|
|
2493
|
+
gender: 'F',
|
|
2494
|
+
age: '28',
|
|
2495
|
+
}}
|
|
2496
|
+
/>
|
|
2497
|
+
)}
|
|
2498
|
+
{isTemperatureFormOpen && (
|
|
2499
|
+
<TemperatureForm
|
|
2500
|
+
isOpen={isTemperatureFormOpen}
|
|
2501
|
+
onClose={() => setIsTemperatureFormOpen(false)}
|
|
2502
|
+
onSubmit={handleTemperatureFormSubmit}
|
|
2503
|
+
initialTime={temperatureFormInitialTime}
|
|
2504
|
+
existingTimeEntries={existingTimeEntries}
|
|
2505
|
+
patient={{
|
|
2506
|
+
uuid: patientUuid,
|
|
2507
|
+
name: 'Patient',
|
|
2508
|
+
gender: 'F',
|
|
2509
|
+
age: '28',
|
|
2510
|
+
}}
|
|
2511
|
+
/>
|
|
2512
|
+
)}
|
|
2513
|
+
{isUrineTestFormOpen && (
|
|
2514
|
+
<UrineTestForm
|
|
2515
|
+
isOpen={isUrineTestFormOpen}
|
|
2516
|
+
onClose={() => setIsUrineTestFormOpen(false)}
|
|
2517
|
+
onSubmit={handleUrineTestFormSubmit}
|
|
2518
|
+
existingTimeEntries={existingTimeEntries}
|
|
2519
|
+
patient={{
|
|
2520
|
+
uuid: patientUuid,
|
|
2521
|
+
name: 'Patient',
|
|
2522
|
+
gender: 'F',
|
|
2523
|
+
age: '28',
|
|
2524
|
+
}}
|
|
2525
|
+
/>
|
|
2526
|
+
)}
|
|
504
2527
|
</Column>
|
|
505
2528
|
</Grid>
|
|
506
2529
|
</Layer>
|