@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.
- package/.turbo/turbo-build.log +4 -4
- package/dist/127.js +1 -1
- package/dist/{805.js → 189.js} +1 -1
- package/dist/189.js.map +1 -0
- package/dist/40.js +1 -1
- package/dist/916.js +1 -1
- package/dist/kenyaemr-esm-patient-clinical-view-app.js +2 -2
- package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +36 -36
- package/dist/main.js +3 -3
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/config-schema.ts +58 -36
- package/src/maternal-and-child-health/partography/components/temperature-graph.component.tsx +0 -4
- package/src/maternal-and-child-health/partography/components/uterine-contractions-graph.component.tsx +33 -11
- package/src/maternal-and-child-health/partography/forms/cervical-contractions-form.component.tsx +124 -136
- package/src/maternal-and-child-health/partography/forms/cervix-form.component.tsx +23 -14
- package/src/maternal-and-child-health/partography/forms/drugs-iv-fluids-form.component.tsx +6 -10
- package/src/maternal-and-child-health/partography/forms/fetal-heart-rate-form.component.tsx +36 -13
- package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.component.tsx +27 -46
- package/src/maternal-and-child-health/partography/forms/useCervixData.ts +2 -2
- package/src/maternal-and-child-health/partography/graphs/cervix-graph.component.tsx +56 -18
- package/src/maternal-and-child-health/partography/graphs/fetal-heart-rate-graph.component.tsx +36 -23
- package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph.component.tsx +10 -5
- package/src/maternal-and-child-health/partography/partograph.component.tsx +315 -371
- package/src/maternal-and-child-health/partography/partography.resource.ts +788 -230
- package/src/maternal-and-child-health/partography/partography.scss +68 -40
- package/src/maternal-and-child-health/partography/resources/cervix.resource.ts +79 -76
- package/src/maternal-and-child-health/partography/resources/uterine-contractions.resource.ts +33 -12
- package/src/maternal-and-child-health/partography/types/index.ts +94 -0
- package/translations/am.json +0 -8
- package/translations/en.json +0 -8
- package/translations/sw.json +0 -8
- package/dist/805.js.map +0 -1
|
@@ -1,25 +1,119 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
466
|
+
throw new Error('Patient UUID is required');
|
|
354
467
|
}
|
|
355
468
|
if (!finalLocationUuid) {
|
|
356
|
-
throw new Error(
|
|
469
|
+
throw new Error('Location UUID is required');
|
|
357
470
|
}
|
|
358
471
|
if (!encounterTypeUuid) {
|
|
359
|
-
throw new Error(
|
|
472
|
+
throw new Error('Encounter type UUID is required');
|
|
360
473
|
}
|
|
361
474
|
if (!observations || observations.length === 0) {
|
|
362
|
-
throw new Error(
|
|
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:
|
|
524
|
+
message:
|
|
525
|
+
t?.('partographyDataSavedSuccessfully', 'Partography data saved successfully') ||
|
|
526
|
+
'Partography data saved successfully',
|
|
416
527
|
encounter,
|
|
417
528
|
};
|
|
418
|
-
} catch (error
|
|
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:
|
|
425
|
-
'failedToSavePartographyData',
|
|
426
|
-
|
|
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
|
-
|
|
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:
|
|
622
|
+
concept: '162653AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
484
623
|
value: amnioticFluidValue,
|
|
485
624
|
obsDatetime,
|
|
486
625
|
});
|
|
487
626
|
}
|
|
488
|
-
const 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:
|
|
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:
|
|
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 '
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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['
|
|
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['
|
|
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:
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
|
912
|
-
id:
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
1356
|
+
|
|
1357
|
+
fetalHeartRateEntries.push(entry);
|
|
919
1358
|
}
|
|
920
1359
|
}
|
|
921
|
-
});
|
|
922
|
-
});
|
|
923
1360
|
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
|
|
930
|
-
export
|
|
931
|
-
|
|
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
|
+
}
|