@kenyaemr/esm-patient-clinical-view-app 5.4.2-pre.2758 → 5.4.2-pre.2764

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 (34) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/dist/127.js +1 -1
  3. package/dist/{805.js → 189.js} +1 -1
  4. package/dist/189.js.map +1 -0
  5. package/dist/40.js +1 -1
  6. package/dist/916.js +1 -1
  7. package/dist/kenyaemr-esm-patient-clinical-view-app.js +2 -2
  8. package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +36 -36
  9. package/dist/main.js +3 -3
  10. package/dist/main.js.map +1 -1
  11. package/dist/routes.json +1 -1
  12. package/package.json +1 -1
  13. package/src/config-schema.ts +58 -36
  14. package/src/maternal-and-child-health/partography/components/temperature-graph.component.tsx +0 -4
  15. package/src/maternal-and-child-health/partography/components/uterine-contractions-graph.component.tsx +33 -11
  16. package/src/maternal-and-child-health/partography/forms/cervical-contractions-form.component.tsx +124 -136
  17. package/src/maternal-and-child-health/partography/forms/cervix-form.component.tsx +23 -14
  18. package/src/maternal-and-child-health/partography/forms/drugs-iv-fluids-form.component.tsx +6 -10
  19. package/src/maternal-and-child-health/partography/forms/fetal-heart-rate-form.component.tsx +36 -13
  20. package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.component.tsx +27 -46
  21. package/src/maternal-and-child-health/partography/forms/useCervixData.ts +2 -2
  22. package/src/maternal-and-child-health/partography/graphs/cervix-graph.component.tsx +56 -18
  23. package/src/maternal-and-child-health/partography/graphs/fetal-heart-rate-graph.component.tsx +36 -23
  24. package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph.component.tsx +10 -5
  25. package/src/maternal-and-child-health/partography/partograph.component.tsx +315 -371
  26. package/src/maternal-and-child-health/partography/partography.resource.ts +788 -230
  27. package/src/maternal-and-child-health/partography/partography.scss +68 -40
  28. package/src/maternal-and-child-health/partography/resources/cervix.resource.ts +79 -76
  29. package/src/maternal-and-child-health/partography/resources/uterine-contractions.resource.ts +33 -12
  30. package/src/maternal-and-child-health/partography/types/index.ts +94 -0
  31. package/translations/am.json +0 -8
  32. package/translations/en.json +0 -8
  33. package/translations/sw.json +0 -8
  34. package/dist/805.js.map +0 -1
@@ -19,6 +19,7 @@ import {
19
19
  import { Add, ChartColumn, Table as TableIcon } from '@carbon/react/icons';
20
20
  import { openmrsFetch, useLayoutType, useSession } from '@openmrs/esm-framework';
21
21
  import React, { useEffect, useMemo, useState } from 'react';
22
+ import { codeToPlus, contractionLevelUuidMap, labelToUuid, contractionLevelUuidToLabel } from './types';
22
23
  import { useTranslation } from 'react-i18next';
23
24
  import {
24
25
  CervicalContractionsForm,
@@ -65,7 +66,6 @@ enum ScaleTypes {
65
66
  LINEAR = 'linear',
66
67
  }
67
68
 
68
- // --- INLINE TYPE DEFINITIONS ADDED FOR CONTEXT ---
69
69
  type GraphDefinition = {
70
70
  id: string;
71
71
  title: string;
@@ -83,22 +83,19 @@ type ChartDataPoint = {
83
83
  group: string;
84
84
  value: number;
85
85
  };
86
- // --- END INLINE TYPE DEFINITIONS ---
87
86
 
88
- // --- CERVIX CHART OPTIONS: MEDICAL PARTOGRAPH STYLING ---
89
87
  const CERVIX_CHART_OPTIONS = {
90
88
  axes: {
91
89
  bottom: {
92
- title: '', // Remove Hours label completely
90
+ title: '',
93
91
  mapsTo: 'hour',
94
92
  scaleType: ScaleTypes.LINEAR,
95
93
  domain: [0, 10],
96
94
  tick: {
97
- count: 21, // Force exactly 21 ticks for all 30-min intervals
95
+ count: 21,
98
96
  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
97
+ 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],
100
98
  formatter: (hour) => {
101
- // Format as hours with 30-minute intervals
102
99
  if (hour === 0) {
103
100
  return '0';
104
101
  } else if (hour === 0.5) {
@@ -125,8 +122,6 @@ const CERVIX_CHART_OPTIONS = {
125
122
  ticks: {
126
123
  values: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
127
124
  formatter: (value) => {
128
- // Show both cervical dilation and descent of head values
129
- // Direct mapping: 5=high position, 1=most descended
130
125
  if (value >= 1 && value <= 5) {
131
126
  return `${value}cm / D${value}`;
132
127
  } else if (value === 0) {
@@ -173,13 +168,11 @@ const CERVIX_CHART_OPTIONS = {
173
168
  clickable: false,
174
169
  },
175
170
  };
176
- // --- END CERVIX CHART OPTIONS ---
177
171
 
178
172
  type PartographyProps = {
179
173
  patientUuid: string;
180
174
  };
181
175
 
182
- // Skeleton Components for Loading States with inline animation
183
176
  const GraphSkeleton: React.FC = () => {
184
177
  const skeletonStyle = {
185
178
  background: 'linear-gradient(90deg, #f4f4f4 25%, #e0e0e0 50%, #f4f4f4 75%)',
@@ -188,7 +181,6 @@ const GraphSkeleton: React.FC = () => {
188
181
  borderRadius: '4px',
189
182
  };
190
183
 
191
- // Add keyframes to head if not already added
192
184
  React.useEffect(() => {
193
185
  if (!document.querySelector('#skeleton-keyframes')) {
194
186
  const style = document.createElement('style');
@@ -218,7 +210,6 @@ const GraphSkeleton: React.FC = () => {
218
210
  marginBottom: '1rem',
219
211
  }}
220
212
  />
221
- {/* Skeleton for custom time labels (cervix specific) */}
222
213
  <div style={{ marginTop: '1rem', borderTop: '1px solid #e0e0e0', paddingTop: '0.5rem' }}>
223
214
  <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.25rem' }}>
224
215
  {Array.from({ length: 11 }).map((_, index) => (
@@ -287,13 +278,10 @@ const TableSkeleton: React.FC = () => {
287
278
  const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
288
279
  const { t } = useTranslation();
289
280
 
290
- // Development flag to enable dummy data (set to false for production)
291
281
  const ENABLE_DUMMY_DATA = false;
292
282
 
293
- // Local state for regular partography data (not saved to OpenMRS yet)
294
283
  const [localPartographyData, setLocalPartographyData] = useState<Record<string, any[]>>({});
295
284
 
296
- // Local state for fetal heart rate data
297
285
  const [localFetalHeartRateData, setLocalFetalHeartRateData] = useState<
298
286
  Array<{
299
287
  hour: number;
@@ -303,7 +291,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
303
291
  }>
304
292
  >([]);
305
293
 
306
- // Backend membrane amniotic fluid data
307
294
  const {
308
295
  membraneAmnioticFluidEntries,
309
296
  isLoading: isMembraneAmnioticFluidLoading,
@@ -339,7 +326,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
339
326
  }>
340
327
  >([]);
341
328
 
342
- // Pulse and BP backend data
343
329
  const {
344
330
  data: loadedPulseData,
345
331
  isLoading: isPulseDataLoading,
@@ -353,7 +339,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
353
339
  mutate: mutateBPData,
354
340
  } = usePartographyData(patientUuid || '', 'blood-pressure');
355
341
 
356
- // Temperature backend data
357
342
  const {
358
343
  data: loadedTemperatureData,
359
344
  isLoading: isTemperatureDataLoading,
@@ -361,7 +346,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
361
346
  mutate: mutateTemperatureData,
362
347
  } = usePartographyData(patientUuid || '', 'temperature');
363
348
 
364
- // Urine Test backend data
365
349
  const {
366
350
  data: urineTestEncounters = [],
367
351
  isLoading: isUrineTestLoading,
@@ -369,34 +353,9 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
369
353
  mutate: mutateUrineTestData,
370
354
  } = usePartographyData(patientUuid || '', 'urine-analysis');
371
355
 
372
- // Transform backend urine test data to UrineTestData[]
373
356
  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
357
  return (urineTestEncounters || []).map((encounter, index) => {
398
358
  const obs = encounter.obs || [];
399
- // Helper to get obs value by concept
400
359
  const getObsValue = (conceptUuid) => {
401
360
  const found = obs.find((o) => o.concept.uuid === conceptUuid);
402
361
  if (!found) {
@@ -409,7 +368,7 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
409
368
  }
410
369
  return v != null ? String(v) : '';
411
370
  };
412
- // Helper to get time from description obs
371
+
413
372
  const getTime = () => {
414
373
  const timeObs = obs.find(
415
374
  (o) =>
@@ -425,9 +384,8 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
425
384
  }
426
385
  return '';
427
386
  };
428
- // Helper to get timeSlot from obs (look for concept with 'time-slot' or value with 'TimeSlot:')
387
+
429
388
  const getTimeSlot = () => {
430
- // Try concept for time-slot
431
389
  const slotObs = obs.find(
432
390
  (o) =>
433
391
  o.concept.uuid === PARTOGRAPHY_CONCEPTS['time-slot'] ||
@@ -445,7 +403,7 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
445
403
  }
446
404
  return '';
447
405
  };
448
- // Sample Collected and Result Returned: try to find obs with concept display or uuid containing 'collected' or 'returned'
406
+
449
407
  const getSampleCollected = () => {
450
408
  const found = obs.find(
451
409
  (o) =>
@@ -468,7 +426,7 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
468
426
  }
469
427
  return '';
470
428
  };
471
- // Get urine volume as string (not code), handle case-insensitive concept id
429
+
472
430
  const getVolume = () => {
473
431
  const found = obs.find(
474
432
  (o) =>
@@ -494,7 +452,7 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
494
452
  }
495
453
  return '';
496
454
  };
497
- // Extract 'Time Slot: HH:mm' from event-description obs
455
+
498
456
  let timeSlot = '';
499
457
  const eventDescObs = obs.find(
500
458
  (o) =>
@@ -508,7 +466,7 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
508
466
  timeSlot = match[1];
509
467
  }
510
468
  }
511
- // Ensure volume is a number if present, otherwise undefined
469
+
512
470
  let volumeRaw = getVolume();
513
471
  let volume: number | undefined = undefined;
514
472
  if (typeof volumeRaw === 'number') {
@@ -518,7 +476,7 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
518
476
  }
519
477
  const sampleCollected = getSampleCollected();
520
478
  const resultReturned = getResultReturned();
521
- // final row prepared
479
+
522
480
  return {
523
481
  id: `urine-test-${index}`,
524
482
  date: new Date(encounter.encounterDatetime).toLocaleDateString(),
@@ -556,7 +514,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
556
514
  });
557
515
  };
558
516
 
559
- // Load cervix data from OpenMRS
560
517
  const {
561
518
  cervixData: loadedCervixData,
562
519
  existingTimeEntries,
@@ -567,7 +524,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
567
524
  mutate: mutateCervixData,
568
525
  } = useCervixFormData(patientUuid || '');
569
526
 
570
- // Load fetal heart rate data from OpenMRS with null safety
571
527
  const {
572
528
  fetalHeartRateData: loadedFetalHeartRateData = [],
573
529
  isLoading: isFetalHeartRateLoading = false,
@@ -575,7 +531,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
575
531
  mutate: mutateFetalHeartRateData = () => {},
576
532
  } = useFetalHeartRateData(patientUuid || '');
577
533
 
578
- // Fetch actual drug orders from OpenMRS
579
534
  const {
580
535
  drugOrders: loadedDrugOrders = [],
581
536
  isLoading: isDrugOrdersLoading = false,
@@ -583,11 +538,8 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
583
538
  mutate: mutateDrugOrders = () => {},
584
539
  } = useDrugOrders(patientUuid || '');
585
540
 
586
- // Debug: Log drug orders data
587
541
  useEffect(() => {
588
542
  if (patientUuid) {
589
- // Drug orders status logged in development only
590
-
591
543
  if (drugOrdersError) {
592
544
  console.error('Drug Orders Error:', drugOrdersError);
593
545
  }
@@ -618,57 +570,93 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
618
570
  const [pulseBPViewMode, setPulseBPViewMode] = useState<'graph' | 'table'>('graph');
619
571
  const [temperatureViewMode, setTemperatureViewMode] = useState<'graph' | 'table'>('graph');
620
572
  const [urineTestViewMode, setUrineTestViewMode] = useState<'graph' | 'table'>('graph');
573
+ function getInitialPageSize(key, fallback = 10) {
574
+ if (typeof window !== 'undefined') {
575
+ const stored = window.localStorage.getItem(key);
576
+ if (stored && !isNaN(Number(stored))) {
577
+ return Number(stored);
578
+ }
579
+ }
580
+ return fallback;
581
+ }
621
582
  const [fetalHeartRateCurrentPage, setFetalHeartRateCurrentPage] = useState(1);
622
- const [fetalHeartRatePageSize, setFetalHeartRatePageSize] = useState(5);
583
+ const [fetalHeartRatePageSize, setFetalHeartRatePageSize] = useState(() =>
584
+ getInitialPageSize('fetalHeartRatePageSize'),
585
+ );
623
586
  const [membraneAmnioticFluidCurrentPage, setMembraneAmnioticFluidCurrentPage] = useState(1);
624
- const [membraneAmnioticFluidPageSize, setMembraneAmnioticFluidPageSize] = useState(5);
587
+ const [membraneAmnioticFluidPageSize, setMembraneAmnioticFluidPageSize] = useState(() =>
588
+ getInitialPageSize('membraneAmnioticFluidPageSize'),
589
+ );
625
590
  const [cervicalContractionsCurrentPage, setCervicalContractionsCurrentPage] = useState(1);
626
- const [cervicalContractionsPageSize, setCervicalContractionsPageSize] = useState(5);
591
+ const [cervicalContractionsPageSize, setCervicalContractionsPageSize] = useState(() =>
592
+ getInitialPageSize('cervicalContractionsPageSize'),
593
+ );
627
594
  const [oxytocinCurrentPage, setOxytocinCurrentPage] = useState(1);
628
- const [oxytocinPageSize, setOxytocinPageSize] = useState(5);
595
+ const [oxytocinPageSize, setOxytocinPageSize] = useState(() => getInitialPageSize('oxytocinPageSize'));
629
596
  const [drugsIVFluidsCurrentPage, setDrugsIVFluidsCurrentPage] = useState(1);
597
+ const [drugsIVFluidsPageSize, setDrugsIVFluidsPageSize] = useState(() => getInitialPageSize('drugsIVFluidsPageSize'));
630
598
  const [pulseBPCurrentPage, setPulseBPCurrentPage] = useState(1);
599
+ const [pulseBPPageSize, setPulseBPPageSize] = useState(() => getInitialPageSize('pulseBPPageSize'));
631
600
  const [temperatureCurrentPage, setTemperatureCurrentPage] = useState(1);
632
- const [temperaturePageSize, setTemperaturePageSize] = useState(5);
601
+ const [temperaturePageSize, setTemperaturePageSize] = useState(() => getInitialPageSize('temperaturePageSize'));
633
602
  const [urineTestCurrentPage, setUrineTestCurrentPage] = useState(1);
634
- const [urineTestPageSize, setUrineTestPageSize] = useState(5);
635
- const [drugsIVFluidsPageSize, setDrugsIVFluidsPageSize] = useState(5);
636
- const [pulseBPPageSize, setPulseBPPageSize] = useState(5);
603
+ const [urineTestPageSize, setUrineTestPageSize] = useState(() => getInitialPageSize('urineTestPageSize'));
604
+
605
+ // Persist page size changes
606
+ function persistPageSize(key, value) {
607
+ if (typeof window !== 'undefined') {
608
+ window.localStorage.setItem(key, String(value));
609
+ }
610
+ }
637
611
  const [currentPage, setCurrentPage] = useState<Record<string, number>>({});
638
612
  const [pageSize, setPageSize] = useState<Record<string, number>>({});
639
613
  const [isLoading, setIsLoading] = useState<Record<string, boolean>>({});
640
614
 
641
- // Transform cervix data to match the existing format expected by the chart
642
615
  const cervixFormData = useMemo(() => {
643
- // Use OpenMRS data if available
644
616
  const dataToUse = loadedCervixData.length > 0 ? loadedCervixData : [];
645
617
 
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,
618
+ // If all hours are the same, fallback to using the row index as the hour
619
+ let processedData = dataToUse
620
+ .filter((data) => {
621
+ const hasHour = data.hour !== null && data.hour !== undefined && !isNaN(data.hour);
622
+ // Accept empty time, but always show fallback in table
623
+ return (
624
+ hasHour &&
625
+ data.cervicalDilation !== null &&
626
+ !isNaN(data.cervicalDilation) &&
627
+ data.descentOfHead !== null &&
628
+ !isNaN(data.descentOfHead)
629
+ );
630
+ })
631
+ .map((data, idx, arr) => ({
632
+ hour: data.hour,
633
+ time: data.time && data.time.trim() !== '' ? data.time : '--:--',
634
+ cervicalDilation: data.cervicalDilation,
635
+ descentOfHead: data.descentOfHead,
652
636
  entryDate: new Date(data.encounterDatetime).toLocaleDateString(),
653
637
  entryTime: new Date(data.encounterDatetime).toLocaleTimeString(),
654
- }))
655
- .filter((data) => data.hour > 0 && data.cervicalDilation > 0 && data.descentOfHead > 0);
638
+ _originalHour: data.hour,
639
+ _rowIndex: idx,
640
+ }));
641
+
642
+ // If all non-null hours are the same, override hour with row index
643
+ const uniqueHours = Array.from(
644
+ new Set(processedData.map((d) => d.hour).filter((h) => h !== null && h !== undefined)),
645
+ );
646
+ if (uniqueHours.length === 1 && processedData.length > 1) {
647
+ processedData = processedData.map((d, idx) => ({ ...d, hour: idx }));
648
+ }
656
649
 
657
- // Return dummy data if no processed data and dummy data is enabled, otherwise return processed data
658
650
  return processedData.length > 0 ? processedData : ENABLE_DUMMY_DATA ? generateExtendedDummyData() : [];
659
651
  }, [loadedCervixData, ENABLE_DUMMY_DATA]);
660
652
 
661
- // Compute existingTimeEntries from both local and OpenMRS data
662
653
  const computedExistingTimeEntries = useMemo(() => {
663
- // Use the existing time entries from OpenMRS only
664
654
  return existingTimeEntries;
665
655
  }, [existingTimeEntries]);
666
656
 
667
- // Compute combined fetal heart rate data from both local and OpenMRS sources
668
657
  const computedFetalHeartRateData = useMemo(() => {
669
658
  const combined = [...localFetalHeartRateData];
670
659
 
671
- // Add OpenMRS data if available and not already in local data
672
660
  if (loadedFetalHeartRateData?.length > 0) {
673
661
  loadedFetalHeartRateData.forEach((openMrsEntry) => {
674
662
  const exists = combined.find(
@@ -676,10 +664,9 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
676
664
  );
677
665
 
678
666
  if (!exists) {
679
- // Transform OpenMRS data to match local data structure
680
667
  const transformedEntry = {
681
668
  hour: openMrsEntry.hour,
682
- value: openMrsEntry.fetalHeartRate, // Map fetalHeartRate to value
669
+ value: openMrsEntry.fetalHeartRate,
683
670
  group: 'Fetal Heart Rate',
684
671
  time: openMrsEntry.time,
685
672
  };
@@ -688,7 +675,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
688
675
  });
689
676
  }
690
677
 
691
- // Sort by hour and time
692
678
  const sorted = combined.sort((a, b) => {
693
679
  if (a.hour !== b.hour) {
694
680
  return a.hour - b.hour;
@@ -729,9 +715,7 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
729
715
  if (!isLoading) {
730
716
  let chartData: ChartDataPoint[] = [];
731
717
 
732
- // Special handling for pulse and BP combined encounters
733
718
  if (graphType === 'maternal-pulse' || graphType === 'blood-pressure') {
734
- // Collect all obs for pulse and BP from all encounters
735
719
  chartData = [];
736
720
  if (Array.isArray(encounters)) {
737
721
  encounters.forEach((encounter) => {
@@ -772,7 +756,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
772
756
  });
773
757
  }
774
758
  } else if (localPartographyData[graphType] && localPartographyData[graphType].length > 0) {
775
- // Transform local data to chart format
776
759
  chartData = localPartographyData[graphType].map((item, index) => ({
777
760
  hour: index + 1, // Use index as hour for now
778
761
  group: graphType,
@@ -802,7 +785,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
802
785
  return { encounters, isLoading, mutate };
803
786
  };
804
787
 
805
- // Function to generate dummy data for different graph types
806
788
  const generateDummyDataForGraph = (graphType: string): ChartDataPoint[] => {
807
789
  const baseTimeEntries = [
808
790
  { hour: 0, time: '08:00' },
@@ -820,7 +802,7 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
820
802
  hour: entry.hour,
821
803
  time: entry.time,
822
804
  group: 'Fetal Heart Rate',
823
- value: 140 + Math.random() * 20, // Normal range 120-160
805
+ value: 140 + Math.random() * 20,
824
806
  }));
825
807
 
826
808
  case 'maternal-pulse':
@@ -828,7 +810,7 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
828
810
  hour: entry.hour,
829
811
  time: entry.time,
830
812
  group: 'Maternal Pulse',
831
- value: 75 + Math.random() * 15, // Normal range 60-100
813
+ value: 75 + Math.random() * 15,
832
814
  }));
833
815
 
834
816
  case 'blood-pressure':
@@ -837,13 +819,13 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
837
819
  hour: entry.hour,
838
820
  time: entry.time,
839
821
  group: 'Systolic',
840
- value: 115 + Math.random() * 10, // Normal systolic
822
+ value: 115 + Math.random() * 10,
841
823
  })),
842
824
  ...baseTimeEntries.map((entry) => ({
843
825
  hour: entry.hour,
844
826
  time: entry.time,
845
827
  group: 'Diastolic',
846
- value: 75 + Math.random() * 5, // Normal diastolic
828
+ value: 75 + Math.random() * 5,
847
829
  })),
848
830
  ];
849
831
 
@@ -1181,31 +1163,16 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
1181
1163
  contractionCount: string;
1182
1164
  timeSlot: string;
1183
1165
  }) => {
1184
- const contractionLevelUuidMap: Record<string, string> = {
1185
- none: '1107AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1186
- mild: '1498AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1187
- moderate: '1499AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1188
- strong: '166788AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
1189
- };
1190
1166
  const contractionLevel = formData.contractionLevel || 'none';
1191
1167
  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
1168
  const contractionCount = parseInt(formData.contractionCount || '1', 10);
1196
1169
  const timeSlot = formData.timeSlot || new Date().toISOString().substring(11, 16);
1197
1170
 
1198
- // Debug info removed for linting
1199
-
1200
- // Always send both obs: count (numeric) and level (coded)
1201
1171
  const encounterFormData = {
1202
- contractionLevelValue, // UUID for level (none, mild, moderate, strong)
1203
- contractionLevelUuid,
1204
1172
  contractionCount,
1205
- contractionCountConcept,
1173
+ contractionLevel: contractionLevelValue,
1206
1174
  timeSlot,
1207
1175
  };
1208
- // Payload prepared for submission
1209
1176
  try {
1210
1177
  const saveResult = await createPartographyEncounter(patientUuid, 'uterine-contractions', encounterFormData);
1211
1178
  if (saveResult.success) {
@@ -1370,7 +1337,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
1370
1337
  timeSampleCollected: string;
1371
1338
  timeResultsReturned: string;
1372
1339
  }) => {
1373
- // Validate data before adding
1374
1340
  if (
1375
1341
  !formData.timeSlot ||
1376
1342
  !formData.exactTime ||
@@ -1384,24 +1350,7 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
1384
1350
  return;
1385
1351
  }
1386
1352
 
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
1353
  try {
1404
- // Get current time in HH:mm format
1405
1354
  const now = new Date();
1406
1355
  const pad = (n) => n.toString().padStart(2, '0');
1407
1356
  const currentTime = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
@@ -1419,7 +1368,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
1419
1368
  }
1420
1369
  };
1421
1370
 
1422
- // Generate table data for membrane amniotic fluid from backend only
1423
1371
  const getMembraneAmnioticFluidTableData = () => {
1424
1372
  return membraneAmnioticFluidEntries.map((data, index) => ({
1425
1373
  id: data.id || `maf-${index}`,
@@ -1479,26 +1427,9 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
1479
1427
  if (obs.concept.uuid === '159682AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') {
1480
1428
  contractionCount = String(obs.value);
1481
1429
  }
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
- }
1430
+ const contractionLabel = contractionLevelUuidToLabel(obs.concept.uuid);
1431
+ if (contractionLabel !== obs.concept.uuid) {
1432
+ contractionLevel = contractionLabel;
1502
1433
  }
1503
1434
  }
1504
1435
  }
@@ -1512,12 +1443,8 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
1512
1443
  });
1513
1444
  };
1514
1445
 
1515
- // Generate table data for oxytocin (use backend data only)
1516
1446
  const getOxytocinTableData = () => {
1517
- // Only map oxytocin-specific fields, do not include any event-description or urine test fields
1518
1447
  return loadedOxytocinData.map((data, index) => {
1519
- // Defensive: Only use fields that are expected for oxytocin
1520
- // If backend returns extra obs, ignore them
1521
1448
  return {
1522
1449
  id: `oxy-${index}`,
1523
1450
  date: data.encounterDatetime ? new Date(data.encounterDatetime).toLocaleDateString() : '',
@@ -1560,22 +1487,23 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
1560
1487
 
1561
1488
  // Generate table data for pulse and BP from backend
1562
1489
  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>);
1490
+ // Prepare BP data as array of {datetime, systolic, diastolic}
1491
+ const bpEntries = (loadedBPData || [])
1492
+ .map((encounter) => {
1493
+ const systolicObs = encounter.obs.find((obs) => obs.concept.uuid === PARTOGRAPHY_CONCEPTS['systolic-bp']);
1494
+ const diastolicObs = encounter.obs.find((obs) => obs.concept.uuid === PARTOGRAPHY_CONCEPTS['diastolic-bp']);
1495
+ if (systolicObs && diastolicObs) {
1496
+ return {
1497
+ datetime: new Date(encounter.encounterDatetime),
1498
+ systolicBP: typeof systolicObs.value === 'number' ? systolicObs.value : parseFloat(systolicObs.value),
1499
+ diastolicBP: typeof diastolicObs.value === 'number' ? diastolicObs.value : parseFloat(diastolicObs.value),
1500
+ };
1501
+ }
1502
+ return null;
1503
+ })
1504
+ .filter(Boolean);
1577
1505
 
1578
- // For each pulse, try to find matching BP by encounterDatetime
1506
+ // For each pulse, find the closest BP entry in time (within 1 hour)
1579
1507
  return (loadedPulseData || []).map((encounter, index) => {
1580
1508
  const pulseObs = encounter.obs.find((obs) => obs.concept.uuid === PARTOGRAPHY_CONCEPTS['maternal-pulse']);
1581
1509
  const pulse = pulseObs
@@ -1583,14 +1511,26 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
1583
1511
  ? pulseObs.value
1584
1512
  : parseFloat(pulseObs.value)
1585
1513
  : 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] || {};
1514
+ const pulseDate = new Date(encounter.encounterDatetime);
1515
+ const date = pulseDate.toLocaleDateString();
1516
+ const time = pulseDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1517
+
1518
+ // Find closest BP entry within 1 hour
1519
+ let closestBP = null;
1520
+ let minDiff = 60 * 60 * 1000; // 1 hour in ms
1521
+ for (const bp of bpEntries) {
1522
+ const diff = Math.abs(bp.datetime.getTime() - pulseDate.getTime());
1523
+ if (diff < minDiff) {
1524
+ minDiff = diff;
1525
+ closestBP = bp;
1526
+ }
1527
+ }
1528
+
1589
1529
  return {
1590
1530
  id: `pulse-bp-${index}`,
1591
1531
  pulse,
1592
- systolicBP: bp.systolicBP ?? '',
1593
- diastolicBP: bp.diastolicBP ?? '',
1532
+ systolicBP: closestBP ? closestBP.systolicBP : '',
1533
+ diastolicBP: closestBP ? closestBP.diastolicBP : '',
1594
1534
  date,
1595
1535
  time,
1596
1536
  };
@@ -1661,24 +1601,25 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
1661
1601
  if (graph.id === 'cervix') {
1662
1602
  return loadedCervixData
1663
1603
  .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
- );
1604
+ // Include if either hour or time is present, and cervicalDilation is valid
1605
+ const hasHour = data.hour !== null && data.hour !== undefined && !isNaN(data.hour);
1606
+ const hasTime = data.time && data.time.trim() !== '';
1607
+ return (hasHour || hasTime) && data.cervicalDilation !== null && !isNaN(data.cervicalDilation);
1672
1608
  })
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
- }));
1609
+ .map((data, index) => {
1610
+ let hourInput = data.hour !== null && data.hour !== undefined && !isNaN(data.hour) ? `${data.hour} hr` : '';
1611
+ // Always show the form time if available
1612
+ let formTime = data.time && data.time.trim() !== '' ? data.time : data.timeDisplay || '';
1613
+ return {
1614
+ id: `cervix-${index}`,
1615
+ date: new Date(data.encounterDatetime).toLocaleDateString() || 'N/A',
1616
+ actualTime: new Date(data.encounterDatetime).toLocaleTimeString() || 'N/A',
1617
+ cervicalDilation: `${data.cervicalDilation} cm`,
1618
+ descentOfHead: `${data.descentLabel || data.descentOfHead || ''}`,
1619
+ hourInput,
1620
+ formTime: formTime || 'N/A',
1621
+ };
1622
+ });
1682
1623
  }
1683
1624
 
1684
1625
  // Membrane amniotic fluid: use backend data only
@@ -1822,15 +1763,12 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
1822
1763
  const endIndex = startIndex + currentPageSize;
1823
1764
  const paginatedData = tableData.slice(startIndex, endIndex);
1824
1765
 
1825
- // Default chart data - declare once here
1826
1766
  let finalChartData: ChartDataPoint[] = patientChartData;
1827
1767
  let zeroTime: Date | undefined;
1828
1768
  let maxChartTime: Date | undefined;
1829
1769
 
1830
- // --- Data for custom time labels ---
1831
1770
  let timeLabelsData: { hours: string; time: string; span: number }[] = [];
1832
1771
 
1833
- // Default chart options with medical partograph styling
1834
1772
  let chartOptions: any = {
1835
1773
  title: graph.title,
1836
1774
  axes: {
@@ -1893,47 +1831,40 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
1893
1831
  },
1894
1832
  };
1895
1833
 
1896
- // Hide X-axis title and labels for non-cervix graphs only
1897
1834
  if (graph.id !== 'cervix') {
1898
1835
  chartOptions.axes.bottom.title = undefined;
1899
1836
  chartOptions.axes.bottom.tick = {
1900
- formatter: () => '', // Hide tick labels for non-cervix graphs
1837
+ formatter: () => '',
1901
1838
  };
1902
1839
  }
1903
1840
 
1904
- // --- START LOGIC FOR CERVIX GRAPH ---
1905
1841
  if (graph.id === 'cervix') {
1906
- // 1. Apply custom axis and styling options
1907
1842
  chartOptions = {
1908
1843
  ...chartOptions,
1909
1844
  ...CERVIX_CHART_OPTIONS,
1910
1845
  title: graph.title,
1911
1846
  color: {
1912
1847
  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
1848
+ 'Alert Line': '#FFD700',
1849
+ 'Action Line': '#FF0000',
1850
+ 'Cervical Dilation': '#22C55E',
1851
+ 'Descent of Head': '#2563EB',
1917
1852
  [graph.id]: getColorForGraph(graph.color),
1918
1853
  },
1919
1854
  },
1920
1855
  legend: {
1921
- position: 'top', // Move legend to the top for Cervix graph
1856
+ position: 'top',
1922
1857
  },
1923
1858
  };
1924
1859
 
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
1860
+ const ALERT_START_CM = 4;
1927
1861
  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
1862
+ const ALERT_ACTION_DIFFERENCE_HOURS = 4;
1863
+ const EXPECTED_LABOR_DURATION_HOURS = 6;
1930
1864
 
1931
1865
  const staticLinesData: ChartDataPoint[] = [
1932
- // Alert Line Points: (Hour 0, 4cm) -> (Hour 6, 10cm)
1933
1866
  { hour: 0, value: ALERT_START_CM, group: 'Alert Line' },
1934
1867
  { hour: EXPECTED_LABOR_DURATION_HOURS, value: CERVIX_DILATION_MAX, group: 'Alert Line' },
1935
-
1936
- // Action Line Points: (Hour 4, 4cm) -> (Hour 10, 10cm)
1937
1868
  { hour: ALERT_ACTION_DIFFERENCE_HOURS, value: ALERT_START_CM, group: 'Action Line' },
1938
1869
  {
1939
1870
  hour: ALERT_ACTION_DIFFERENCE_HOURS + EXPECTED_LABOR_DURATION_HOURS,
@@ -1942,7 +1873,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
1942
1873
  },
1943
1874
  ];
1944
1875
 
1945
- // 3. Add Cervical Dilation data points from form inputs
1946
1876
  const cervicalDilationData: ChartDataPoint[] = cervixFormData.map((data) => ({
1947
1877
  hour: data.hour,
1948
1878
  value: data.cervicalDilation,
@@ -1950,40 +1880,86 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
1950
1880
  time: data.time,
1951
1881
  }));
1952
1882
 
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
1883
  const descentOfHeadData: ChartDataPoint[] = cervixFormData.map((data) => ({
1957
1884
  hour: data.hour,
1958
- value: data.descentOfHead, // Direct mapping: medical 5→chart 5 (high), medical 1→chart 1 (low)
1885
+ value: data.descentOfHead,
1959
1886
  group: 'Descent of Head',
1960
1887
  time: data.time,
1961
1888
  }));
1962
1889
 
1963
- // 5. Combine all data - Alert/Action lines + patient data + form data
1964
1890
  finalChartData = [...patientChartData, ...staticLinesData, ...cervicalDilationData, ...descentOfHeadData];
1965
1891
 
1966
- // 6. Generate dynamic time labels for the custom footer (from form data)
1967
1892
  timeLabelsData = [];
1968
1893
 
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);
1894
+ const allHours = [...cervixFormData.map((d) => d.hour), ...loadedCervixData.map((d) => d.hour)]
1895
+ .filter((h) => h !== null && h !== undefined && !isNaN(h))
1896
+ .map(Number);
1897
+ const uniqueSortedHours = Array.from(new Set(allHours)).sort((a, b) => a - b);
1898
+
1899
+ const usedTimeEntries: string[] = [];
1971
1900
 
1972
- for (let i = 0; i < Math.min(maxHours, 10); i++) {
1973
- const hourLabel = i === 0 ? '0' : `${i}hr`;
1901
+ uniqueSortedHours.forEach((hourVal) => {
1902
+ const hourLabel = `${hourVal}`;
1903
+ let timeValue = '--:--';
1904
+ const formEntry = (cervixFormData as Array<{ hour: number; time: string /* etc. */ }>).find(
1905
+ (d) => d.hour === hourVal && typeof d.time === 'string' && d.time.trim() !== '',
1906
+ );
1974
1907
 
1975
- // Find corresponding form data for this hour
1976
- const formDataForHour = cervixFormData.find((data) => data.hour === i);
1977
- const timeValue = formDataForHour ? formDataForHour.time : '--:--';
1908
+ if (formEntry) {
1909
+ timeValue = formEntry.time;
1910
+ } else {
1911
+ const backendEntry = loadedCervixData.find(
1912
+ (d) =>
1913
+ d.hour === hourVal &&
1914
+ ((d.time && d.time.trim() !== '') || (typeof d.timeDisplay === 'string' && d.timeDisplay.trim() !== '')),
1915
+ );
1916
+ if (backendEntry) {
1917
+ if (backendEntry.time && backendEntry.time.trim() !== '') {
1918
+ timeValue = backendEntry.time;
1919
+ } else if (typeof backendEntry.timeDisplay === 'string' && backendEntry.timeDisplay.trim() !== '') {
1920
+ timeValue = backendEntry.timeDisplay;
1921
+ }
1922
+ } else {
1923
+ const cervixFormArray = Array.isArray(cervixFormData) ? cervixFormData : [];
1924
+ const loadedCervixArray = Array.isArray(loadedCervixData) ? loadedCervixData : [];
1925
+
1926
+ const cervixFormFiltered = (cervixFormArray as Array<{ hour: number | null; time: string }>).filter(
1927
+ (d) => d.hour == null && d.time && d.time.trim() !== '' && !usedTimeEntries.includes(d.time),
1928
+ );
1929
+
1930
+ const loadedCervixFiltered = loadedCervixArray.filter(
1931
+ (d) =>
1932
+ d.hour == null &&
1933
+ ((d.time && d.time.trim() !== '') ||
1934
+ (typeof d.timeDisplay === 'string' && d.timeDisplay.trim() !== '')) &&
1935
+ !usedTimeEntries.includes(d.time || d.timeDisplay),
1936
+ );
1937
+ const allTimeEntries = [...cervixFormFiltered, ...loadedCervixFiltered];
1938
+ if (allTimeEntries.length > 0) {
1939
+ const nextTimeEntry = allTimeEntries[0];
1940
+ if (nextTimeEntry.time && nextTimeEntry.time.trim() !== '') {
1941
+ timeValue = nextTimeEntry.time;
1942
+ } else if (
1943
+ 'timeDisplay' in nextTimeEntry &&
1944
+ typeof nextTimeEntry.timeDisplay === 'string' &&
1945
+ nextTimeEntry.timeDisplay.trim() !== ''
1946
+ ) {
1947
+ timeValue = nextTimeEntry.timeDisplay;
1948
+ } else {
1949
+ timeValue = '--:--';
1950
+ }
1951
+ usedTimeEntries.push(timeValue);
1952
+ }
1953
+ }
1954
+ }
1978
1955
 
1979
1956
  timeLabelsData.push({
1980
1957
  hours: hourLabel,
1981
1958
  time: timeValue,
1982
1959
  span: 1,
1983
1960
  });
1984
- }
1961
+ });
1985
1962
  }
1986
- // --- END LOGIC FOR CERVIX GRAPH ---
1987
1963
 
1988
1964
  const shouldRenderChart = finalChartData.length > 0;
1989
1965
 
@@ -2066,41 +2042,53 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2066
2042
  </>
2067
2043
  )}
2068
2044
  </div>
2069
- {/* --- Custom Time Labels Display for Cervix Graph only --- */}
2070
- {graph.id === 'cervix' && timeLabelsData.length > 0 && (
2045
+ {graph.id === 'cervix' && (
2071
2046
  <div
2072
2047
  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 && (
2048
+ style={{
2049
+ overflowX: timeLabelsData.length > 14 ? 'auto' : 'hidden',
2050
+ width: 'fit-content',
2051
+ maxWidth: '100%',
2052
+ margin: '1rem auto',
2053
+ WebkitOverflowScrolling: 'touch',
2054
+ border: '1px solid #e0e0e0',
2055
+ borderRadius: '4px',
2056
+ background: '#fff',
2057
+ padding: 0,
2058
+ display: 'block',
2059
+ }}>
2060
+ <table
2061
+ style={{
2062
+ borderCollapse: 'collapse',
2063
+ width: '100%',
2064
+ minWidth:
2065
+ timeLabelsData.length > 14 ? `${100 * (Math.max(6, timeLabelsData.length) + 1)}px` : '100%',
2066
+ tableLayout: 'fixed',
2067
+ }}>
2068
+ <thead>
2069
+ <tr style={{ background: '#f4f4f4', fontWeight: 700 }}>
2070
+ <th style={{ width: 100, border: '1px solid #e0e0e0', padding: 8, textAlign: 'left' }}>Hour</th>
2071
+ {Array.from({ length: Math.max(6, timeLabelsData.length) }).map((_, idx) => (
2072
+ <th
2073
+ key={`hour-col-${idx}`}
2074
+ style={{ width: 100, border: '1px solid #e0e0e0', padding: 8, textAlign: 'left' }}>
2075
+ {timeLabelsData[idx]?.hours || ''}
2076
+ </th>
2077
+ ))}
2078
+ </tr>
2079
+ </thead>
2080
+ <tbody>
2081
+ <tr style={{ background: '#fafafa' }}>
2082
+ <td style={{ width: 100, border: '1px solid #e0e0e0', padding: 8 }}>Time</td>
2083
+ {Array.from({ length: Math.max(6, timeLabelsData.length) }).map((_, idx) => (
2084
+ <td key={`time-col-${idx}`} style={{ width: 100, border: '1px solid #e0e0e0', padding: 8 }}>
2085
+ {timeLabelsData[idx]?.time || ''}
2086
+ </td>
2087
+ ))}
2088
+ </tr>
2089
+ </tbody>
2090
+ </table>
2091
+ {timeLabelsData.length > 14 && (
2104
2092
  <div
2105
2093
  style={{
2106
2094
  fontSize: '12px',
@@ -2115,7 +2103,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2115
2103
  )}
2116
2104
  </div>
2117
2105
  )}
2118
- {/* --- END Custom Time Labels --- */}
2119
2106
 
2120
2107
  {patientChartData.length > 0 && !isGraphLoading && (
2121
2108
  <div className={styles.chartStats}>
@@ -2139,7 +2126,7 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2139
2126
  <div className={styles.tableContainer}>
2140
2127
  {isGraphLoading ? (
2141
2128
  <TableSkeleton />
2142
- ) : paginatedData.length > 0 ? (
2129
+ ) : (
2143
2130
  <>
2144
2131
  <DataTable rows={paginatedData} headers={getTableHeaders(graph)}>
2145
2132
  {({ rows, headers, getTableProps, getHeaderProps, getRowProps }) => (
@@ -2155,34 +2142,43 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2155
2142
  </TableRow>
2156
2143
  </TableHead>
2157
2144
  <TableBody>
2158
- {rows.map((row) => (
2159
- <TableRow {...getRowProps({ row })} key={row.id}>
2160
- {row.cells.map((cell) => {
2161
- let cellContent = cell.value;
2162
-
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
- ) {
2169
- const cellValue = cell.value;
2170
- const status = getValueStatus(parseFloat(cellValue), graph);
2171
- const statusClass =
2172
- status === 'high' ? styles.highValue : status === 'low' ? styles.lowValue : '';
2173
- cellContent = (
2174
- <span className={statusClass}>
2175
- {cellValue}
2176
- {status === 'high' && <span className={styles.arrow}> ↑</span>}
2177
- {status === 'low' && <span className={styles.arrow}> ↓</span>}
2178
- </span>
2179
- );
2180
- }
2181
-
2182
- return <TableCell key={cell.id}>{cellContent}</TableCell>;
2183
- })}
2145
+ {rows.length > 0 ? (
2146
+ rows.map((row) => (
2147
+ <TableRow {...getRowProps({ row })} key={row.id}>
2148
+ {row.cells.map((cell) => {
2149
+ let cellContent = cell.value;
2150
+
2151
+ // Only apply value status logic for non-cervix graphs
2152
+ if (
2153
+ graph.id !== 'cervix' &&
2154
+ cell.info.header === 'value' &&
2155
+ row.cells.find((c) => c.info.header === 'value')
2156
+ ) {
2157
+ const cellValue = cell.value;
2158
+ const status = getValueStatus(parseFloat(cellValue), graph);
2159
+ const statusClass =
2160
+ status === 'high' ? styles.highValue : status === 'low' ? styles.lowValue : '';
2161
+ cellContent = (
2162
+ <span className={statusClass}>
2163
+ {cellValue}
2164
+ {status === 'high' && <span className={styles.arrow}> ↑</span>}
2165
+ {status === 'low' && <span className={styles.arrow}> ↓</span>}
2166
+ </span>
2167
+ );
2168
+ }
2169
+
2170
+ return <TableCell key={cell.id}>{cellContent}</TableCell>;
2171
+ })}
2172
+ </TableRow>
2173
+ ))
2174
+ ) : // Always render an empty row for cervix table if no data
2175
+ graph.id === 'cervix' ? (
2176
+ <TableRow>
2177
+ {getTableHeaders(graph).map((header) => (
2178
+ <TableCell key={header.key}>&nbsp;</TableCell>
2179
+ ))}
2184
2180
  </TableRow>
2185
- ))}
2181
+ ) : null}
2186
2182
  </TableBody>
2187
2183
  </Table>
2188
2184
  </TableContainer>
@@ -2215,13 +2211,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2215
2211
  </span>
2216
2212
  </div>
2217
2213
  </>
2218
- ) : (
2219
- <div className={styles.emptyState}>
2220
- <p>{t('noDataAvailable', 'No data available for this graph')}</p>
2221
- <Button kind="primary" size={controlSize} renderIcon={Add} onClick={() => handleAddDataPoint(graph.id)}>
2222
- {t('addFirstDataPoint', 'Add first data point')}
2223
- </Button>
2224
- </div>
2225
2214
  )}
2226
2215
  </div>
2227
2216
  )}
@@ -2234,7 +2223,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2234
2223
  <Layer>
2235
2224
  <Grid>
2236
2225
  <Column lg={16} md={8} sm={4}>
2237
- {/* Fetal Heart Rate Graph - Standalone */}
2238
2226
  <FetalHeartRateGraph
2239
2227
  data={computedFetalHeartRateData}
2240
2228
  tableData={getFetalHeartRateTableData()}
@@ -2246,11 +2234,13 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2246
2234
  onAddData={() => setIsFetalHeartRateFormOpen(true)}
2247
2235
  onViewModeChange={setFetalHeartRateViewMode}
2248
2236
  onPageChange={setFetalHeartRateCurrentPage}
2249
- onPageSizeChange={setFetalHeartRatePageSize}
2237
+ onPageSizeChange={(size) => {
2238
+ setFetalHeartRatePageSize(size);
2239
+ persistPageSize('fetalHeartRatePageSize', size);
2240
+ }}
2250
2241
  isAddButtonDisabled={false}
2251
2242
  />
2252
2243
 
2253
- {/* Membrane Amniotic Fluid Graph - Standalone */}
2254
2244
  <MembraneAmnioticFluidGraph
2255
2245
  data={membraneAmnioticFluidEntries}
2256
2246
  tableData={membraneAmnioticFluidEntries}
@@ -2262,16 +2252,17 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2262
2252
  onAddData={() => setIsMembraneAmnioticFluidFormOpen(true)}
2263
2253
  onViewModeChange={setMembraneAmnioticFluidViewMode}
2264
2254
  onPageChange={setMembraneAmnioticFluidCurrentPage}
2265
- onPageSizeChange={setMembraneAmnioticFluidPageSize}
2255
+ onPageSizeChange={(size) => {
2256
+ setMembraneAmnioticFluidPageSize(size);
2257
+ persistPageSize('membraneAmnioticFluidPageSize', size);
2258
+ }}
2266
2259
  isAddButtonDisabled={false}
2267
2260
  />
2268
2261
 
2269
- {/* Existing Partography Graphs - Contains Cervix Graph */}
2270
2262
  <div className={styles.partographyGrid}>
2271
2263
  {partographGraphs.map((graph, index) => renderGraph(graph, index, partographGraphs.length))}
2272
2264
  </div>
2273
2265
 
2274
- {/* Cervical Contractions Graph - Positioned below Cervix graph */}
2275
2266
  <CervicalContractionsGraph
2276
2267
  data={transformEncounterToTableData(cervicalContractionsData || [], 'uterine-contractions').map(
2277
2268
  (row, index) => ({
@@ -2291,17 +2282,13 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2291
2282
  onAddData={() => setIsCervicalContractionsFormOpen(true)}
2292
2283
  onViewModeChange={setCervicalContractionsViewMode}
2293
2284
  onPageChange={setCervicalContractionsCurrentPage}
2294
- onPageSizeChange={setCervicalContractionsPageSize}
2295
- isAddButtonDisabled={false}
2296
- patient={{
2297
- uuid: patientUuid,
2298
- name: 'Patient Name',
2299
- gender: 'F',
2300
- age: '28',
2285
+ onPageSizeChange={(size) => {
2286
+ setCervicalContractionsPageSize(size);
2287
+ persistPageSize('cervicalContractionsPageSize', size);
2301
2288
  }}
2289
+ isAddButtonDisabled={false}
2302
2290
  />
2303
2291
 
2304
- {/* Oxytocin Graph - Positioned below Cervical Contractions */}
2305
2292
  <OxytocinGraph
2306
2293
  data={loadedOxytocinData.map((item) => ({
2307
2294
  timeSlot: item.time ?? '',
@@ -2325,11 +2312,13 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2325
2312
  onAddData={() => setIsOxytocinFormOpen(true)}
2326
2313
  onViewModeChange={setOxytocinViewMode}
2327
2314
  onPageChange={setOxytocinCurrentPage}
2328
- onPageSizeChange={setOxytocinPageSize}
2315
+ onPageSizeChange={(size) => {
2316
+ setOxytocinPageSize(size);
2317
+ persistPageSize('oxytocinPageSize', size);
2318
+ }}
2329
2319
  isAddButtonDisabled={false}
2330
2320
  />
2331
2321
 
2332
- {/* Drugs and IV Fluids Graph - Positioned below Oxytocin */}
2333
2322
  <DrugsIVFluidsGraph
2334
2323
  data={getDrugsIVFluidsTableData()}
2335
2324
  tableData={getDrugsIVFluidsTableData()}
@@ -2338,22 +2327,18 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2338
2327
  pageSize={drugsIVFluidsPageSize}
2339
2328
  totalItems={getDrugsIVFluidsTableData().length}
2340
2329
  controlSize={controlSize}
2341
- onAddData={() => {}} // Form handling is done by the wrapper component
2330
+ onAddData={() => {}}
2342
2331
  onViewModeChange={setDrugsIVFluidsViewMode}
2343
2332
  onPageChange={setDrugsIVFluidsCurrentPage}
2344
- onPageSizeChange={setDrugsIVFluidsPageSize}
2345
- isAddButtonDisabled={false}
2346
- patient={{
2347
- uuid: patientUuid,
2348
- name: 'Patient Name',
2349
- gender: 'F',
2350
- age: '28',
2333
+ onPageSizeChange={(size) => {
2334
+ setDrugsIVFluidsPageSize(size);
2335
+ persistPageSize('drugsIVFluidsPageSize', size);
2351
2336
  }}
2337
+ isAddButtonDisabled={false}
2352
2338
  onDrugsIVFluidsSubmit={handleDrugsIVFluidsFormSubmit}
2353
2339
  onDataSaved={handleDrugOrderDataSaved}
2354
2340
  />
2355
2341
 
2356
- {/* Pulse and BP Graph - Positioned below Drugs and IV Fluids */}
2357
2342
  <PulseBPGraph
2358
2343
  data={getPulseBPTableData()}
2359
2344
  tableData={getPulseBPTableData()}
@@ -2365,18 +2350,14 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2365
2350
  onAddData={() => {}}
2366
2351
  onViewModeChange={setPulseBPViewMode}
2367
2352
  onPageChange={setPulseBPCurrentPage}
2368
- onPageSizeChange={setPulseBPPageSize}
2369
- isAddButtonDisabled={false}
2370
- patient={{
2371
- uuid: patientUuid,
2372
- name: 'Patient Name',
2373
- gender: 'F',
2374
- age: '28',
2353
+ onPageSizeChange={(size) => {
2354
+ setPulseBPPageSize(size);
2355
+ persistPageSize('pulseBPPageSize', size);
2375
2356
  }}
2357
+ isAddButtonDisabled={false}
2376
2358
  onPulseBPSubmit={handlePulseBPFormSubmit}
2377
2359
  />
2378
2360
 
2379
- {/* Temperature Graph - Positioned below Pulse and BP */}
2380
2361
  <TemperatureGraph
2381
2362
  data={getTemperatureTableData()}
2382
2363
  tableData={getTemperatureTableData()}
@@ -2394,11 +2375,13 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2394
2375
  }}
2395
2376
  onViewModeChange={setTemperatureViewMode}
2396
2377
  onPageChange={setTemperatureCurrentPage}
2397
- onPageSizeChange={setTemperaturePageSize}
2378
+ onPageSizeChange={(size) => {
2379
+ setTemperaturePageSize(size);
2380
+ persistPageSize('temperaturePageSize', size);
2381
+ }}
2398
2382
  isAddButtonDisabled={false}
2399
2383
  />
2400
2384
 
2401
- {/* Urine Test Graph - Now using backend data */}
2402
2385
  <UrineTestGraph
2403
2386
  data={urineTestData}
2404
2387
  tableData={urineTestData}
@@ -2410,7 +2393,10 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2410
2393
  onAddData={() => setIsUrineTestFormOpen(true)}
2411
2394
  onViewModeChange={setUrineTestViewMode}
2412
2395
  onPageChange={setUrineTestCurrentPage}
2413
- onPageSizeChange={setUrineTestPageSize}
2396
+ onPageSizeChange={(size) => {
2397
+ setUrineTestPageSize(size);
2398
+ persistPageSize('urineTestPageSize', size);
2399
+ }}
2414
2400
  isAddButtonDisabled={false}
2415
2401
  />
2416
2402
 
@@ -2432,12 +2418,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2432
2418
  selectedHours={selectedHours}
2433
2419
  existingTimeEntries={computedExistingTimeEntries}
2434
2420
  existingCervixData={existingCervixData}
2435
- patient={{
2436
- uuid: patientUuid,
2437
- name: 'Patient',
2438
- gender: 'Female',
2439
- age: '40 Years',
2440
- }}
2441
2421
  />
2442
2422
  )}
2443
2423
  {isFetalHeartRateFormOpen && (
@@ -2447,12 +2427,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2447
2427
  onSubmit={handleFetalHeartRateFormSubmit}
2448
2428
  onDataSaved={handleFetalHeartRateDataSaved}
2449
2429
  existingTimeEntries={computedExistingTimeEntries}
2450
- patient={{
2451
- uuid: patientUuid,
2452
- name: 'Patient',
2453
- gender: 'Female',
2454
- age: '40 Years',
2455
- }}
2456
2430
  />
2457
2431
  )}
2458
2432
  {isMembraneAmnioticFluidFormOpen && (
@@ -2460,12 +2434,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2460
2434
  isOpen={isMembraneAmnioticFluidFormOpen}
2461
2435
  onClose={handleMembraneAmnioticFluidFormClose}
2462
2436
  onSubmit={handleMembraneAmnioticFluidFormSubmit}
2463
- patient={{
2464
- uuid: patientUuid,
2465
- name: 'Patient',
2466
- gender: 'Female',
2467
- age: '40 Years',
2468
- }}
2469
2437
  />
2470
2438
  )}
2471
2439
  {isCervicalContractionsFormOpen && (
@@ -2473,12 +2441,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2473
2441
  isOpen={isCervicalContractionsFormOpen}
2474
2442
  onClose={handleCervicalContractionsFormClose}
2475
2443
  onSubmit={handleCervicalContractionsFormSubmit}
2476
- patient={{
2477
- uuid: patientUuid,
2478
- name: 'Patient',
2479
- gender: 'Female',
2480
- age: '40 Years',
2481
- }}
2482
2444
  />
2483
2445
  )}
2484
2446
  {isOxytocinFormOpen && (
@@ -2487,12 +2449,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2487
2449
  onClose={handleOxytocinFormClose}
2488
2450
  onSubmit={handleOxytocinFormSubmit}
2489
2451
  existingTimeEntries={existingTimeEntries}
2490
- patient={{
2491
- uuid: patientUuid,
2492
- name: 'Patient',
2493
- gender: 'F',
2494
- age: '28',
2495
- }}
2496
2452
  />
2497
2453
  )}
2498
2454
  {isTemperatureFormOpen && (
@@ -2502,12 +2458,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2502
2458
  onSubmit={handleTemperatureFormSubmit}
2503
2459
  initialTime={temperatureFormInitialTime}
2504
2460
  existingTimeEntries={existingTimeEntries}
2505
- patient={{
2506
- uuid: patientUuid,
2507
- name: 'Patient',
2508
- gender: 'F',
2509
- age: '28',
2510
- }}
2511
2461
  />
2512
2462
  )}
2513
2463
  {isUrineTestFormOpen && (
@@ -2516,12 +2466,6 @@ const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
2516
2466
  onClose={() => setIsUrineTestFormOpen(false)}
2517
2467
  onSubmit={handleUrineTestFormSubmit}
2518
2468
  existingTimeEntries={existingTimeEntries}
2519
- patient={{
2520
- uuid: patientUuid,
2521
- name: 'Patient',
2522
- gender: 'F',
2523
- age: '28',
2524
- }}
2525
2469
  />
2526
2470
  )}
2527
2471
  </Column>