@kenyaemr/esm-patient-clinical-view-app 5.4.2-pre.2823 → 5.4.2-pre.2832
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/{791.js → 343.js} +1 -1
- package/dist/343.js.map +1 -0
- package/dist/kenyaemr-esm-patient-clinical-view-app.js +3 -3
- package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +27 -27
- package/dist/main.js +17 -17
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/maternal-and-child-health/partography/forms/cervix-form.component.tsx +60 -9
- package/src/maternal-and-child-health/partography/forms/drugs-iv-fluids-form.component.tsx +21 -17
- package/src/maternal-and-child-health/partography/forms/fetal-heart-rate-form.component.tsx +70 -6
- package/src/maternal-and-child-health/partography/forms/membrane-amniotic-fluid-form.component.tsx +30 -7
- package/src/maternal-and-child-health/partography/forms/urine-test-form.component.tsx +63 -6
- package/src/maternal-and-child-health/partography/graphs/cervix-graph.component.tsx +100 -46
- package/src/maternal-and-child-health/partography/graphs/oxytocin-graph.component.tsx +2 -1
- package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph.component.tsx +231 -133
- package/src/maternal-and-child-health/partography/partograph.component.tsx +141 -30
- package/src/maternal-and-child-health/partography/partography-data-form.scss +31 -12
- package/src/maternal-and-child-health/partography/partography.scss +22 -86
- package/src/maternal-and-child-health/partography/resources/cervix.resource.ts +15 -1
- package/src/maternal-and-child-health/partography/resources/membrane-amniotic-fluid.resource.ts +170 -1
- package/src/maternal-and-child-health/partography/resources/oxytocin.resource.ts +88 -15
- package/src/maternal-and-child-health/partography/resources/temperature.resource.ts +138 -1
- package/dist/791.js.map +0 -1
|
@@ -17,6 +17,38 @@ export interface SaveOxytocinDataResponse {
|
|
|
17
17
|
error?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Helper function to extract time from oxytocin observation value
|
|
22
|
+
*/
|
|
23
|
+
function extractTimeFromOxytocinObs(obsValue: any): string {
|
|
24
|
+
if (!obsValue) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof obsValue === 'string') {
|
|
29
|
+
// Handle "Time: HH:MM" format
|
|
30
|
+
if (obsValue.startsWith('Time:')) {
|
|
31
|
+
const match = obsValue.match(/Time:\s*(.+)/);
|
|
32
|
+
if (match) {
|
|
33
|
+
const time = match[1].trim();
|
|
34
|
+
// Validate HH:MM format
|
|
35
|
+
if (time.match(/^\d{1,2}:\d{2}$/)) {
|
|
36
|
+
return time;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Handle direct HH:MM format
|
|
41
|
+
if (obsValue.match(/^\d{1,2}:\d{2}$/)) {
|
|
42
|
+
return obsValue;
|
|
43
|
+
}
|
|
44
|
+
} else if (typeof obsValue === 'object' && obsValue && 'display' in obsValue) {
|
|
45
|
+
// Handle concept object with display property
|
|
46
|
+
return extractTimeFromOxytocinObs(obsValue.display);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
20
52
|
export function useOxytocinData(patientUuid: string) {
|
|
21
53
|
const { t } = useTranslation();
|
|
22
54
|
const encounterRepresentation =
|
|
@@ -34,31 +66,72 @@ export function useOxytocinData(patientUuid: string) {
|
|
|
34
66
|
const sortedEncounters = oxytocinData.sort(
|
|
35
67
|
(a, b) => new Date(b.encounterDatetime).getTime() - new Date(a.encounterDatetime).getTime(),
|
|
36
68
|
);
|
|
37
|
-
const transformedData = sortedEncounters
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
const transformedData = sortedEncounters
|
|
70
|
+
.map((encounter) => {
|
|
71
|
+
const observations = encounter.obs || [];
|
|
72
|
+
const timeObs = observations.find((obs) => obs.concept.uuid === OXYTOCIN_FORM_CONCEPTS.time);
|
|
73
|
+
const dropsObs = observations.find((obs) => obs.concept.uuid === OXYTOCIN_FORM_CONCEPTS.oxytocinDropsPerMinute);
|
|
74
|
+
|
|
75
|
+
// Only include encounters that have both time and drops data (indicating genuine oxytocin form submissions)
|
|
76
|
+
if (!timeObs?.value || !dropsObs?.value) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const timeValue = String(timeObs.value);
|
|
81
|
+
const dropsValue = Number(dropsObs.value);
|
|
82
|
+
|
|
83
|
+
// Additional validation: ensure the time value looks like oxytocin form data
|
|
84
|
+
const extractedTime = extractTimeFromOxytocinObs(timeValue);
|
|
85
|
+
if (!extractedTime || !extractedTime.match(/^\d{1,2}:\d{2}$/)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Ensure drops per minute is a valid number
|
|
90
|
+
if (isNaN(dropsValue) || dropsValue < 0) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
uuid: encounter.uuid,
|
|
96
|
+
encounterDatetime: encounter.encounterDatetime,
|
|
97
|
+
time: timeValue,
|
|
98
|
+
dropsPerMinute: dropsValue,
|
|
99
|
+
timeDisplay: extractedTime,
|
|
100
|
+
};
|
|
101
|
+
})
|
|
102
|
+
.filter(Boolean) as Array<{
|
|
103
|
+
uuid: string;
|
|
104
|
+
encounterDatetime: string;
|
|
105
|
+
time: string;
|
|
106
|
+
dropsPerMinute: number;
|
|
107
|
+
timeDisplay: string;
|
|
108
|
+
}>;
|
|
109
|
+
|
|
110
|
+
const existingOxytocinEntries = transformedData.map((data) => ({
|
|
111
|
+
time: data.time,
|
|
112
|
+
dropsPerMinute: data.dropsPerMinute,
|
|
113
|
+
}));
|
|
52
114
|
|
|
53
115
|
let localizedError = error;
|
|
54
116
|
if (error) {
|
|
55
117
|
localizedError = t('Failed to load oxytocin data');
|
|
56
118
|
}
|
|
57
119
|
|
|
120
|
+
// Transform data for TimePickerDropdown component (format: Array<{ hour: number; time: string }>)
|
|
121
|
+
const existingTimeEntries = transformedData.map((data) => {
|
|
122
|
+
const timeStr = data.timeDisplay; // Already extracted and validated above
|
|
123
|
+
const [hours] = timeStr.split(':').map(Number);
|
|
124
|
+
return {
|
|
125
|
+
hour: hours,
|
|
126
|
+
time: timeStr,
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
|
|
58
130
|
return {
|
|
59
131
|
encounters: sortedEncounters,
|
|
60
132
|
oxytocinData: transformedData,
|
|
61
133
|
existingOxytocinEntries,
|
|
134
|
+
existingTimeEntries, // For TimePickerDropdown component
|
|
62
135
|
isLoading,
|
|
63
136
|
error: localizedError,
|
|
64
137
|
mutate,
|
|
@@ -1,12 +1,105 @@
|
|
|
1
1
|
import useSWR from 'swr';
|
|
2
|
-
import { usePartographyEncounters } from '../partography.resource';
|
|
2
|
+
import { usePartographyEncounters, usePartographyData } from '../partography.resource';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { useMemo } from 'react';
|
|
4
5
|
|
|
5
6
|
export function useTemperatureData(patientUuid: string) {
|
|
6
7
|
const { encounters, isLoading, error, mutate } = usePartographyEncounters(patientUuid, 'temperature');
|
|
7
8
|
return { data: encounters, isLoading, error, mutate };
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
export interface TemperatureTimeEntry {
|
|
12
|
+
hour: number;
|
|
13
|
+
time: string;
|
|
14
|
+
encounterDatetime: string;
|
|
15
|
+
temperature?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UseTemperatureFormDataResult {
|
|
19
|
+
temperatureData: any[];
|
|
20
|
+
temperatureTimeEntries: TemperatureTimeEntry[];
|
|
21
|
+
isLoading: boolean;
|
|
22
|
+
error: any;
|
|
23
|
+
mutate: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Custom hook for temperature-specific data isolation
|
|
28
|
+
* This ensures temperature form validations only consider temperature data
|
|
29
|
+
*/
|
|
30
|
+
export const useTemperatureFormData = (patientUuid: string): UseTemperatureFormDataResult => {
|
|
31
|
+
const { data: temperatureEncounters = [], isLoading, error, mutate } = usePartographyData(patientUuid, 'temperature');
|
|
32
|
+
|
|
33
|
+
// Extract temperature-specific time entries for form validation
|
|
34
|
+
const temperatureTimeEntries = useMemo(() => {
|
|
35
|
+
if (!temperatureEncounters || temperatureEncounters.length === 0) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return temperatureEncounters
|
|
40
|
+
.map((encounter) => {
|
|
41
|
+
// Find temperature observation
|
|
42
|
+
const tempObs = encounter.obs.find((obs) => obs.concept.uuid === '5088AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
|
|
43
|
+
|
|
44
|
+
// Find time observation
|
|
45
|
+
const timeObs = encounter.obs.find(
|
|
46
|
+
(obs) =>
|
|
47
|
+
obs.concept.uuid === '160632AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' &&
|
|
48
|
+
typeof obs.value === 'string' &&
|
|
49
|
+
obs.value.startsWith('Time:'),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (!tempObs || !timeObs) {
|
|
53
|
+
return null; // Skip if no temperature or time data
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Extract time from observation value
|
|
57
|
+
let time = '';
|
|
58
|
+
if (timeObs && typeof timeObs.value === 'string') {
|
|
59
|
+
const timeMatch = timeObs.value.match(/Time:\s*(.+)/);
|
|
60
|
+
if (timeMatch) {
|
|
61
|
+
time = timeMatch[1].trim();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!time || !time.match(/^\d{1,2}:\d{2}$/)) {
|
|
66
|
+
return null; // Skip if invalid time format
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Convert time to hour value for progressive validation
|
|
70
|
+
const [hours, minutes] = time.split(':').map(Number);
|
|
71
|
+
const hourValue = hours + minutes / 60; // Convert to decimal hour
|
|
72
|
+
|
|
73
|
+
// Extract temperature value
|
|
74
|
+
let temperature = tempObs?.value ?? null;
|
|
75
|
+
if (typeof temperature === 'string') {
|
|
76
|
+
const parsed = parseFloat(temperature);
|
|
77
|
+
temperature = isNaN(parsed) ? null : parsed;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
hour: hourValue,
|
|
82
|
+
time: time,
|
|
83
|
+
encounterDatetime: encounter.encounterDatetime,
|
|
84
|
+
temperature: typeof temperature === 'number' ? temperature : undefined,
|
|
85
|
+
};
|
|
86
|
+
})
|
|
87
|
+
.filter((entry) => entry !== null)
|
|
88
|
+
.sort((a, b) => {
|
|
89
|
+
// Sort by encounter datetime for chronological order
|
|
90
|
+
return new Date(a!.encounterDatetime).getTime() - new Date(b!.encounterDatetime).getTime();
|
|
91
|
+
}) as TemperatureTimeEntry[];
|
|
92
|
+
}, [temperatureEncounters]);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
temperatureData: temperatureEncounters,
|
|
96
|
+
temperatureTimeEntries,
|
|
97
|
+
isLoading,
|
|
98
|
+
error,
|
|
99
|
+
mutate,
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
10
103
|
import { PARTOGRAPHY_CONCEPTS } from '../types';
|
|
11
104
|
import { toOmrsIsoString } from '@openmrs/esm-framework';
|
|
12
105
|
|
|
@@ -82,3 +175,47 @@ export function transformTemperatureEncounterToTableData(encounters: any[]): any
|
|
|
82
175
|
});
|
|
83
176
|
return tableData;
|
|
84
177
|
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Helper function to convert time string to decimal hour
|
|
181
|
+
* Example: "14:30" -> 14.5
|
|
182
|
+
*/
|
|
183
|
+
export const convertTimeToHour = (timeString: string): number => {
|
|
184
|
+
if (!timeString || !timeString.match(/^\d{1,2}:\d{2}$/)) {
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const [hours, minutes] = timeString.split(':').map(Number);
|
|
189
|
+
return hours + minutes / 60;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Helper function to extract time from observation value
|
|
194
|
+
* Handles various time formats in OpenMRS observations
|
|
195
|
+
*/
|
|
196
|
+
export const extractTimeFromTemperatureObs = (obsValue: any): string => {
|
|
197
|
+
if (!obsValue) {
|
|
198
|
+
return '';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (typeof obsValue === 'string') {
|
|
202
|
+
// Handle "Time: HH:MM" format
|
|
203
|
+
if (obsValue.startsWith('Time:')) {
|
|
204
|
+
const match = obsValue.match(/Time:\s*(.+)/);
|
|
205
|
+
if (match) {
|
|
206
|
+
const time = match[1].trim();
|
|
207
|
+
// Validate HH:MM format
|
|
208
|
+
if (time.match(/^\d{1,2}:\d{2}$/)) {
|
|
209
|
+
return time;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Handle direct HH:MM format
|
|
215
|
+
if (obsValue.match(/^\d{1,2}:\d{2}$/)) {
|
|
216
|
+
return obsValue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return '';
|
|
221
|
+
};
|