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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/dist/805.js +1 -0
  3. package/dist/805.js.map +1 -0
  4. package/dist/kenyaemr-esm-patient-clinical-view-app.js +2 -2
  5. package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +27 -27
  6. package/dist/main.js +27 -27
  7. package/dist/main.js.map +1 -1
  8. package/dist/routes.json +1 -1
  9. package/package.json +1 -1
  10. package/src/config-schema.ts +97 -0
  11. package/src/contact-list/contact-tracing-history.component.tsx +18 -15
  12. package/src/maternal-and-child-health/partography/components/pulse-bp-graph.component.tsx +1 -0
  13. package/src/maternal-and-child-health/partography/components/temperature-graph.component.tsx +218 -0
  14. package/src/maternal-and-child-health/partography/components/uterine-contractions-graph.component.tsx +209 -0
  15. package/src/maternal-and-child-health/partography/forms/cervical-contractions-form.component.tsx +211 -0
  16. package/src/maternal-and-child-health/partography/forms/cervix-form.component.tsx +354 -0
  17. package/src/maternal-and-child-health/partography/forms/drugs-iv-fluids-form.component.tsx +321 -0
  18. package/src/maternal-and-child-health/partography/forms/fetal-heart-rate-form.component.tsx +275 -0
  19. package/src/maternal-and-child-health/partography/forms/index.ts +9 -0
  20. package/src/maternal-and-child-health/partography/forms/membrane-amniotic-fluid-form.component.tsx +330 -0
  21. package/src/maternal-and-child-health/partography/forms/oxytocin-form.component.tsx +207 -0
  22. package/src/maternal-and-child-health/partography/forms/pulse-bp-form.component.tsx +174 -0
  23. package/src/maternal-and-child-health/partography/forms/temperature-form.component.tsx +210 -0
  24. package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.component.tsx +218 -0
  25. package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.scss +107 -0
  26. package/src/maternal-and-child-health/partography/forms/time-picker-with-clock.component.tsx +174 -0
  27. package/src/maternal-and-child-health/partography/forms/time-picker-with-clock.scss +178 -0
  28. package/src/maternal-and-child-health/partography/forms/urine-test-form.component.tsx +255 -0
  29. package/src/maternal-and-child-health/partography/forms/useCervixData.ts +16 -0
  30. package/src/maternal-and-child-health/partography/graphs/cervical-contractions-graph.component.tsx +266 -0
  31. package/src/maternal-and-child-health/partography/graphs/cervix-graph.component.tsx +429 -0
  32. package/src/maternal-and-child-health/partography/graphs/drugs-iv-fluids-graph-wrapper.component.tsx +163 -0
  33. package/src/maternal-and-child-health/partography/graphs/drugs-iv-fluids-graph.component.tsx +82 -0
  34. package/src/maternal-and-child-health/partography/graphs/fetal-heart-rate-graph.component.tsx +359 -0
  35. package/src/maternal-and-child-health/partography/graphs/index.ts +10 -0
  36. package/src/maternal-and-child-health/partography/graphs/membrane-amniotic-fluid-graph.component.tsx +266 -0
  37. package/src/maternal-and-child-health/partography/graphs/oxytocin-graph-wrapper.component.tsx +190 -0
  38. package/src/maternal-and-child-health/partography/graphs/oxytocin-graph.component.tsx +126 -0
  39. package/src/maternal-and-child-health/partography/graphs/partograph-graph.component.tsx +266 -0
  40. package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph-wrapper.component.tsx +298 -0
  41. package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph.component.tsx +267 -0
  42. package/src/maternal-and-child-health/partography/graphs/temperature-graph.component.tsx +242 -0
  43. package/src/maternal-and-child-health/partography/graphs/urine-test-graph.component.tsx +246 -0
  44. package/src/maternal-and-child-health/partography/partograph.component.tsx +2141 -118
  45. package/src/maternal-and-child-health/partography/partography-dashboard.meta.ts +8 -0
  46. package/src/maternal-and-child-health/partography/partography-data-form.scss +163 -0
  47. package/src/maternal-and-child-health/partography/partography.resource.ts +233 -326
  48. package/src/maternal-and-child-health/partography/partography.scss +1341 -3
  49. package/src/maternal-and-child-health/partography/resources/blood-pressure.resource.ts +96 -0
  50. package/src/maternal-and-child-health/partography/resources/cervical-dilation.resource.ts +109 -0
  51. package/src/maternal-and-child-health/partography/resources/cervix.resource.ts +362 -0
  52. package/src/maternal-and-child-health/partography/resources/descent-of-head.resource.ts +101 -0
  53. package/src/maternal-and-child-health/partography/resources/drugs-fluids.resource.ts +88 -0
  54. package/src/maternal-and-child-health/partography/resources/fetal-heart-rate.resource.ts +122 -0
  55. package/src/maternal-and-child-health/partography/resources/maternal-pulse.resource.ts +77 -0
  56. package/src/maternal-and-child-health/partography/resources/membrane-amniotic-fluid.resource.ts +108 -0
  57. package/src/maternal-and-child-health/partography/resources/oxytocin.resource.ts +159 -0
  58. package/src/maternal-and-child-health/partography/resources/progress-events.resource.ts +6 -0
  59. package/src/maternal-and-child-health/partography/resources/pulse-bp-combined.resource.ts +53 -0
  60. package/src/maternal-and-child-health/partography/resources/temperature.resource.ts +84 -0
  61. package/src/maternal-and-child-health/partography/resources/uterine-contractions.resource.ts +173 -0
  62. package/src/maternal-and-child-health/partography/table/temperature-table.component.tsx +99 -0
  63. package/src/maternal-and-child-health/partography/table/uterine-contractions-table.component.tsx +86 -0
  64. package/src/maternal-and-child-health/partography/types/index.ts +319 -101
  65. package/dist/397.js +0 -1
  66. package/dist/397.js.map +0 -1
@@ -1,6 +1,10 @@
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
4
  import { openmrsFetch, restBaseUrl, toOmrsIsoString, useConfig } from '@openmrs/esm-framework';
2
5
  import useSWR from 'swr';
3
- import { useState, useEffect } from 'react';
6
+ import React, { useState, useEffect, useMemo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
4
8
  import {
5
9
  PARTOGRAPHY_CONCEPTS,
6
10
  PARTOGRAPHY_ENCOUNTER_TYPES,
@@ -9,8 +13,13 @@ import {
9
13
  type PartographyObservation,
10
14
  type PartographyEncounter,
11
15
  type PartographyGraphType,
16
+ CONTRACTION_INTENSITY_OPTIONS,
12
17
  } from './types';
13
18
  import { configSchema, type ConfigObject } from '../../config-schema';
19
+ import {
20
+ buildTemperatureObservation,
21
+ transformTemperatureEncounterToChartData,
22
+ } from './resources/temperature.resource';
14
23
 
15
24
  export type { PartographyObservation, PartographyEncounter };
16
25
  const defaultPartographyConfig = configSchema.partography._default;
@@ -192,8 +201,12 @@ export function usePartographyEncounters(patientUuid: string, graphType: string)
192
201
  export function usePartographyData(patientUuid: string, graphType: string) {
193
202
  const { encounters, isLoading, error, mutate } = usePartographyEncounters(patientUuid, graphType);
194
203
 
204
+ const shouldUseLocalFallback = graphType !== 'fetal-heart-rate';
205
+
195
206
  const localDataFallback = useSWR(
196
- !isLoading && encounters.length === 0 ? `partography_local_${patientUuid}_${graphType}` : null,
207
+ !isLoading && encounters.length === 0 && shouldUseLocalFallback
208
+ ? `partography_local_${patientUuid}_${graphType}`
209
+ : null,
197
210
  () => {
198
211
  const localData = loadPartographyData(patientUuid, graphType);
199
212
  return localData;
@@ -219,7 +232,6 @@ function loadPartographyData(patientUuid: string, graphType: string): Partograph
219
232
  try {
220
233
  const storageKey = generateStorageKey(patientUuid, graphType);
221
234
  const localData = JSON.parse(localStorage.getItem(storageKey) || '[]');
222
-
223
235
  const localEncounters = localData.map((item) => ({
224
236
  uuid: item.id,
225
237
  encounterDatetime: item.timestamp,
@@ -241,11 +253,10 @@ function loadPartographyData(patientUuid: string, graphType: string): Partograph
241
253
  },
242
254
  ],
243
255
  }));
244
-
245
256
  return localEncounters.sort(
246
257
  (a, b) => new Date(b.encounterDatetime).getTime() - new Date(a.encounterDatetime).getTime(),
247
258
  );
248
- } catch (e) {
259
+ } catch {
249
260
  return [];
250
261
  }
251
262
  }
@@ -258,28 +269,33 @@ export async function createPartographyEncounter(
258
269
  providerUuid?: string,
259
270
  t?: (key: string, fallback?: string) => string,
260
271
  ): Promise<{ success: boolean; message: string; encounter?: PartographyEncounter }> {
272
+ const translate = t || ((key, fallback) => fallback || key);
261
273
  try {
262
274
  const observations = buildObservations(graphType, formData);
263
-
264
275
  if (observations.length === 0) {
265
- throw new Error(t?.('noValidObservations', 'No valid observations to save') || 'No valid observations to save');
276
+ throw new Error(translate('noValidObservations', 'No valid observations to save'));
266
277
  }
267
-
268
278
  const encounterTypeUuid = await getEncounterTypeForGraph(graphType);
269
279
  if (!encounterTypeUuid) {
270
- throw new Error(
271
- t?.('noEncounterTypeFound', 'No encounter type found for graph: {{graphType}}')?.replace(
272
- '{{graphType}}',
273
- graphType,
274
- ) || `No encounter type found for graph: ${graphType}`,
275
- );
280
+ throw new Error(translate('noEncounterTypeFound', `No encounter type found for graph: ${graphType}`));
281
+ }
282
+ let finalLocationUuid = locationUuid;
283
+ if (!finalLocationUuid) {
284
+ try {
285
+ const sessionResponse = await openmrsFetch(`${restBaseUrl}/session`);
286
+ if (sessionResponse.ok) {
287
+ const sessionData = await sessionResponse.json();
288
+ if (sessionData.sessionLocation?.uuid) {
289
+ finalLocationUuid = sessionData.sessionLocation.uuid;
290
+ }
291
+ }
292
+ } catch {}
293
+ if (!finalLocationUuid) {
294
+ finalLocationUuid = getDefaultLocationUuid();
295
+ }
276
296
  }
277
-
278
- const finalLocationUuid = locationUuid || getDefaultLocationUuid();
279
-
280
297
  const timeConfig = getTimeConfig();
281
298
  const encounterDatetime = toOmrsIsoString(new Date(Date.now() - timeConfig.defaultEncounterOffset));
282
-
283
299
  const encounterPayload: any = {
284
300
  patient: patientUuid,
285
301
  location: finalLocationUuid,
@@ -287,9 +303,7 @@ export async function createPartographyEncounter(
287
303
  obs: observations,
288
304
  encounterType: encounterTypeUuid,
289
305
  };
290
-
291
306
  let finalProviderUuid = providerUuid;
292
-
293
307
  if (!finalProviderUuid) {
294
308
  try {
295
309
  const sessionResponse = await openmrsFetch(`${restBaseUrl}/session`);
@@ -308,8 +322,7 @@ export async function createPartographyEncounter(
308
322
  }
309
323
  }
310
324
  }
311
- } catch (error) {}
312
-
325
+ } catch {}
313
326
  if (!finalProviderUuid) {
314
327
  try {
315
328
  const anyProviderResponse = await openmrsFetch(`${restBaseUrl}/provider?v=default&limit=1`);
@@ -319,10 +332,9 @@ export async function createPartographyEncounter(
319
332
  finalProviderUuid = anyProviderData.results[0].uuid;
320
333
  }
321
334
  }
322
- } catch (error) {}
335
+ } catch {}
323
336
  }
324
337
  }
325
-
326
338
  if (finalProviderUuid) {
327
339
  try {
328
340
  const providerValidationResponse = await openmrsFetch(`${restBaseUrl}/provider/${finalProviderUuid}?v=default`);
@@ -335,128 +347,32 @@ export async function createPartographyEncounter(
335
347
  },
336
348
  ];
337
349
  }
338
- } catch (validationError) {}
350
+ } catch {}
351
+ }
352
+ if (!patientUuid) {
353
+ throw new Error(translate('Patient UUID is required'));
354
+ }
355
+ if (!finalLocationUuid) {
356
+ throw new Error(translate('Location UUID is required'));
357
+ }
358
+ if (!encounterTypeUuid) {
359
+ throw new Error(translate('Encounter type UUID is required'));
360
+ }
361
+ if (!observations || observations.length === 0) {
362
+ throw new Error(translate('At least one observation is required'));
339
363
  }
340
-
341
364
  const response = await openmrsFetch(`${restBaseUrl}/encounter`, {
342
365
  method: 'POST',
343
366
  headers: { 'Content-Type': 'application/json' },
344
367
  body: JSON.stringify(encounterPayload),
345
368
  });
346
-
347
369
  if (!response.ok) {
348
370
  let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
349
-
350
371
  try {
351
372
  const responseText = await response.text();
352
373
  if (responseText) {
353
374
  try {
354
375
  const detailedError = JSON.parse(responseText);
355
-
356
- if (
357
- detailedError.error &&
358
- detailedError.error.fieldErrors &&
359
- detailedError.error.fieldErrors.encounterType
360
- ) {
361
- const availableTypes = await discoverEncounterTypes();
362
- let retryEncounterType = null;
363
-
364
- const retryFallbackTypes = getRetryFallbackTypes();
365
- for (const fallback of retryFallbackTypes) {
366
- if (availableTypes[fallback]) {
367
- retryEncounterType = availableTypes[fallback];
368
- break;
369
- }
370
- }
371
- if (!retryEncounterType && Object.keys(availableTypes).length > 0) {
372
- retryEncounterType = Object.values(availableTypes)[0];
373
- }
374
-
375
- if (retryEncounterType) {
376
- const retryPayload = { ...encounterPayload, encounterType: retryEncounterType };
377
- const retryResponse = await openmrsFetch(`${restBaseUrl}/encounter`, {
378
- method: 'POST',
379
- headers: { 'Content-Type': 'application/json' },
380
- body: JSON.stringify(retryPayload),
381
- });
382
-
383
- if (retryResponse.ok) {
384
- const encounter = await retryResponse.json();
385
-
386
- try {
387
- await saveToLocalStorage(patientUuid, graphType, formData, encounter.uuid);
388
- } catch (localError) {}
389
-
390
- return {
391
- success: true,
392
- message:
393
- t?.('partographyDataSavedFallbackEncounter', 'Partography data saved successfully') ||
394
- 'Partography data saved successfully',
395
- encounter,
396
- };
397
- }
398
- }
399
- }
400
-
401
- const isProviderConstraintError =
402
- (detailedError.error.fieldErrors && detailedError.error.fieldErrors.encounterProviders) ||
403
- (detailedError.error.message && detailedError.error.message.includes('provider_id'));
404
-
405
- if (isProviderConstraintError) {
406
- const retryPayloadWithoutProvider = { ...encounterPayload };
407
- delete retryPayloadWithoutProvider.encounterProviders;
408
-
409
- const retryResponse = await openmrsFetch(`${restBaseUrl}/encounter`, {
410
- method: 'POST',
411
- headers: { 'Content-Type': 'application/json' },
412
- body: JSON.stringify(retryPayloadWithoutProvider),
413
- });
414
-
415
- if (retryResponse.ok) {
416
- const encounter = await retryResponse.json();
417
-
418
- try {
419
- await saveToLocalStorage(patientUuid, graphType, formData, encounter.uuid);
420
- } catch (localError) {}
421
-
422
- return {
423
- success: true,
424
- message:
425
- t?.('partographyDataSavedWithoutProvider', 'Partography data saved successfully') ||
426
- 'Partography data saved successfully',
427
- encounter,
428
- };
429
- }
430
- }
431
-
432
- if (detailedError.error.fieldErrors && detailedError.error.fieldErrors.encounterDatetime) {
433
- const timeConfig = getTimeConfig();
434
- const earlierDatetime = toOmrsIsoString(new Date(Date.now() - timeConfig.retryEncounterOffset));
435
- const retryPayload = { ...encounterPayload, encounterDatetime: earlierDatetime };
436
-
437
- const retryResponse = await openmrsFetch(`${restBaseUrl}/encounter`, {
438
- method: 'POST',
439
- headers: { 'Content-Type': 'application/json' },
440
- body: JSON.stringify(retryPayload),
441
- });
442
-
443
- if (retryResponse.ok) {
444
- const encounter = await retryResponse.json();
445
-
446
- try {
447
- await saveToLocalStorage(patientUuid, graphType, formData, encounter.uuid);
448
- } catch (localError) {}
449
-
450
- return {
451
- success: true,
452
- message:
453
- t?.('partographyDataSavedAdjustedDatetime', 'Partography data saved successfully') ||
454
- 'Partography data saved successfully',
455
- encounter,
456
- };
457
- }
458
- }
459
-
460
376
  if (detailedError.error) {
461
377
  if (detailedError.error.message) {
462
378
  errorMessage += ` - ${detailedError.error.message}`;
@@ -465,17 +381,26 @@ export async function createPartographyEncounter(
465
381
  errorMessage += ` (${detailedError.error.detail})`;
466
382
  }
467
383
  }
468
- } catch (parseError) {
384
+ } catch {
469
385
  errorMessage += ` - Raw Response: ${responseText}`;
470
386
  }
471
387
  }
472
- } catch (e) {}
473
-
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
+ }
474
401
  throw new Error(errorMessage);
475
402
  }
476
-
477
403
  const encounter = await response.json();
478
-
479
404
  const cacheKey = generateCacheKey(patientUuid, graphType);
480
405
  try {
481
406
  const { mutate: globalMutate } = await import('swr');
@@ -484,38 +409,23 @@ export async function createPartographyEncounter(
484
409
  setTimeout(async () => {
485
410
  await globalMutate(cacheKey);
486
411
  }, timeConfig.cacheInvalidationDelay);
487
- } catch (mutateError) {}
488
-
489
- try {
490
- await saveToLocalStorage(patientUuid, graphType, formData, encounter.uuid);
491
- } catch (localError) {}
492
-
412
+ } catch {}
493
413
  return {
494
414
  success: true,
495
- message:
496
- t?.('partographyDataSavedSuccessfully', 'Partography data saved successfully') ||
497
- 'Partography data saved successfully',
415
+ message: translate('partographyDataSavedSuccessfully', 'Partography data saved successfully'),
498
416
  encounter,
499
417
  };
500
- } catch (error) {
501
- try {
502
- await saveToLocalStorage(patientUuid, graphType, formData);
503
- return {
504
- success: true,
505
- message:
506
- t?.('partographyDataSavedLocalStorage', 'Partography data saved to local storage') ||
507
- 'Partography data saved to local storage',
508
- };
509
- } catch (localError) {
510
- return {
511
- success: false,
512
- message:
513
- t?.('failedToSavePartographyData', 'Failed to save partography data: {{error}}')?.replace(
514
- '{{error}}',
515
- error.message,
516
- ) || `Failed to save partography data: ${error.message}`,
517
- };
518
- }
418
+ } catch (error: any) {
419
+ // Log error details for debugging
420
+ // eslint-disable-next-line no-console
421
+ console.error('createPartographyEncounter error:', error);
422
+ return {
423
+ success: false,
424
+ message: translate(
425
+ 'failedToSavePartographyData',
426
+ `Failed to save partography data: ${error && error.message ? error.message : String(error)}`,
427
+ ),
428
+ };
519
429
  }
520
430
  }
521
431
 
@@ -555,14 +465,72 @@ function buildObservations(graphType: string, formData: any): any[] {
555
465
  const obsDatetime = toOmrsIsoString(new Date(Date.now() - timeConfig.defaultEncounterOffset));
556
466
 
557
467
  switch (graphType) {
468
+ case 'membrane-amniotic-fluid':
469
+ if (formData.time) {
470
+ observations.push({
471
+ concept: PARTOGRAPHY_CONCEPTS['fetal-heart-rate-time'],
472
+ value: `Time: ${formData.time}`,
473
+ obsDatetime,
474
+ });
475
+ }
476
+ const amnioticFluidMap = defaultPartographyConfig.amnioticFluidMap;
477
+ let amnioticFluidValue = formData.amnioticFluid;
478
+ if (amnioticFluidValue && amnioticFluidMap[amnioticFluidValue]) {
479
+ amnioticFluidValue = amnioticFluidMap[amnioticFluidValue];
480
+ }
481
+ if (amnioticFluidValue) {
482
+ observations.push({
483
+ concept: PARTOGRAPHY_CONCEPTS['amniotic-fluid'],
484
+ value: amnioticFluidValue,
485
+ obsDatetime,
486
+ });
487
+ }
488
+ const mouldingMap = defaultPartographyConfig.mouldingMap;
489
+ let mouldingValue = formData.moulding;
490
+ if (mouldingValue && mouldingMap[mouldingValue]) {
491
+ mouldingValue = mouldingMap[mouldingValue];
492
+ }
493
+ if (mouldingValue) {
494
+ observations.push({
495
+ concept: PARTOGRAPHY_CONCEPTS['moulding'],
496
+ value: mouldingValue,
497
+ obsDatetime,
498
+ });
499
+ }
500
+ break;
558
501
  case 'fetal-heart-rate':
559
- if (formData.value || formData.measurementValue) {
502
+ if (formData.fetalHeartRate || formData.value || formData.measurementValue) {
560
503
  observations.push({
561
504
  concept: PARTOGRAPHY_CONCEPTS['fetal-heart-rate'],
562
- value: parseFloat(formData.value || formData.measurementValue),
505
+ value: parseFloat(formData.fetalHeartRate || formData.value || formData.measurementValue),
563
506
  obsDatetime,
564
507
  });
565
508
  }
509
+
510
+ if (formData.hour !== undefined && formData.hour !== '') {
511
+ try {
512
+ const hourValue = parseFloat(formData.hour);
513
+
514
+ if (hourValue >= 0 && hourValue <= 24) {
515
+ const hourText = `Hour: ${hourValue}`;
516
+ observations.push({
517
+ concept: PARTOGRAPHY_CONCEPTS['fetal-heart-rate-hour'],
518
+ value: hourText,
519
+ obsDatetime,
520
+ });
521
+ }
522
+ } catch (error) {}
523
+ }
524
+
525
+ if (formData.time) {
526
+ try {
527
+ observations.push({
528
+ concept: PARTOGRAPHY_CONCEPTS['fetal-heart-rate-time'],
529
+ value: `Time: ${formData.time}`,
530
+ obsDatetime,
531
+ });
532
+ } catch (error) {}
533
+ }
566
534
  break;
567
535
 
568
536
  case 'cervical-dilation':
@@ -591,14 +559,12 @@ function buildObservations(graphType: string, formData: any): any[] {
591
559
  }
592
560
  break;
593
561
 
594
- case 'descent-of-head':
562
+ case 'descent-of-head': {
595
563
  if (formData.value || formData.measurementValue) {
596
564
  let conceptValue = formData.value || formData.measurementValue;
597
-
598
565
  if (formData.conceptUuid) {
599
566
  conceptValue = formData.conceptUuid;
600
567
  }
601
-
602
568
  observations.push({
603
569
  concept: PARTOGRAPHY_CONCEPTS['descent-of-head'],
604
570
  value: conceptValue,
@@ -606,55 +572,86 @@ function buildObservations(graphType: string, formData: any): any[] {
606
572
  });
607
573
  }
608
574
  break;
609
-
610
- case 'uterine-contractions':
611
- if (formData.value || formData.measurementValue) {
612
- observations.push({
613
- concept: PARTOGRAPHY_CONCEPTS['uterine-contractions'],
614
- value: parseFloat(formData.value || formData.measurementValue),
615
- obsDatetime,
616
- });
617
- }
618
- break;
619
-
620
- case 'maternal-pulse':
621
- if (formData.value || formData.measurementValue) {
622
- observations.push({
623
- concept: PARTOGRAPHY_CONCEPTS['maternal-pulse'],
624
- value: parseFloat(formData.value || formData.measurementValue),
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,
625
592
  obsDatetime,
626
593
  });
627
594
  }
628
- break;
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
+ }
629
613
 
630
- case 'blood-pressure':
631
- if (formData.systolic) {
632
- observations.push({
633
- concept: PARTOGRAPHY_CONCEPTS['systolic-bp'],
634
- value: parseFloat(formData.systolic),
635
- obsDatetime,
636
- });
637
- }
638
- if (formData.diastolic) {
639
- observations.push({
640
- concept: PARTOGRAPHY_CONCEPTS['diastolic-bp'],
641
- value: parseFloat(formData.diastolic),
642
- obsDatetime,
643
- });
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
+ }
644
633
  }
645
- break;
646
634
 
647
- case 'temperature':
648
- if (formData.value || formData.measurementValue) {
649
- observations.push({
650
- concept: PARTOGRAPHY_CONCEPTS['temperature'],
651
- value: parseFloat(formData.value || formData.measurementValue),
635
+ if (formData.timeSlot) {
636
+ extra.push({
637
+ concept: PARTOGRAPHY_CONCEPTS['time-slot'] || PARTOGRAPHY_CONCEPTS['event-description'],
638
+ value: `TimeSlot: ${formData.timeSlot}`,
652
639
  obsDatetime,
653
640
  });
654
641
  }
655
- break;
656
642
 
657
- case 'urine-analysis':
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': {
658
655
  if (formData.proteinLevel) {
659
656
  observations.push({
660
657
  concept: PARTOGRAPHY_CONCEPTS['protein-level'],
@@ -677,12 +674,13 @@ function buildObservations(graphType: string, formData: any): any[] {
677
674
  });
678
675
  }
679
676
  break;
677
+ }
680
678
 
681
679
  case 'drugs-fluids':
682
- if (formData.medication) {
680
+ if (formData.medication || formData.drugName) {
683
681
  observations.push({
684
682
  concept: PARTOGRAPHY_CONCEPTS['medication'],
685
- value: formData.medication,
683
+ value: formData.medication || formData.drugName,
686
684
  obsDatetime,
687
685
  });
688
686
  }
@@ -693,6 +691,20 @@ function buildObservations(graphType: string, formData: any): any[] {
693
691
  obsDatetime,
694
692
  });
695
693
  }
694
+ if (formData.route) {
695
+ observations.push({
696
+ concept: PARTOGRAPHY_CONCEPTS['route'] || PARTOGRAPHY_CONCEPTS['event-description'],
697
+ value: formData.route,
698
+ obsDatetime,
699
+ });
700
+ }
701
+ if (formData.frequency) {
702
+ observations.push({
703
+ concept: PARTOGRAPHY_CONCEPTS['frequency'] || PARTOGRAPHY_CONCEPTS['event-description'],
704
+ value: formData.frequency,
705
+ obsDatetime,
706
+ });
707
+ }
696
708
  break;
697
709
 
698
710
  case 'progress-events':
@@ -729,120 +741,11 @@ function buildObservations(graphType: string, formData: any): any[] {
729
741
  return observations;
730
742
  }
731
743
 
732
- export function transformEncounterToChartData(encounters: PartographyEncounter[], graphType: string): any[] {
733
- const chartData = [];
734
-
735
- encounters.forEach((encounter) => {
736
- const encounterTime = new Date(encounter.encounterDatetime).toLocaleTimeString('en-US', {
737
- hour: '2-digit',
738
- minute: '2-digit',
739
- hour12: false,
740
- });
741
-
742
- encounter.obs?.forEach((obs) => {
743
- try {
744
- if (typeof obs.value === 'string' && obs.value.startsWith('{')) {
745
- const parsedData = JSON.parse(obs.value);
746
-
747
- if (parsedData.graphType === graphType && parsedData.data) {
748
- const formData = parsedData.data;
749
- const value = formData.value || formData.measurementValue;
750
-
751
- if (value) {
752
- const dataPoint = {
753
- group: getGraphTypeDisplayName(graphType),
754
- time: formData.time || encounterTime,
755
- value: parseFloat(value),
756
- };
757
- chartData.push(dataPoint);
758
- }
759
- }
760
- } else {
761
- const conceptUuid = obs.concept.uuid;
762
- let value = null;
763
- let groupName = '';
764
-
765
- switch (conceptUuid) {
766
- case PARTOGRAPHY_CONCEPTS['fetal-heart-rate']:
767
- if (graphType === 'fetal-heart-rate') {
768
- value = parseFloat(obs.value as string);
769
- groupName = getGraphTypeDisplayName('fetal-heart-rate');
770
- }
771
- break;
772
- case PARTOGRAPHY_CONCEPTS['cervical-dilation']:
773
- if (graphType === 'cervical-dilation') {
774
- value = parseFloat(obs.value as string);
775
- groupName = getGraphTypeDisplayName('cervical-dilation');
776
- }
777
- break;
778
- case PARTOGRAPHY_CONCEPTS['descent-of-head']:
779
- if (graphType === 'descent-of-head') {
780
- value = getStationValue(obs.value as string) ?? parseFloat(obs.value as string);
781
- groupName = getGraphTypeDisplayName('descent-of-head');
782
- }
783
- break;
784
- case PARTOGRAPHY_CONCEPTS['uterine-contractions']:
785
- if (graphType === 'uterine-contractions') {
786
- value = parseFloat(obs.value as string);
787
- groupName = getGraphTypeDisplayName('uterine-contractions');
788
- }
789
- break;
790
- case PARTOGRAPHY_CONCEPTS['maternal-pulse']:
791
- if (graphType === 'maternal-pulse') {
792
- value = parseFloat(obs.value as string);
793
- groupName = getGraphTypeDisplayName('maternal-pulse');
794
- }
795
- break;
796
- case PARTOGRAPHY_CONCEPTS['systolic-bp']:
797
- if (graphType === 'blood-pressure') {
798
- value = parseFloat(obs.value as string);
799
- groupName = 'Systolic';
800
- }
801
- break;
802
- case PARTOGRAPHY_CONCEPTS['diastolic-bp']:
803
- if (graphType === 'blood-pressure') {
804
- value = parseFloat(obs.value as string);
805
- groupName = 'Diastolic';
806
- }
807
- break;
808
- case PARTOGRAPHY_CONCEPTS['temperature']:
809
- if (graphType === 'temperature') {
810
- value = parseFloat(obs.value as string);
811
- groupName = getGraphTypeDisplayName('temperature');
812
- }
813
- break;
814
- default:
815
- if (typeof obs.value === 'number' || !isNaN(parseFloat(obs.value as string))) {
816
- value = parseFloat(obs.value as string);
817
- groupName = getGraphTypeDisplayName(graphType);
818
- }
819
- break;
820
- }
821
-
822
- if (value !== null && groupName) {
823
- const dataPoint = {
824
- group: groupName,
825
- time: encounterTime,
826
- value: value,
827
- };
828
- chartData.push(dataPoint);
829
- }
830
- }
831
- } catch (e) {}
832
- });
833
- });
834
-
835
- return chartData.sort((a, b) => {
836
- // Parse time strings (HH:MM format) for comparison
837
- const timeA = a.time.split(':').map(Number);
838
- const timeB = b.time.split(':').map(Number);
839
-
840
- // Convert to minutes for easy comparison
841
- const minutesA = timeA[0] * 60 + (timeA[1] || 0);
842
- const minutesB = timeB[0] * 60 + (timeB[1] || 0);
843
-
844
- return minutesA - minutesB;
845
- });
744
+ export function transformEncounterToChartData(encounters: any[], graphType: string): any[] {
745
+ if (graphType === 'temperature') {
746
+ return transformTemperatureEncounterToChartData(encounters);
747
+ }
748
+ return [];
846
749
  }
847
750
 
848
751
  function getGroupNameForGraph(graphType: string): string {
@@ -1022,3 +925,7 @@ export function transformEncounterToTableData(
1022
925
  (a, b) => new Date(a.dateTime.split(' — ')[0]).getTime() - new Date(b.dateTime.split(' — ')[0]).getTime(),
1023
926
  );
1024
927
  }
928
+
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';