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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/dist/805.js +1 -0
  3. package/dist/805.js.map +1 -0
  4. package/dist/kenyaemr-esm-patient-clinical-view-app.js +2 -2
  5. package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +27 -27
  6. package/dist/main.js +27 -27
  7. package/dist/main.js.map +1 -1
  8. package/dist/routes.json +1 -1
  9. package/package.json +1 -1
  10. package/src/config-schema.ts +97 -0
  11. package/src/contact-list/contact-tracing-history.component.tsx +18 -15
  12. package/src/maternal-and-child-health/partography/components/pulse-bp-graph.component.tsx +1 -0
  13. package/src/maternal-and-child-health/partography/components/temperature-graph.component.tsx +218 -0
  14. package/src/maternal-and-child-health/partography/components/uterine-contractions-graph.component.tsx +209 -0
  15. package/src/maternal-and-child-health/partography/forms/cervical-contractions-form.component.tsx +211 -0
  16. package/src/maternal-and-child-health/partography/forms/cervix-form.component.tsx +354 -0
  17. package/src/maternal-and-child-health/partography/forms/drugs-iv-fluids-form.component.tsx +321 -0
  18. package/src/maternal-and-child-health/partography/forms/fetal-heart-rate-form.component.tsx +275 -0
  19. package/src/maternal-and-child-health/partography/forms/index.ts +9 -0
  20. package/src/maternal-and-child-health/partography/forms/membrane-amniotic-fluid-form.component.tsx +330 -0
  21. package/src/maternal-and-child-health/partography/forms/oxytocin-form.component.tsx +207 -0
  22. package/src/maternal-and-child-health/partography/forms/pulse-bp-form.component.tsx +174 -0
  23. package/src/maternal-and-child-health/partography/forms/temperature-form.component.tsx +210 -0
  24. package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.component.tsx +218 -0
  25. package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.scss +107 -0
  26. package/src/maternal-and-child-health/partography/forms/time-picker-with-clock.component.tsx +174 -0
  27. package/src/maternal-and-child-health/partography/forms/time-picker-with-clock.scss +178 -0
  28. package/src/maternal-and-child-health/partography/forms/urine-test-form.component.tsx +255 -0
  29. package/src/maternal-and-child-health/partography/forms/useCervixData.ts +16 -0
  30. package/src/maternal-and-child-health/partography/graphs/cervical-contractions-graph.component.tsx +266 -0
  31. package/src/maternal-and-child-health/partography/graphs/cervix-graph.component.tsx +429 -0
  32. package/src/maternal-and-child-health/partography/graphs/drugs-iv-fluids-graph-wrapper.component.tsx +163 -0
  33. package/src/maternal-and-child-health/partography/graphs/drugs-iv-fluids-graph.component.tsx +82 -0
  34. package/src/maternal-and-child-health/partography/graphs/fetal-heart-rate-graph.component.tsx +359 -0
  35. package/src/maternal-and-child-health/partography/graphs/index.ts +10 -0
  36. package/src/maternal-and-child-health/partography/graphs/membrane-amniotic-fluid-graph.component.tsx +266 -0
  37. package/src/maternal-and-child-health/partography/graphs/oxytocin-graph-wrapper.component.tsx +190 -0
  38. package/src/maternal-and-child-health/partography/graphs/oxytocin-graph.component.tsx +126 -0
  39. package/src/maternal-and-child-health/partography/graphs/partograph-graph.component.tsx +266 -0
  40. package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph-wrapper.component.tsx +298 -0
  41. package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph.component.tsx +267 -0
  42. package/src/maternal-and-child-health/partography/graphs/temperature-graph.component.tsx +242 -0
  43. package/src/maternal-and-child-health/partography/graphs/urine-test-graph.component.tsx +246 -0
  44. package/src/maternal-and-child-health/partography/partograph.component.tsx +2141 -118
  45. package/src/maternal-and-child-health/partography/partography-dashboard.meta.ts +8 -0
  46. package/src/maternal-and-child-health/partography/partography-data-form.scss +163 -0
  47. package/src/maternal-and-child-health/partography/partography.resource.ts +233 -326
  48. package/src/maternal-and-child-health/partography/partography.scss +1341 -3
  49. package/src/maternal-and-child-health/partography/resources/blood-pressure.resource.ts +96 -0
  50. package/src/maternal-and-child-health/partography/resources/cervical-dilation.resource.ts +109 -0
  51. package/src/maternal-and-child-health/partography/resources/cervix.resource.ts +362 -0
  52. package/src/maternal-and-child-health/partography/resources/descent-of-head.resource.ts +101 -0
  53. package/src/maternal-and-child-health/partography/resources/drugs-fluids.resource.ts +88 -0
  54. package/src/maternal-and-child-health/partography/resources/fetal-heart-rate.resource.ts +122 -0
  55. package/src/maternal-and-child-health/partography/resources/maternal-pulse.resource.ts +77 -0
  56. package/src/maternal-and-child-health/partography/resources/membrane-amniotic-fluid.resource.ts +108 -0
  57. package/src/maternal-and-child-health/partography/resources/oxytocin.resource.ts +159 -0
  58. package/src/maternal-and-child-health/partography/resources/progress-events.resource.ts +6 -0
  59. package/src/maternal-and-child-health/partography/resources/pulse-bp-combined.resource.ts +53 -0
  60. package/src/maternal-and-child-health/partography/resources/temperature.resource.ts +84 -0
  61. package/src/maternal-and-child-health/partography/resources/uterine-contractions.resource.ts +173 -0
  62. package/src/maternal-and-child-health/partography/table/temperature-table.component.tsx +99 -0
  63. package/src/maternal-and-child-health/partography/table/uterine-contractions-table.component.tsx +86 -0
  64. package/src/maternal-and-child-health/partography/types/index.ts +319 -101
  65. package/dist/397.js +0 -1
  66. package/dist/397.js.map +0 -1
@@ -1,66 +1,707 @@
1
- import React, { useState, useEffect, useMemo } from 'react';
2
- import { useTranslation } from 'react-i18next';
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
- TableContainer,
7
+ Grid,
8
+ Layer,
9
+ Pagination,
16
10
  Table,
17
- TableHead,
18
- TableRow,
19
- TableHeader,
20
11
  TableBody,
21
12
  TableCell,
22
- Pagination,
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 { LineChart } from '@carbon/charts-react';
26
- import { useSession, useLayoutType } from '@openmrs/esm-framework';
27
- import '@carbon/charts/styles.css';
28
- import styles from './partography.scss';
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 { getTranslatedPartographyGraphs, getPartographyTableHeaders, getColorForGraph } from './types/index';
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, any[]>>({});
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
- const partographGraphs = useMemo(() => getTranslatedPartographyGraphs(t), [t]);
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
- const chartData = transformEncounterToChartData(encounters, graphType);
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
- const fetalHeartRateData = useGraphData('fetal-heart-rate');
107
- const cervicalDilationData = useGraphData('cervical-dilation');
108
- const descentOfHeadData = useGraphData('descent-of-head');
109
- const uterineContractionsData = useGraphData('uterine-contractions');
110
- const maternalPulseData = useGraphData('maternal-pulse');
111
- const bloodPressureData = useGraphData('blood-pressure');
112
- const temperatureData = useGraphData('temperature');
113
- const urineAnalysisData = useGraphData('urine-analysis');
114
- const drugsFluidsData = useGraphData('drugs-fluids');
115
- const progressEventsData = useGraphData('progress-events');
116
-
117
- const graphDataHooks = {
118
- 'fetal-heart-rate': fetalHeartRateData,
119
- 'cervical-dilation': cervicalDilationData,
120
- 'descent-of-head': descentOfHeadData,
121
- 'uterine-contractions': uterineContractionsData,
122
- 'maternal-pulse': maternalPulseData,
123
- 'blood-pressure': bloodPressureData,
124
- temperature: temperatureData,
125
- 'urine-analysis': urineAnalysisData,
126
- 'drugs-fluids': drugsFluidsData,
127
- 'progress-events': progressEventsData,
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
- setSelectedGraphType(graphId);
149
- setIsFormOpen(true);
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
- const encounterResult = await createPartographyEncounter(
160
- patientUuid,
161
- formData.graphType,
162
- formData,
163
- session?.sessionLocation?.uuid,
164
- session?.user?.uuid,
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
- const currentHook = graphDataHooks[formData.graphType];
173
- if (currentHook?.mutate) {
174
- await currentHook.mutate();
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
- setIsLoading((prev) => ({
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
- const currentHook = graphDataHooks[graph.id];
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
- if (!currentHook?.encounters) {
227
- return [];
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
- const transformedData = transformEncounterToTableData(currentHook.encounters, graph.id, t);
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
- return transformedData;
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 chartData = graphData[graph.id] || [];
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
- const chartOptions = {
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: 'curveMonotoneX',
1859
+ curve: 'curveLinear',
290
1860
  height: '500px',
291
1861
  color: {
292
1862
  scale: {
293
- [chartData[0]?.group]: getColorForGraph(graph.color),
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: 4,
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
- <div className={styles.loadingContainer}>
352
- <p>{t('loadingData', 'Loading data...')}</p>
353
- </div>
354
- ) : chartData.length > 0 ? (
355
- <LineChart data={chartData} options={chartOptions} />
2029
+ <GraphSkeleton />
356
2030
  ) : (
357
- <div className={styles.emptyState}>
358
- <p>{t('noDataAvailable', 'No data available for this graph')}</p>
359
- <Button
360
- kind="primary"
361
- size={controlSize}
362
- renderIcon={Add}
363
- onClick={() => handleAddDataPoint(graph.id)}>
364
- {t('addFirstDataPoint', 'Add first data point')}
365
- </Button>
366
- </div>
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
- {chartData.length > 0 && !isGraphLoading && (
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
- {chartData[chartData.length - 1]?.value?.toFixed(1)} {graph.yAxisLabel}
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
- {(chartData.reduce((sum, item) => sum + item.value, 0) / chartData.length).toFixed(1)}{' '}
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
- <div className={styles.loadingContainer}>
391
- <p>{t('loadingData', 'Loading data...')}</p>
392
- </div>
2141
+ <TableSkeleton />
393
2142
  ) : paginatedData.length > 0 ? (
394
2143
  <>
395
- <DataTable rows={paginatedData} headers={getPartographyTableHeaders(t)}>
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
- if (cell.info.header === 'value' && row.cells.find((c) => c.info.header === 'value')) {
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
- <Tabs selectedIndex={selectedTab} onChange={(data) => setSelectedTab(data.selectedIndex)}>
484
- <TabList className={styles.tabList} aria-label="Partography graphs">
485
- {partographGraphs.map((graph) => (
486
- <Tab key={graph.id}>{graph.title}</Tab>
487
- ))}
488
- </TabList>
489
- <TabPanels>
490
- {partographGraphs.map((graph) => (
491
- <TabPanel key={graph.id}>{renderGraph(graph)}</TabPanel>
492
- ))}
493
- </TabPanels>
494
- </Tabs>
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>