@kenyaemr/esm-patient-clinical-view-app 5.4.2-pre.2763 → 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
@@ -1,25 +1,119 @@
1
- import { buildUterineContractionsObservation } from './resources/uterine-contractions.resource';
2
- import { buildBloodPressureObservation } from './resources/blood-pressure.resource';
3
- import { buildMaternalPulseObservation } from './resources/maternal-pulse.resource';
1
+ // Hook to fetch and manage membrane amniotic fluid and moulding data from OpenMRS
2
+ export function useMembraneAmnioticFluidData(patientUuid: string) {
3
+ const fetcher = (url: string) => openmrsFetch(url).then((res) => res.json());
4
+
5
+ const { data, error, isLoading, mutate } = useSWR(
6
+ patientUuid
7
+ ? `${restBaseUrl}/encounter?patient=${patientUuid}&encounterType=${MCH_PARTOGRAPHY_ENCOUNTER_UUID}&v=full&limit=100&order=desc`
8
+ : null,
9
+ fetcher,
10
+ {
11
+ onError: (error) => {
12
+ console.error('Error fetching membrane amniotic fluid data:', error);
13
+ },
14
+ revalidateOnFocus: true,
15
+ revalidateOnReconnect: true,
16
+ },
17
+ );
18
+
19
+ const membraneAmnioticFluidEntries = useMemo(() => {
20
+ try {
21
+ if (!data?.results || !Array.isArray(data.results)) {
22
+ return [];
23
+ }
24
+
25
+ const entries = [];
26
+ for (const encounter of data.results) {
27
+ if (!encounter.obs || !Array.isArray(encounter.obs)) {
28
+ continue;
29
+ }
30
+
31
+ const amnioticFluidObs = encounter.obs.find(
32
+ (obs) => obs.concept.uuid === PARTOGRAPHY_CONCEPTS['amniotic-fluid'],
33
+ );
34
+ const mouldingObs = encounter.obs.find((obs) => obs.concept.uuid === PARTOGRAPHY_CONCEPTS['moulding']);
35
+ const timeObs = encounter.obs.find(
36
+ (obs) =>
37
+ obs.concept.uuid === PARTOGRAPHY_CONCEPTS['fetal-heart-rate-time'] &&
38
+ typeof obs.value === 'string' &&
39
+ obs.value.startsWith('Time:'),
40
+ );
41
+
42
+ let time = '';
43
+ if (timeObs && typeof timeObs.value === 'string') {
44
+ const timeMatch = timeObs.value.match(/Time:\s*(.+)/);
45
+ if (timeMatch) {
46
+ time = timeMatch[1].trim();
47
+ }
48
+ }
49
+
50
+ if (amnioticFluidObs || mouldingObs) {
51
+ entries.push({
52
+ id: `maf-${encounter.uuid}`,
53
+ uuid: encounter.uuid,
54
+ amnioticFluid: amnioticFluidObs?.value?.display || amnioticFluidObs?.value || '',
55
+ moulding: mouldingObs?.value?.display || mouldingObs?.value || '',
56
+ time,
57
+ date: new Date(encounter.encounterDatetime).toLocaleDateString(),
58
+ encounterDatetime: encounter.encounterDatetime,
59
+ });
60
+ }
61
+ }
62
+ return entries;
63
+ } catch (error) {
64
+ console.error('Error processing membrane amniotic fluid data:', error);
65
+ return [];
66
+ }
67
+ }, [data]);
68
+
69
+ return {
70
+ membraneAmnioticFluidEntries,
71
+ isLoading,
72
+ error,
73
+ mutate,
74
+ };
75
+ }
76
+
77
+ // Function to save membrane amniotic fluid and moulding data to OpenMRS
78
+ export async function saveMembraneAmnioticFluidData(
79
+ patientUuid: string,
80
+ formData: { amnioticFluid: string; moulding: string; time: string },
81
+ t: unknown,
82
+ locationUuid?: string,
83
+ providerUuid?: string,
84
+ ) {
85
+ try {
86
+ const result = await createPartographyEncounter(
87
+ patientUuid,
88
+ 'membrane-amniotic-fluid',
89
+ formData,
90
+ locationUuid,
91
+ providerUuid,
92
+ );
93
+ if (result?.success && result?.encounter) {
94
+ return result;
95
+ } else {
96
+ throw new Error(result?.message || 'Failed to save membrane amniotic fluid data');
97
+ }
98
+ } catch (error) {
99
+ console.error('Error saving membrane amniotic fluid data:', error);
100
+ throw new Error(error?.message || 'Failed to save membrane amniotic fluid data');
101
+ }
102
+ }
4
103
  import { openmrsFetch, restBaseUrl, toOmrsIsoString, useConfig } from '@openmrs/esm-framework';
5
104
  import useSWR from 'swr';
6
105
  import React, { useState, useEffect, useMemo } from 'react';
7
- import { useTranslation } from 'react-i18next';
8
106
  import {
9
107
  PARTOGRAPHY_CONCEPTS,
10
108
  PARTOGRAPHY_ENCOUNTER_TYPES,
109
+ MCH_PARTOGRAPHY_ENCOUNTER_UUID,
11
110
  getPartographyUnit,
12
111
  type OpenMRSResponse,
13
112
  type PartographyObservation,
14
113
  type PartographyEncounter,
15
114
  type PartographyGraphType,
16
- CONTRACTION_INTENSITY_OPTIONS,
17
115
  } from './types';
18
116
  import { configSchema, type ConfigObject } from '../../config-schema';
19
- import {
20
- buildTemperatureObservation,
21
- transformTemperatureEncounterToChartData,
22
- } from './resources/temperature.resource';
23
117
 
24
118
  export type { PartographyObservation, PartographyEncounter };
25
119
  const defaultPartographyConfig = configSchema.partography._default;
@@ -201,12 +295,8 @@ export function usePartographyEncounters(patientUuid: string, graphType: string)
201
295
  export function usePartographyData(patientUuid: string, graphType: string) {
202
296
  const { encounters, isLoading, error, mutate } = usePartographyEncounters(patientUuid, graphType);
203
297
 
204
- const shouldUseLocalFallback = graphType !== 'fetal-heart-rate';
205
-
206
298
  const localDataFallback = useSWR(
207
- !isLoading && encounters.length === 0 && shouldUseLocalFallback
208
- ? `partography_local_${patientUuid}_${graphType}`
209
- : null,
299
+ !isLoading && encounters.length === 0 ? `partography_local_${patientUuid}_${graphType}` : null,
210
300
  () => {
211
301
  const localData = loadPartographyData(patientUuid, graphType);
212
302
  return localData;
@@ -232,6 +322,7 @@ function loadPartographyData(patientUuid: string, graphType: string): Partograph
232
322
  try {
233
323
  const storageKey = generateStorageKey(patientUuid, graphType);
234
324
  const localData = JSON.parse(localStorage.getItem(storageKey) || '[]');
325
+
235
326
  const localEncounters = localData.map((item) => ({
236
327
  uuid: item.id,
237
328
  encounterDatetime: item.timestamp,
@@ -253,10 +344,11 @@ function loadPartographyData(patientUuid: string, graphType: string): Partograph
253
344
  },
254
345
  ],
255
346
  }));
347
+
256
348
  return localEncounters.sort(
257
349
  (a, b) => new Date(b.encounterDatetime).getTime() - new Date(a.encounterDatetime).getTime(),
258
350
  );
259
- } catch {
351
+ } catch (e) {
260
352
  return [];
261
353
  }
262
354
  }
@@ -269,17 +361,26 @@ export async function createPartographyEncounter(
269
361
  providerUuid?: string,
270
362
  t?: (key: string, fallback?: string) => string,
271
363
  ): Promise<{ success: boolean; message: string; encounter?: PartographyEncounter }> {
272
- const translate = t || ((key, fallback) => fallback || key);
273
364
  try {
274
365
  const observations = buildObservations(graphType, formData);
366
+
275
367
  if (observations.length === 0) {
276
- throw new Error(translate('noValidObservations', 'No valid observations to save'));
368
+ throw new Error(t?.('noValidObservations', 'No valid observations to save') || 'No valid observations to save');
277
369
  }
370
+
278
371
  const encounterTypeUuid = await getEncounterTypeForGraph(graphType);
279
372
  if (!encounterTypeUuid) {
280
- throw new Error(translate('noEncounterTypeFound', `No encounter type found for graph: ${graphType}`));
373
+ throw new Error(
374
+ t?.('noEncounterTypeFound', 'No encounter type found for graph: {{graphType}}')?.replace(
375
+ '{{graphType}}',
376
+ graphType,
377
+ ) || `No encounter type found for graph: ${graphType}`,
378
+ );
281
379
  }
380
+
282
381
  let finalLocationUuid = locationUuid;
382
+
383
+ // If no location provided, try to get from session or use default
283
384
  if (!finalLocationUuid) {
284
385
  try {
285
386
  const sessionResponse = await openmrsFetch(`${restBaseUrl}/session`);
@@ -289,13 +390,19 @@ export async function createPartographyEncounter(
289
390
  finalLocationUuid = sessionData.sessionLocation.uuid;
290
391
  }
291
392
  }
292
- } catch {}
393
+ } catch (error) {
394
+ console.warn('Could not get session location:', error);
395
+ }
396
+
397
+ // Fallback to default if still no location
293
398
  if (!finalLocationUuid) {
294
399
  finalLocationUuid = getDefaultLocationUuid();
295
400
  }
296
401
  }
402
+
297
403
  const timeConfig = getTimeConfig();
298
404
  const encounterDatetime = toOmrsIsoString(new Date(Date.now() - timeConfig.defaultEncounterOffset));
405
+
299
406
  const encounterPayload: any = {
300
407
  patient: patientUuid,
301
408
  location: finalLocationUuid,
@@ -303,7 +410,9 @@ export async function createPartographyEncounter(
303
410
  obs: observations,
304
411
  encounterType: encounterTypeUuid,
305
412
  };
413
+
306
414
  let finalProviderUuid = providerUuid;
415
+
307
416
  if (!finalProviderUuid) {
308
417
  try {
309
418
  const sessionResponse = await openmrsFetch(`${restBaseUrl}/session`);
@@ -322,7 +431,8 @@ export async function createPartographyEncounter(
322
431
  }
323
432
  }
324
433
  }
325
- } catch {}
434
+ } catch (error) {}
435
+
326
436
  if (!finalProviderUuid) {
327
437
  try {
328
438
  const anyProviderResponse = await openmrsFetch(`${restBaseUrl}/provider?v=default&limit=1`);
@@ -332,9 +442,10 @@ export async function createPartographyEncounter(
332
442
  finalProviderUuid = anyProviderData.results[0].uuid;
333
443
  }
334
444
  }
335
- } catch {}
445
+ } catch (error) {}
336
446
  }
337
447
  }
448
+
338
449
  if (finalProviderUuid) {
339
450
  try {
340
451
  const providerValidationResponse = await openmrsFetch(`${restBaseUrl}/provider/${finalProviderUuid}?v=default`);
@@ -347,25 +458,32 @@ export async function createPartographyEncounter(
347
458
  },
348
459
  ];
349
460
  }
350
- } catch {}
461
+ } catch (validationError) {}
351
462
  }
463
+
464
+ // Validate required fields before making API call
352
465
  if (!patientUuid) {
353
- throw new Error(translate('Patient UUID is required'));
466
+ throw new Error('Patient UUID is required');
354
467
  }
355
468
  if (!finalLocationUuid) {
356
- throw new Error(translate('Location UUID is required'));
469
+ throw new Error('Location UUID is required');
357
470
  }
358
471
  if (!encounterTypeUuid) {
359
- throw new Error(translate('Encounter type UUID is required'));
472
+ throw new Error('Encounter type UUID is required');
360
473
  }
361
474
  if (!observations || observations.length === 0) {
362
- throw new Error(translate('At least one observation is required'));
475
+ throw new Error('At least one observation is required');
363
476
  }
477
+
478
+ // Add debugging to see what payload is being sent
479
+ // Debug: encounterPayload being sent
480
+
364
481
  const response = await openmrsFetch(`${restBaseUrl}/encounter`, {
365
482
  method: 'POST',
366
483
  headers: { 'Content-Type': 'application/json' },
367
484
  body: JSON.stringify(encounterPayload),
368
485
  });
486
+
369
487
  if (!response.ok) {
370
488
  let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
371
489
  try {
@@ -381,26 +499,16 @@ export async function createPartographyEncounter(
381
499
  errorMessage += ` (${detailedError.error.detail})`;
382
500
  }
383
501
  }
384
- } catch {
502
+ } catch (parseError) {
385
503
  errorMessage += ` - Raw Response: ${responseText}`;
386
504
  }
387
505
  }
388
- } catch {}
389
- // eslint-disable-next-line no-console
390
- console.error('Encounter POST failed', { status: response.status, statusText: response.statusText });
391
- try {
392
- const raw = await response.text();
393
- // eslint-disable-next-line no-console
394
- console.error('Encounter POST response body:', raw);
395
- if (raw) {
396
- errorMessage += ` - Response Body: ${raw}`;
397
- }
398
- } catch (e) {
399
- // ignore
400
- }
506
+ } catch (e) {}
401
507
  throw new Error(errorMessage);
402
508
  }
509
+
403
510
  const encounter = await response.json();
511
+
404
512
  const cacheKey = generateCacheKey(patientUuid, graphType);
405
513
  try {
406
514
  const { mutate: globalMutate } = await import('swr');
@@ -409,22 +517,23 @@ export async function createPartographyEncounter(
409
517
  setTimeout(async () => {
410
518
  await globalMutate(cacheKey);
411
519
  }, timeConfig.cacheInvalidationDelay);
412
- } catch {}
520
+ } catch (mutateError) {}
521
+
413
522
  return {
414
523
  success: true,
415
- message: translate('partographyDataSavedSuccessfully', 'Partography data saved successfully'),
524
+ message:
525
+ t?.('partographyDataSavedSuccessfully', 'Partography data saved successfully') ||
526
+ 'Partography data saved successfully',
416
527
  encounter,
417
528
  };
418
- } catch (error: any) {
419
- // Log error details for debugging
420
- // eslint-disable-next-line no-console
421
- console.error('createPartographyEncounter error:', error);
529
+ } catch (error) {
422
530
  return {
423
531
  success: false,
424
- message: translate(
425
- 'failedToSavePartographyData',
426
- `Failed to save partography data: ${error && error.message ? error.message : String(error)}`,
427
- ),
532
+ message:
533
+ t?.('failedToSavePartographyData', 'Failed to save partography data: {{error}}')?.replace(
534
+ '{{error}}',
535
+ error.message,
536
+ ) || `Failed to save partography data: ${error.message}`,
428
537
  };
429
538
  }
430
539
  }
@@ -465,6 +574,29 @@ function buildObservations(graphType: string, formData: any): any[] {
465
574
  const obsDatetime = toOmrsIsoString(new Date(Date.now() - timeConfig.defaultEncounterOffset));
466
575
 
467
576
  switch (graphType) {
577
+ case 'pulse-bp-combined':
578
+ if (formData.pulse) {
579
+ observations.push({
580
+ concept: PARTOGRAPHY_CONCEPTS['maternal-pulse'],
581
+ value: parseFloat(formData.pulse),
582
+ obsDatetime,
583
+ });
584
+ }
585
+ if (formData.systolic) {
586
+ observations.push({
587
+ concept: PARTOGRAPHY_CONCEPTS['systolic-bp'],
588
+ value: parseFloat(formData.systolic),
589
+ obsDatetime,
590
+ });
591
+ }
592
+ if (formData.diastolic) {
593
+ observations.push({
594
+ concept: PARTOGRAPHY_CONCEPTS['diastolic-bp'],
595
+ value: parseFloat(formData.diastolic),
596
+ obsDatetime,
597
+ });
598
+ }
599
+ break;
468
600
  case 'membrane-amniotic-fluid':
469
601
  if (formData.time) {
470
602
  observations.push({
@@ -473,26 +605,38 @@ function buildObservations(graphType: string, formData: any): any[] {
473
605
  obsDatetime,
474
606
  });
475
607
  }
476
- const amnioticFluidMap = defaultPartographyConfig.amnioticFluidMap;
608
+
609
+ const amnioticFluidMap = {
610
+ 'Membrane intact': '164899AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
611
+ 'Clear liquor': '159484AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
612
+ 'Meconium Stained': '134488AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
613
+ Absent: '163747AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
614
+ 'Blood Stained': '1077AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
615
+ };
477
616
  let amnioticFluidValue = formData.amnioticFluid;
478
617
  if (amnioticFluidValue && amnioticFluidMap[amnioticFluidValue]) {
479
618
  amnioticFluidValue = amnioticFluidMap[amnioticFluidValue];
480
619
  }
481
620
  if (amnioticFluidValue) {
482
621
  observations.push({
483
- concept: PARTOGRAPHY_CONCEPTS['amniotic-fluid'],
622
+ concept: '162653AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
484
623
  value: amnioticFluidValue,
485
624
  obsDatetime,
486
625
  });
487
626
  }
488
- const mouldingMap = defaultPartographyConfig.mouldingMap;
627
+ const mouldingMap = {
628
+ '0': '1107AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
629
+ '+': '1362AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
630
+ '++': '1363AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
631
+ '+++': '1364AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
632
+ };
489
633
  let mouldingValue = formData.moulding;
490
634
  if (mouldingValue && mouldingMap[mouldingValue]) {
491
635
  mouldingValue = mouldingMap[mouldingValue];
492
636
  }
493
637
  if (mouldingValue) {
494
638
  observations.push({
495
- concept: PARTOGRAPHY_CONCEPTS['moulding'],
639
+ concept: '166527AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
496
640
  value: mouldingValue,
497
641
  obsDatetime,
498
642
  });
@@ -507,21 +651,23 @@ function buildObservations(graphType: string, formData: any): any[] {
507
651
  });
508
652
  }
509
653
 
654
+ // Save hour as a number (not string)
510
655
  if (formData.hour !== undefined && formData.hour !== '') {
511
656
  try {
512
657
  const hourValue = parseFloat(formData.hour);
513
-
514
658
  if (hourValue >= 0 && hourValue <= 24) {
515
- const hourText = `Hour: ${hourValue}`;
516
659
  observations.push({
517
660
  concept: PARTOGRAPHY_CONCEPTS['fetal-heart-rate-hour'],
518
- value: hourText,
661
+ value: hourValue,
519
662
  obsDatetime,
520
663
  });
521
664
  }
522
- } catch (error) {}
665
+ } catch (error) {
666
+ console.warn('Skipping hour observation due to validation error:', error);
667
+ }
523
668
  }
524
669
 
670
+ // Using text concept for time (should work fine)
525
671
  if (formData.time) {
526
672
  try {
527
673
  observations.push({
@@ -529,7 +675,9 @@ function buildObservations(graphType: string, formData: any): any[] {
529
675
  value: `Time: ${formData.time}`,
530
676
  obsDatetime,
531
677
  });
532
- } catch (error) {}
678
+ } catch (error) {
679
+ console.warn('Skipping time observation due to validation error:', error);
680
+ }
533
681
  }
534
682
  break;
535
683
 
@@ -559,12 +707,14 @@ function buildObservations(graphType: string, formData: any): any[] {
559
707
  }
560
708
  break;
561
709
 
562
- case 'descent-of-head': {
710
+ case 'descent-of-head':
563
711
  if (formData.value || formData.measurementValue) {
564
712
  let conceptValue = formData.value || formData.measurementValue;
713
+
565
714
  if (formData.conceptUuid) {
566
715
  conceptValue = formData.conceptUuid;
567
716
  }
717
+
568
718
  observations.push({
569
719
  concept: PARTOGRAPHY_CONCEPTS['descent-of-head'],
570
720
  value: conceptValue,
@@ -572,86 +722,77 @@ function buildObservations(graphType: string, formData: any): any[] {
572
722
  });
573
723
  }
574
724
  break;
575
- }
576
- case 'pulse-bp-combined': {
577
- // Save both maternal pulse and blood pressure observations
578
- const pulseObs = buildMaternalPulseObservation({ value: formData.pulse });
579
- const bpObs = buildBloodPressureObservation({ systolic: formData.systolic, diastolic: formData.diastolic });
580
- return [...pulseObs, ...bpObs];
581
- }
582
- case 'uterine-contractions': {
583
- // Build the primary numeric observation for uterine contractions
584
- const uterineObs = buildUterineContractionsObservation(formData) || [];
585
- // Allow additional related observations: contraction count, intensity, and time-slot
586
- const extra: any[] = [];
587
-
588
- if (formData.contractionCount !== undefined && formData.contractionCount !== null) {
589
- extra.push({
590
- concept: PARTOGRAPHY_CONCEPTS['contraction-count'],
591
- value: formData.contractionCount,
725
+
726
+ case 'uterine-contractions':
727
+ if (formData.contractionCount !== undefined) {
728
+ observations.push({
729
+ concept: '159682AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
730
+ value: parseInt(formData.contractionCount, 10),
592
731
  obsDatetime,
593
732
  });
594
733
  }
595
- // Defensive normalization: sometimes the UI mistakenly provides the
596
- // question concept UUID (uterine-contractions) instead of the answer
597
- // concept UUID. Try to map common shapes to a valid answer UUID and
598
- // only send the coded obs when we have a reasonable answer UUID.
599
- if (formData.contractionLevelUuid) {
600
- let intensityUuid = String(formData.contractionLevelUuid);
601
-
602
- // If the provided value equals the question concept UUID, attempt to
603
- // resolve the intended answer using other form fields (value/label).
604
- if (intensityUuid === PARTOGRAPHY_CONCEPTS['uterine-contractions']) {
605
- // Try mapping from any provided label or option value
606
- const candidate =
607
- CONTRACTION_INTENSITY_OPTIONS.find((opt) => opt.value === String(formData.contractionLevelValue)) ||
608
- CONTRACTION_INTENSITY_OPTIONS.find((opt) => opt.conceptUuid === String(formData.contractionLevelValue));
609
- if (candidate && candidate.conceptUuid) {
610
- intensityUuid = candidate.conceptUuid;
611
- }
612
- }
734
+ if (formData.contractionLevel) {
735
+ observations.push({
736
+ concept: '163750AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
737
+ value: formData.contractionLevel, // UUID string for coded value
738
+ obsDatetime,
739
+ });
740
+ }
741
+ if (formData.timeSlot) {
742
+ observations.push({
743
+ concept: '160632AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
744
+ value: `Time: ${formData.timeSlot}`,
745
+ obsDatetime,
746
+ });
747
+ }
748
+ break;
613
749
 
614
- // If intensityUuid still looks like the question UUID or is empty,
615
- // skip sending it to avoid server conversion errors.
616
- if (intensityUuid && intensityUuid !== PARTOGRAPHY_CONCEPTS['uterine-contractions']) {
617
- // Send coded obs as the answer UUID string. Some OpenMRS instances
618
- // accept either a plain UUID string or an object; prefer string to
619
- // avoid server-side conversion issues seen in some deployments.
620
- extra.push({
621
- concept:
622
- PARTOGRAPHY_CONCEPTS['uterine-contraction-duration'] || PARTOGRAPHY_CONCEPTS['uterine-contractions'],
623
- value: intensityUuid,
624
- obsDatetime,
625
- });
626
- } else {
627
- // eslint-disable-next-line no-console
628
- console.warn(
629
- 'Skipping contraction intensity obs due to invalid/mismatched UUID',
630
- formData.contractionLevelUuid,
631
- );
632
- }
750
+ case 'maternal-pulse':
751
+ if (formData.value || formData.measurementValue) {
752
+ observations.push({
753
+ concept: PARTOGRAPHY_CONCEPTS['maternal-pulse'],
754
+ value: parseFloat(formData.value || formData.measurementValue),
755
+ obsDatetime,
756
+ });
633
757
  }
758
+ break;
634
759
 
635
- if (formData.timeSlot) {
636
- extra.push({
637
- concept: PARTOGRAPHY_CONCEPTS['time-slot'] || PARTOGRAPHY_CONCEPTS['event-description'],
638
- value: `TimeSlot: ${formData.timeSlot}`,
760
+ case 'blood-pressure':
761
+ if (formData.systolic) {
762
+ observations.push({
763
+ concept: PARTOGRAPHY_CONCEPTS['systolic-bp'],
764
+ value: parseFloat(formData.systolic),
765
+ obsDatetime,
766
+ });
767
+ }
768
+ if (formData.diastolic) {
769
+ observations.push({
770
+ concept: PARTOGRAPHY_CONCEPTS['diastolic-bp'],
771
+ value: parseFloat(formData.diastolic),
639
772
  obsDatetime,
640
773
  });
641
774
  }
775
+ break;
642
776
 
643
- return [...uterineObs, ...extra];
644
- }
645
- case 'maternal-pulse': {
646
- return [];
647
- }
648
- case 'blood-pressure': {
649
- return [];
650
- }
651
- case 'temperature': {
652
- return buildTemperatureObservation(formData);
653
- }
654
- case 'urine-analysis': {
777
+ case 'temperature':
778
+ if (formData.value || formData.measurementValue) {
779
+ observations.push({
780
+ concept: PARTOGRAPHY_CONCEPTS['temperature'],
781
+ value: parseFloat(formData.value || formData.measurementValue),
782
+ obsDatetime,
783
+ });
784
+ }
785
+ // Save time observation if provided
786
+ if (formData.time) {
787
+ observations.push({
788
+ concept: '160632AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
789
+ value: `Time: ${formData.time}`,
790
+ obsDatetime,
791
+ });
792
+ }
793
+ break;
794
+
795
+ case 'urine-analysis':
655
796
  if (formData.proteinLevel) {
656
797
  observations.push({
657
798
  concept: PARTOGRAPHY_CONCEPTS['protein-level'],
@@ -673,8 +814,29 @@ function buildObservations(graphType: string, formData: any): any[] {
673
814
  obsDatetime,
674
815
  });
675
816
  }
817
+ if (formData.volume !== undefined && formData.volume !== null && formData.volume !== '') {
818
+ observations.push({
819
+ concept: PARTOGRAPHY_CONCEPTS['urine-volume'],
820
+ value: formData.volume,
821
+ obsDatetime,
822
+ });
823
+ }
824
+ // Remove time-slot obs to avoid OpenMRS errors
825
+ if (formData.eventDescription) {
826
+ observations.push({
827
+ concept: PARTOGRAPHY_CONCEPTS['event-description'],
828
+ value: formData.eventDescription,
829
+ obsDatetime,
830
+ });
831
+ }
832
+ if (formData.timeResultsReturned) {
833
+ observations.push({
834
+ concept: PARTOGRAPHY_CONCEPTS['event-description'],
835
+ value: `Results Returned: ${formData.timeResultsReturned}`,
836
+ obsDatetime,
837
+ });
838
+ }
676
839
  break;
677
- }
678
840
 
679
841
  case 'drugs-fluids':
680
842
  if (formData.medication || formData.drugName) {
@@ -691,17 +853,18 @@ function buildObservations(graphType: string, formData: any): any[] {
691
853
  obsDatetime,
692
854
  });
693
855
  }
856
+ // Add route and frequency as event descriptions
694
857
  if (formData.route) {
695
858
  observations.push({
696
- concept: PARTOGRAPHY_CONCEPTS['route'] || PARTOGRAPHY_CONCEPTS['event-description'],
697
- value: formData.route,
859
+ concept: PARTOGRAPHY_CONCEPTS['event-description'],
860
+ value: `Route: ${formData.route}`,
698
861
  obsDatetime,
699
862
  });
700
863
  }
701
864
  if (formData.frequency) {
702
865
  observations.push({
703
- concept: PARTOGRAPHY_CONCEPTS['frequency'] || PARTOGRAPHY_CONCEPTS['event-description'],
704
- value: formData.frequency,
866
+ concept: PARTOGRAPHY_CONCEPTS['event-description'],
867
+ value: `Frequency: ${formData.frequency}`,
705
868
  obsDatetime,
706
869
  });
707
870
  }
@@ -741,11 +904,137 @@ function buildObservations(graphType: string, formData: any): any[] {
741
904
  return observations;
742
905
  }
743
906
 
744
- export function transformEncounterToChartData(encounters: any[], graphType: string): any[] {
745
- if (graphType === 'temperature') {
746
- return transformTemperatureEncounterToChartData(encounters);
747
- }
748
- return [];
907
+ export function transformEncounterToChartData(encounters: PartographyEncounter[], graphType: string): any[] {
908
+ const chartData = [];
909
+
910
+ encounters.forEach((encounter) => {
911
+ const encounterTime = new Date(encounter.encounterDatetime).toLocaleTimeString('en-US', {
912
+ hour: '2-digit',
913
+ minute: '2-digit',
914
+ hour12: false,
915
+ });
916
+
917
+ encounter.obs?.forEach((obs) => {
918
+ try {
919
+ if (typeof obs.value === 'string' && obs.value.startsWith('{')) {
920
+ const parsedData = JSON.parse(obs.value);
921
+ if (parsedData.graphType === graphType && parsedData.data) {
922
+ const formData = parsedData.data;
923
+ const value = formData.value || formData.measurementValue;
924
+ if (value) {
925
+ const dataPoint = {
926
+ group: getGraphTypeDisplayName(graphType),
927
+ time: formData.time || encounterTime,
928
+ value: parseFloat(value),
929
+ };
930
+ chartData.push(dataPoint);
931
+ }
932
+ }
933
+ } else {
934
+ const conceptUuid = obs.concept.uuid;
935
+ let value = null;
936
+ let groupName = '';
937
+ // Robustly handle uterine contractions: both numeric and coded contraction level
938
+ if (graphType === 'uterine-contractions') {
939
+ // Numeric value for graphing
940
+ if (conceptUuid === PARTOGRAPHY_CONCEPTS['uterine-contractions']) {
941
+ value = parseFloat(obs.value as string);
942
+ groupName = getGraphTypeDisplayName('uterine-contractions');
943
+ }
944
+ // Coded contraction level (none, mild, moderate, strong)
945
+ const contractionLevelMap: Record<string, string> = {
946
+ '1107AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA': 'none',
947
+ '1498AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA': 'mild',
948
+ '1499AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA': 'moderate',
949
+ '166788AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA': 'strong',
950
+ };
951
+ if (contractionLevelMap[conceptUuid]) {
952
+ // For charting, you may want to assign a numeric value for each level
953
+ const contractionLevelValueMap: Record<string, number> = {
954
+ none: 0,
955
+ mild: 1,
956
+ moderate: 2,
957
+ strong: 3,
958
+ };
959
+ value = contractionLevelValueMap[contractionLevelMap[conceptUuid]];
960
+ groupName = contractionLevelMap[conceptUuid];
961
+ }
962
+ } else {
963
+ switch (conceptUuid) {
964
+ case PARTOGRAPHY_CONCEPTS['fetal-heart-rate']:
965
+ if (graphType === 'fetal-heart-rate') {
966
+ value = parseFloat(obs.value as string);
967
+ groupName = getGraphTypeDisplayName('fetal-heart-rate');
968
+ }
969
+ break;
970
+ case PARTOGRAPHY_CONCEPTS['cervical-dilation']:
971
+ if (graphType === 'cervical-dilation') {
972
+ value = parseFloat(obs.value as string);
973
+ groupName = getGraphTypeDisplayName('cervical-dilation');
974
+ }
975
+ break;
976
+ case PARTOGRAPHY_CONCEPTS['descent-of-head']:
977
+ if (graphType === 'descent-of-head') {
978
+ value = getStationValue(obs.value as string) ?? parseFloat(obs.value as string);
979
+ groupName = getGraphTypeDisplayName('descent-of-head');
980
+ }
981
+ break;
982
+ case PARTOGRAPHY_CONCEPTS['maternal-pulse']:
983
+ if (graphType === 'maternal-pulse') {
984
+ value = parseFloat(obs.value as string);
985
+ groupName = getGraphTypeDisplayName('maternal-pulse');
986
+ }
987
+ break;
988
+ case PARTOGRAPHY_CONCEPTS['systolic-bp']:
989
+ if (graphType === 'blood-pressure') {
990
+ value = parseFloat(obs.value as string);
991
+ groupName = 'Systolic';
992
+ }
993
+ break;
994
+ case PARTOGRAPHY_CONCEPTS['diastolic-bp']:
995
+ if (graphType === 'blood-pressure') {
996
+ value = parseFloat(obs.value as string);
997
+ groupName = 'Diastolic';
998
+ }
999
+ break;
1000
+ case PARTOGRAPHY_CONCEPTS['temperature']:
1001
+ if (graphType === 'temperature') {
1002
+ value = parseFloat(obs.value as string);
1003
+ groupName = getGraphTypeDisplayName('temperature');
1004
+ }
1005
+ break;
1006
+ default:
1007
+ if (typeof obs.value === 'number' || !isNaN(parseFloat(obs.value as string))) {
1008
+ value = parseFloat(obs.value as string);
1009
+ groupName = getGraphTypeDisplayName(graphType);
1010
+ }
1011
+ break;
1012
+ }
1013
+ }
1014
+ if (value !== null && groupName) {
1015
+ const dataPoint = {
1016
+ group: groupName,
1017
+ time: encounterTime,
1018
+ value: value,
1019
+ };
1020
+ chartData.push(dataPoint);
1021
+ }
1022
+ }
1023
+ } catch (e) {}
1024
+ });
1025
+ });
1026
+
1027
+ return chartData.sort((a, b) => {
1028
+ // Parse time strings (HH:MM format) for comparison
1029
+ const timeA = a.time.split(':').map(Number);
1030
+ const timeB = b.time.split(':').map(Number);
1031
+
1032
+ // Convert to minutes for easy comparison
1033
+ const minutesA = timeA[0] * 60 + (timeA[1] || 0);
1034
+ const minutesB = timeB[0] * 60 + (timeB[1] || 0);
1035
+
1036
+ return minutesA - minutesB;
1037
+ });
749
1038
  }
750
1039
 
751
1040
  function getGroupNameForGraph(graphType: string): string {
@@ -814,118 +1103,387 @@ export function transformEncounterToTableData(
814
1103
  hour12: false,
815
1104
  })}`;
816
1105
 
817
- encounter.obs.forEach((obs, obsIndex) => {
818
- try {
819
- if (typeof obs.value === 'string') {
820
- const parsedData = JSON.parse(obs.value);
821
-
822
- if (parsedData.graphType === graphType && parsedData.data) {
823
- const formData = parsedData.data;
824
- let value = formData.value || formData.measurementValue;
825
-
826
- if (graphType === 'descent-of-head' && formData.conceptUuid) {
827
- value = formData.conceptUuid;
1106
+ if (graphType === 'uterine-contractions') {
1107
+ // Group obs by time for uterine contractions
1108
+ let timeSlot = '';
1109
+ let contractionCount = '';
1110
+ let contractionLevel = 'none';
1111
+ if (Array.isArray(encounter.obs)) {
1112
+ // Always use the last seen contraction level in the encounter (in case order is not guaranteed)
1113
+ for (const obs of encounter.obs) {
1114
+ // Time slot
1115
+ if (
1116
+ obs.concept.uuid === '160632AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' &&
1117
+ typeof obs.value === 'string' &&
1118
+ obs.value.startsWith('Time:')
1119
+ ) {
1120
+ const match = obs.value.match(/Time:\s*(.+)/);
1121
+ if (match) {
1122
+ timeSlot = match[1].trim();
828
1123
  }
829
-
830
- if (value) {
831
- let displayValue;
832
-
833
- if (graphType === 'descent-of-head') {
834
- let valueToMap;
835
- if (typeof value === 'object' && value !== null) {
836
- const valueObj = value as any;
837
- if (valueObj.conceptUuid) {
838
- valueToMap = valueObj.conceptUuid;
839
- } else if (valueObj.value) {
840
- valueToMap = valueObj.value;
1124
+ }
1125
+ // Contraction count (numeric)
1126
+ if (obs.concept.uuid === '159682AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') {
1127
+ contractionCount = String(obs.value);
1128
+ }
1129
+ }
1130
+ // Now, after looping, get the latest contraction level (if any)
1131
+ for (const obs of encounter.obs) {
1132
+ if (obs.concept.uuid === '163750AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') {
1133
+ let levelUuid = '';
1134
+ if (typeof obs.value === 'string') {
1135
+ levelUuid = obs.value;
1136
+ } else if (obs.value != null && typeof obs.value === 'object' && (obs.value as any)?.uuid) {
1137
+ levelUuid = (obs.value as any).uuid;
1138
+ }
1139
+ if (levelUuid === '1107AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') {
1140
+ contractionLevel = 'none';
1141
+ }
1142
+ if (levelUuid === '1498AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') {
1143
+ contractionLevel = 'mild';
1144
+ }
1145
+ if (levelUuid === '1499AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') {
1146
+ contractionLevel = 'moderate';
1147
+ }
1148
+ if (levelUuid === '166788AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') {
1149
+ contractionLevel = 'strong';
1150
+ }
1151
+ }
1152
+ }
1153
+ }
1154
+ if (contractionCount || contractionLevel !== 'none') {
1155
+ tableData.push({
1156
+ id: `${graphType}-${index}`,
1157
+ dateTime,
1158
+ timeSlot,
1159
+ contractionCount,
1160
+ contractionLevel,
1161
+ unit: getUnitForGraphType(graphType),
1162
+ });
1163
+ }
1164
+ } else {
1165
+ // ...existing code for other graph types...
1166
+ encounter.obs.forEach((obs, obsIndex) => {
1167
+ try {
1168
+ if (typeof obs.value === 'string') {
1169
+ const parsedData = JSON.parse(obs.value);
1170
+ if (parsedData.graphType === graphType && parsedData.data) {
1171
+ const formData = parsedData.data;
1172
+ let value = formData.value || formData.measurementValue;
1173
+ if (graphType === 'descent-of-head' && formData.conceptUuid) {
1174
+ value = formData.conceptUuid;
1175
+ }
1176
+ if (value) {
1177
+ let displayValue;
1178
+ if (graphType === 'descent-of-head') {
1179
+ let valueToMap;
1180
+ if (typeof value === 'object' && value !== null) {
1181
+ const valueObj = value as any;
1182
+ if (valueObj.conceptUuid) {
1183
+ valueToMap = valueObj.conceptUuid;
1184
+ } else if (valueObj.value) {
1185
+ valueToMap = valueObj.value;
1186
+ } else {
1187
+ valueToMap = String(value);
1188
+ }
841
1189
  } else {
842
1190
  valueToMap = String(value);
843
1191
  }
1192
+ displayValue = getStationDisplay(valueToMap);
844
1193
  } else {
845
- valueToMap = String(value);
846
- }
847
-
848
- displayValue = getStationDisplay(valueToMap);
849
- } else {
850
- const numericValue = parseFloat(String(value));
851
- if (!isNaN(numericValue)) {
852
- displayValue = numericValue.toFixed(1);
853
- } else {
854
- displayValue = String(value);
1194
+ const numericValue = parseFloat(String(value));
1195
+ if (!isNaN(numericValue)) {
1196
+ displayValue = numericValue.toFixed(1);
1197
+ } else {
1198
+ displayValue = String(value);
1199
+ }
855
1200
  }
1201
+ const rowData = {
1202
+ id: `${graphType}-${index}-${obsIndex}`,
1203
+ dateTime,
1204
+ value: displayValue,
1205
+ unit: getUnitForGraphType(graphType),
1206
+ };
1207
+ tableData.push(rowData);
856
1208
  }
857
-
858
- const rowData = {
859
- id: `${graphType}-${index}-${obsIndex}`,
860
- dateTime,
861
- value: displayValue,
862
- unit: getUnitForGraphType(graphType),
863
- };
864
- tableData.push(rowData);
865
1209
  }
1210
+ } else {
1211
+ throw new Error('Not a JSON string');
866
1212
  }
867
- } else {
868
- throw new Error('Not a JSON string');
869
- }
870
- } catch (e) {
871
- if (mapObservationToGraphType(obs, graphType)) {
872
- let conceptName = obs.concept?.display || t?.('unknown', 'Unknown') || 'Unknown';
873
- let unit = getUnitForGraphType(graphType);
874
-
875
- if (graphType === 'progress-events') {
876
- const eventInfo = getProgressEventInfo(obs.concept?.uuid || '');
877
- if (eventInfo) {
878
- conceptName = eventInfo.name;
879
- unit = eventInfo.unit;
880
- } else {
881
- conceptName = obs.concept?.display || t?.('progressEvent', 'Progress Event') || 'Progress Event';
1213
+ } catch (e) {
1214
+ if (mapObservationToGraphType(obs, graphType)) {
1215
+ let conceptName = obs.concept?.display || t?.('unknown', 'Unknown') || 'Unknown';
1216
+ let unit = getUnitForGraphType(graphType);
1217
+ if (graphType === 'progress-events') {
1218
+ const eventInfo = getProgressEventInfo(obs.concept?.uuid || '');
1219
+ if (eventInfo) {
1220
+ conceptName = eventInfo.name;
1221
+ unit = eventInfo.unit;
1222
+ } else {
1223
+ conceptName = obs.concept?.display || t?.('progressEvent', 'Progress Event') || 'Progress Event';
1224
+ }
882
1225
  }
883
- }
884
-
885
- let displayValue;
886
- if (graphType === 'descent-of-head') {
887
- let valueToMap;
888
- if (typeof obs.value === 'object' && obs.value !== null) {
889
- const valueObj = obs.value as any;
890
- if (valueObj.conceptUuid) {
891
- valueToMap = valueObj.conceptUuid;
892
- } else if (valueObj.value) {
893
- valueToMap = valueObj.value;
1226
+ let displayValue;
1227
+ if (graphType === 'descent-of-head') {
1228
+ let valueToMap;
1229
+ if (typeof obs.value === 'object' && obs.value !== null) {
1230
+ const valueObj = obs.value as any;
1231
+ if (valueObj.conceptUuid) {
1232
+ valueToMap = valueObj.conceptUuid;
1233
+ } else if (valueObj.value) {
1234
+ valueToMap = valueObj.value;
1235
+ } else {
1236
+ valueToMap = String(obs.value);
1237
+ }
894
1238
  } else {
895
1239
  valueToMap = String(obs.value);
896
1240
  }
1241
+ displayValue = getStationDisplay(valueToMap);
897
1242
  } else {
898
- valueToMap = String(obs.value);
1243
+ const numericValue = parseFloat(String(obs.value));
1244
+ if (!isNaN(numericValue)) {
1245
+ displayValue = numericValue.toFixed(1);
1246
+ } else {
1247
+ displayValue = String(obs.value);
1248
+ }
899
1249
  }
1250
+ const rowData = {
1251
+ id: `${graphType}-${index}-${obsIndex}`,
1252
+ dateTime,
1253
+ measurement: conceptName,
1254
+ value: displayValue,
1255
+ unit: unit,
1256
+ };
1257
+ tableData.push(rowData);
1258
+ }
1259
+ }
1260
+ });
1261
+ }
1262
+ });
900
1263
 
901
- displayValue = getStationDisplay(valueToMap);
902
- } else {
903
- const numericValue = parseFloat(String(obs.value));
904
- if (!isNaN(numericValue)) {
905
- displayValue = numericValue.toFixed(1);
906
- } else {
907
- displayValue = String(obs.value);
1264
+ return tableData.sort(
1265
+ (a, b) => new Date(a.dateTime.split(' — ')[0]).getTime() - new Date(b.dateTime.split(' — ')[0]).getTime(),
1266
+ );
1267
+ }
1268
+
1269
+ // Hook to fetch and manage fetal heart rate data from OpenMRS
1270
+ export function useFetalHeartRateData(patientUuid: string) {
1271
+ const fetcher = (url: string) => openmrsFetch(url).then((res) => res.json());
1272
+
1273
+ // Fetch encounters instead of just observations to get all related data
1274
+ const { data, error, isLoading, mutate } = useSWR(
1275
+ patientUuid
1276
+ ? `${restBaseUrl}/encounter?patient=${patientUuid}&encounterType=${MCH_PARTOGRAPHY_ENCOUNTER_UUID}&v=full&limit=100&order=desc`
1277
+ : null,
1278
+ fetcher,
1279
+ {
1280
+ onError: (error) => {
1281
+ console.error('Error fetching fetal heart rate data:', error);
1282
+ },
1283
+ },
1284
+ );
1285
+
1286
+ const fetalHeartRateData = useMemo(() => {
1287
+ try {
1288
+ if (!data?.results || !Array.isArray(data.results)) {
1289
+ return [];
1290
+ }
1291
+
1292
+ const fetalHeartRateEntries = [];
1293
+
1294
+ for (const encounter of data.results) {
1295
+ if (!encounter.obs || !Array.isArray(encounter.obs)) {
1296
+ continue;
1297
+ }
1298
+
1299
+ // Look for fetal heart rate observations in this encounter
1300
+ const fetalHeartRateObs = encounter.obs.find(
1301
+ (obs) => obs.concept.uuid === PARTOGRAPHY_CONCEPTS['fetal-heart-rate'],
1302
+ );
1303
+
1304
+ if (fetalHeartRateObs) {
1305
+ const encounterDatetime = new Date(encounter.encounterDatetime);
1306
+
1307
+ // Look for hour and time observations with text format
1308
+ let hour = 0;
1309
+ let time = '';
1310
+
1311
+ // Find hour observation (saved as "Hour: X.X")
1312
+ const hourObs = encounter.obs.find(
1313
+ (obs) =>
1314
+ obs.concept.uuid === PARTOGRAPHY_CONCEPTS['fetal-heart-rate-hour'] &&
1315
+ obs.value &&
1316
+ typeof obs.value === 'string' &&
1317
+ obs.value.startsWith('Hour:'),
1318
+ );
1319
+
1320
+ // Find time observation (saved as "Time: XX:XX")
1321
+ const timeObs = encounter.obs.find(
1322
+ (obs) =>
1323
+ obs.concept.uuid === PARTOGRAPHY_CONCEPTS['fetal-heart-rate-time'] &&
1324
+ obs.value &&
1325
+ typeof obs.value === 'string' &&
1326
+ obs.value.startsWith('Time:'),
1327
+ );
1328
+
1329
+ // Parse hour from text format "Hour: 2.5"
1330
+ if (hourObs && typeof hourObs.value === 'string') {
1331
+ const hourMatch = hourObs.value.match(/Hour:\s*([0-9.]+)/);
1332
+ if (hourMatch) {
1333
+ hour = parseFloat(hourMatch[1]) || 0;
1334
+ }
1335
+ }
1336
+
1337
+ // Parse time from text format "Time: 03:15"
1338
+ if (timeObs && typeof timeObs.value === 'string') {
1339
+ const timeMatch = timeObs.value.match(/Time:\s*(.+)/);
1340
+ if (timeMatch) {
1341
+ time = timeMatch[1].trim();
908
1342
  }
909
1343
  }
910
1344
 
911
- const rowData = {
912
- id: `${graphType}-${index}-${obsIndex}`,
913
- dateTime,
914
- measurement: conceptName,
915
- value: displayValue,
916
- unit: unit,
1345
+ const entry = {
1346
+ id: `fhr-${fetalHeartRateObs.uuid}`,
1347
+ uuid: fetalHeartRateObs.uuid,
1348
+ encounterUuid: encounter.uuid,
1349
+ fetalHeartRate: parseFloat(fetalHeartRateObs.value) || 0,
1350
+ hour,
1351
+ time,
1352
+ date: encounterDatetime.toLocaleDateString(),
1353
+ encounterDatetime: encounterDatetime.toISOString(),
1354
+ obsDatetime: fetalHeartRateObs.obsDatetime,
917
1355
  };
918
- tableData.push(rowData);
1356
+
1357
+ fetalHeartRateEntries.push(entry);
919
1358
  }
920
1359
  }
921
- });
922
- });
923
1360
 
924
- return tableData.sort(
925
- (a, b) => new Date(a.dateTime.split(' — ')[0]).getTime() - new Date(b.dateTime.split(' — ')[0]).getTime(),
926
- );
1361
+ const sortedEntries = fetalHeartRateEntries.sort(
1362
+ (a, b) => new Date(b.encounterDatetime).getTime() - new Date(a.encounterDatetime).getTime(),
1363
+ );
1364
+
1365
+ return sortedEntries;
1366
+ } catch (error) {
1367
+ console.error('Error processing fetal heart rate data:', error);
1368
+ return [];
1369
+ }
1370
+ }, [data]);
1371
+
1372
+ return {
1373
+ fetalHeartRateData,
1374
+ isLoading,
1375
+ error,
1376
+ mutate,
1377
+ };
1378
+ }
1379
+
1380
+ // Function to save fetal heart rate data to OpenMRS
1381
+ export async function saveFetalHeartRateData(
1382
+ patientUuid: string,
1383
+ formData: { hour: number; time: string; fetalHeartRate: number },
1384
+ locationUuid?: string,
1385
+ providerUuid?: string,
1386
+ ) {
1387
+ try {
1388
+ const result = await createPartographyEncounter(
1389
+ patientUuid,
1390
+ 'fetal-heart-rate',
1391
+ formData,
1392
+ locationUuid,
1393
+ providerUuid,
1394
+ );
1395
+
1396
+ return result;
1397
+ } catch (error) {
1398
+ console.error('Error saving fetal heart rate data:', error);
1399
+ return {
1400
+ success: false,
1401
+ message: error?.message || 'Failed to save fetal heart rate data',
1402
+ };
1403
+ }
1404
+ }
1405
+
1406
+ // Function to save drug order data to OpenMRS
1407
+ export async function saveDrugOrderData(
1408
+ patientUuid: string,
1409
+ formData: {
1410
+ drugName: string;
1411
+ dosage: string;
1412
+ route: string;
1413
+ frequency: string;
1414
+ },
1415
+ ) {
1416
+ try {
1417
+ // Use the existing createPartographyEncounter function with drugs-fluids graph type
1418
+ const result = await createPartographyEncounter(patientUuid, 'drugs-fluids', formData);
1419
+
1420
+ return result;
1421
+ } catch (error) {
1422
+ console.error('Error saving drug order data:', error);
1423
+ return {
1424
+ success: false,
1425
+ message: error?.message || 'Failed to save drug order data',
1426
+ };
1427
+ }
927
1428
  }
928
1429
 
929
- export { useFetalHeartRateData } from './resources/fetal-heart-rate.resource';
930
- export { useDrugOrders, saveDrugOrderData } from './resources/drugs-fluids.resource';
931
- export { saveMembraneAmnioticFluidData } from './resources/membrane-amniotic-fluid.resource';
1430
+ // Function to fetch drug orders for a patient
1431
+ export function useDrugOrders(patientUuid: string) {
1432
+ // Remove the problematic sort parameter that's causing 500 error
1433
+ const apiUrl = patientUuid
1434
+ ? `${restBaseUrl}/order?patient=${patientUuid}&orderType=131168f4-15f5-102d-96e4-000c29c2a5d7&v=full&limit=50`
1435
+ : null;
1436
+
1437
+ const { data, error, isLoading, mutate } = useSWR(apiUrl, openmrsFetch);
1438
+
1439
+ const drugOrders = useMemo(() => {
1440
+ const responseData = data?.data as any;
1441
+
1442
+ if (!responseData?.results || !Array.isArray(responseData.results)) {
1443
+ // console.log('Drug Orders: No results found');
1444
+ return [];
1445
+ }
1446
+
1447
+ const allOrders = responseData.results;
1448
+ // Filter for active orders and manually sort by dateActivated (newest first)
1449
+ const activeOrders = allOrders
1450
+ .filter((order: any) => order.action === 'NEW' && !order.dateStopped)
1451
+ .sort((a: any, b: any) => {
1452
+ // Manual sorting by dateActivated, handling null values
1453
+ const dateA = a.dateActivated ? new Date(a.dateActivated).getTime() : 0;
1454
+ const dateB = b.dateActivated ? new Date(b.dateActivated).getTime() : 0;
1455
+ return dateB - dateA; // Descending order (newest first)
1456
+ });
1457
+
1458
+ // console.log(`Drug Orders: Found ${allOrders.length} total orders, ${activeOrders.length} active orders`);
1459
+
1460
+ const processedOrders = activeOrders.map((order: any) => {
1461
+ const processed = {
1462
+ id: order.uuid,
1463
+ drugName: order.drug?.display || order.drugNonCoded || 'Unknown Drug',
1464
+ dosage: `${order.dose || ''} ${order.doseUnits?.display || ''}`.trim(),
1465
+ route: order.route?.display || '',
1466
+ frequency: order.frequency?.display || '',
1467
+ date: order.dateActivated ? new Date(order.dateActivated).toLocaleDateString() : '',
1468
+ orderNumber: order.orderNumber,
1469
+ display: order.display,
1470
+ quantity: order.quantity,
1471
+ duration: order.duration,
1472
+ durationUnits: order.durationUnits?.display,
1473
+ asNeeded: order.asNeeded,
1474
+ instructions: order.instructions,
1475
+ orderer: order.orderer?.display,
1476
+ };
1477
+ return processed;
1478
+ });
1479
+
1480
+ return processedOrders;
1481
+ }, [data]);
1482
+
1483
+ return {
1484
+ drugOrders,
1485
+ isLoading,
1486
+ error,
1487
+ mutate,
1488
+ };
1489
+ }