@kenyaemr/esm-patient-clinical-view-app 5.4.2-pre.2823 → 5.4.2-pre.2825

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 (25) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/dist/{791.js → 343.js} +1 -1
  3. package/dist/343.js.map +1 -0
  4. package/dist/kenyaemr-esm-patient-clinical-view-app.js +3 -3
  5. package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +27 -27
  6. package/dist/main.js +17 -17
  7. package/dist/main.js.map +1 -1
  8. package/dist/routes.json +1 -1
  9. package/package.json +1 -1
  10. package/src/maternal-and-child-health/partography/forms/cervix-form.component.tsx +60 -9
  11. package/src/maternal-and-child-health/partography/forms/drugs-iv-fluids-form.component.tsx +21 -17
  12. package/src/maternal-and-child-health/partography/forms/fetal-heart-rate-form.component.tsx +70 -6
  13. package/src/maternal-and-child-health/partography/forms/membrane-amniotic-fluid-form.component.tsx +30 -7
  14. package/src/maternal-and-child-health/partography/forms/urine-test-form.component.tsx +63 -6
  15. package/src/maternal-and-child-health/partography/graphs/cervix-graph.component.tsx +100 -46
  16. package/src/maternal-and-child-health/partography/graphs/oxytocin-graph.component.tsx +2 -1
  17. package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph.component.tsx +231 -133
  18. package/src/maternal-and-child-health/partography/partograph.component.tsx +141 -30
  19. package/src/maternal-and-child-health/partography/partography-data-form.scss +31 -12
  20. package/src/maternal-and-child-health/partography/partography.scss +22 -86
  21. package/src/maternal-and-child-health/partography/resources/cervix.resource.ts +15 -1
  22. package/src/maternal-and-child-health/partography/resources/membrane-amniotic-fluid.resource.ts +170 -1
  23. package/src/maternal-and-child-health/partography/resources/oxytocin.resource.ts +88 -15
  24. package/src/maternal-and-child-health/partography/resources/temperature.resource.ts +138 -1
  25. 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.map((encounter) => {
38
- const observations = encounter.obs || [];
39
- const timeObs = observations.find((obs) => obs.concept.uuid === OXYTOCIN_FORM_CONCEPTS.time);
40
- const dropsObs = observations.find((obs) => obs.concept.uuid === OXYTOCIN_FORM_CONCEPTS.oxytocinDropsPerMinute);
41
- return {
42
- uuid: encounter.uuid,
43
- encounterDatetime: encounter.encounterDatetime,
44
- time: timeObs?.value ? String(timeObs.value) : null,
45
- dropsPerMinute: dropsObs?.value ? Number(dropsObs.value) : null,
46
- timeDisplay: timeObs?.value ? `${timeObs.value}` : null,
47
- };
48
- });
49
- const existingOxytocinEntries = transformedData
50
- .filter((data) => data.time !== null && data.dropsPerMinute !== null)
51
- .map((data) => ({ time: data.time!, dropsPerMinute: data.dropsPerMinute! }));
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
+ };