@kenyaemr/esm-patient-clinical-view-app 5.4.2-pre.2592 → 5.4.2-pre.2598
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 +6 -6
- package/dist/127.js +1 -1
- package/dist/152.js +3 -3
- package/dist/152.js.map +1 -1
- package/dist/481.js +66 -0
- package/dist/481.js.map +1 -0
- package/dist/671.js +1 -1
- package/dist/671.js.map +1 -1
- package/dist/941.js +1 -0
- package/dist/941.js.map +1 -0
- package/dist/kenyaemr-esm-patient-clinical-view-app.js +2 -2
- package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +55 -55
- package/dist/kenyaemr-esm-patient-clinical-view-app.js.map +1 -1
- package/dist/main.js +87 -14
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/config-schema.ts +144 -0
- package/src/index.ts +2 -2
- package/src/maternal-and-child-health/partography/labour-delivery.scss +6 -7
- package/src/maternal-and-child-health/partography/partograph.component.tsx +487 -151
- package/src/maternal-and-child-health/partography/partography-data-form.component.tsx +434 -0
- package/src/maternal-and-child-health/partography/partography-data-form.scss +50 -0
- package/src/maternal-and-child-health/partography/partography-link.component.tsx +21 -0
- package/src/maternal-and-child-health/partography/partography.resource.ts +1024 -0
- package/src/maternal-and-child-health/partography/partography.scss +378 -0
- package/src/maternal-and-child-health/partography/types/index.ts +980 -0
- package/translations/en.json +11 -1
- package/dist/287.js +0 -1
- package/dist/287.js.map +0 -1
- package/dist/98.js +0 -1
- package/dist/98.js.map +0 -1
- package/src/maternal-and-child-health/partography/cervical-dilation.component.tsx +0 -16
- package/src/maternal-and-child-health/partography/contraction-level.component.tsx +0 -16
- package/src/maternal-and-child-health/partography/descent-of-head.component.tsx +0 -16
- package/src/maternal-and-child-health/partography/foetal-heart-rate.component.tsx +0 -17
- package/src/maternal-and-child-health/partography/membrane-amniotic-fluid-moulding.component.tsx +0 -17
- package/src/maternal-and-child-health/partography/partograph-chart.scss +0 -94
- package/src/maternal-and-child-health/partography/partograph-chart.tsx +0 -176
|
@@ -0,0 +1,1024 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl, toOmrsIsoString, useConfig } from '@openmrs/esm-framework';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
PARTOGRAPHY_CONCEPTS,
|
|
6
|
+
PARTOGRAPHY_ENCOUNTER_TYPES,
|
|
7
|
+
getPartographyUnit,
|
|
8
|
+
type OpenMRSResponse,
|
|
9
|
+
type PartographyObservation,
|
|
10
|
+
type PartographyEncounter,
|
|
11
|
+
type PartographyGraphType,
|
|
12
|
+
} from './types';
|
|
13
|
+
import { configSchema, type ConfigObject } from '../../config-schema';
|
|
14
|
+
|
|
15
|
+
export type { PartographyObservation, PartographyEncounter };
|
|
16
|
+
const defaultPartographyConfig = configSchema.partography._default;
|
|
17
|
+
const getConceptMappingsForGraphType = (graphType: string): string[] => {
|
|
18
|
+
return defaultPartographyConfig.conceptMappings[graphType] || [];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const getFallbackEncounterTypes = (graphType: string): string[] => {
|
|
22
|
+
return (
|
|
23
|
+
defaultPartographyConfig.fallbackEncounterMapping[graphType] || defaultPartographyConfig.defaultFallbackSequence
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const getStationDisplay = (conceptUuid: string): string => {
|
|
28
|
+
return defaultPartographyConfig.stationDisplayMapping[conceptUuid] || conceptUuid;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getStationValue = (conceptUuid: string): number => {
|
|
32
|
+
return defaultPartographyConfig.stationValueMapping[conceptUuid] ?? 0;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getProgressEventInfo = (conceptUuid: string): { name: string; unit: string } | null => {
|
|
36
|
+
return defaultPartographyConfig.progressEventConceptNames[conceptUuid] || null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const getGraphTypeDisplayName = (graphType: string): string => {
|
|
40
|
+
return (
|
|
41
|
+
defaultPartographyConfig.graphTypeDisplayNames[graphType] ||
|
|
42
|
+
graphType.replace('-', ' ').replace(/\b\w/g, (l) => l.toUpperCase())
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getTestGraphTypes = (): string[] => {
|
|
47
|
+
return defaultPartographyConfig.testData.testGraphTypes;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getSampleDataPoints = (): Array<{ value: number; time: string }> => {
|
|
51
|
+
return defaultPartographyConfig.testData.sampleDataPoints;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const getTestDataConfig = () => {
|
|
55
|
+
return defaultPartographyConfig.testData;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const doesObservationMapToGraphType = (obsConceptUuid: string, targetGraphType: string): boolean => {
|
|
59
|
+
const conceptUuids = getConceptMappingsForGraphType(targetGraphType);
|
|
60
|
+
return conceptUuids.includes(obsConceptUuid);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const generateStorageKey = (patientUuid: string, graphType: string): string => {
|
|
64
|
+
return `${defaultPartographyConfig.storage.storageKeyPrefix}${patientUuid}_${graphType}`;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const generateCacheKey = (patientUuid: string, graphType: string): string => {
|
|
68
|
+
return `${defaultPartographyConfig.storage.cacheKeyPrefix}${patientUuid}_${graphType}`;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const getDefaultLocationUuid = (): string => {
|
|
72
|
+
return defaultPartographyConfig.defaultLocationUuid;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const getDefaultEncounterProviderRole = (): string => {
|
|
76
|
+
return defaultPartographyConfig.defaultEncounterProviderRole;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const getRetryFallbackTypes = (): string[] => {
|
|
80
|
+
return defaultPartographyConfig.retryFallbackTypes;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const getTimeConfig = () => {
|
|
84
|
+
return defaultPartographyConfig.timeConfig;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getStorageConfig = () => {
|
|
88
|
+
return defaultPartographyConfig.storage;
|
|
89
|
+
};
|
|
90
|
+
export const usePartographyConfig = () => {
|
|
91
|
+
const config = useConfig<ConfigObject>();
|
|
92
|
+
return config.partography;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
let encounterTypeCache: { [key: string]: string } = {};
|
|
96
|
+
|
|
97
|
+
async function discoverEncounterTypes(): Promise<{ [key: string]: string }> {
|
|
98
|
+
if (Object.keys(encounterTypeCache).length > 0) {
|
|
99
|
+
return encounterTypeCache;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const response = await openmrsFetch(`${restBaseUrl}/encountertype?v=default`);
|
|
104
|
+
|
|
105
|
+
if (response.ok) {
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
const encounterTypes = data.results || [];
|
|
108
|
+
encounterTypes.forEach((et) => {
|
|
109
|
+
const normalizedName = et.name.toLowerCase().replace(/\s+/g, '-');
|
|
110
|
+
encounterTypeCache[normalizedName] = et.uuid;
|
|
111
|
+
if (et.display) {
|
|
112
|
+
const normalizedDisplay = et.display.toLowerCase().replace(/\s+/g, '-');
|
|
113
|
+
encounterTypeCache[normalizedDisplay] = et.uuid;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return encounterTypeCache;
|
|
118
|
+
} else {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function getEncounterTypeForGraph(graphType: string): Promise<string | null> {
|
|
127
|
+
const predefinedType = PARTOGRAPHY_ENCOUNTER_TYPES[graphType];
|
|
128
|
+
if (predefinedType) {
|
|
129
|
+
return predefinedType;
|
|
130
|
+
}
|
|
131
|
+
const availableTypes = await discoverEncounterTypes();
|
|
132
|
+
const specificType = availableTypes[graphType];
|
|
133
|
+
if (specificType) {
|
|
134
|
+
return specificType;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const pattern of defaultPartographyConfig.encounterPatterns) {
|
|
138
|
+
if (availableTypes[pattern]) {
|
|
139
|
+
return availableTypes[pattern];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const fallbacks = getFallbackEncounterTypes(graphType);
|
|
144
|
+
|
|
145
|
+
for (const fallback of fallbacks) {
|
|
146
|
+
if (availableTypes[fallback]) {
|
|
147
|
+
return availableTypes[fallback];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const anyType = Object.values(availableTypes)[0];
|
|
152
|
+
if (anyType) {
|
|
153
|
+
return anyType;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function usePartographyEncounters(patientUuid: string, graphType: string) {
|
|
160
|
+
const customRep =
|
|
161
|
+
'custom:(uuid,encounterDatetime,encounterType:(uuid,display),obs:(uuid,concept:(uuid,display),value,obsDatetime))';
|
|
162
|
+
|
|
163
|
+
const [encounterTypeUuid, setEncounterTypeUuid] = useState<string | null>(null);
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (patientUuid && graphType) {
|
|
166
|
+
getEncounterTypeForGraph(graphType).then(setEncounterTypeUuid);
|
|
167
|
+
}
|
|
168
|
+
}, [patientUuid, graphType]);
|
|
169
|
+
const apiUrl =
|
|
170
|
+
encounterTypeUuid && patientUuid
|
|
171
|
+
? `${restBaseUrl}/encounter?patient=${patientUuid}&encounterType=${encounterTypeUuid}&v=${customRep}`
|
|
172
|
+
: null;
|
|
173
|
+
|
|
174
|
+
const { data, error, isLoading, mutate } = useSWR<OpenMRSResponse<PartographyEncounter>>(
|
|
175
|
+
apiUrl,
|
|
176
|
+
async (url: string) => {
|
|
177
|
+
const response = await openmrsFetch(url);
|
|
178
|
+
return response.data;
|
|
179
|
+
},
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const encounters = data?.results ?? [];
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
encounters,
|
|
186
|
+
isLoading: isLoading || !encounterTypeUuid,
|
|
187
|
+
error,
|
|
188
|
+
mutate,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function usePartographyData(patientUuid: string, graphType: string) {
|
|
193
|
+
const { encounters, isLoading, error, mutate } = usePartographyEncounters(patientUuid, graphType);
|
|
194
|
+
|
|
195
|
+
const localDataFallback = useSWR(
|
|
196
|
+
!isLoading && encounters.length === 0 ? `partography_local_${patientUuid}_${graphType}` : null,
|
|
197
|
+
() => {
|
|
198
|
+
const localData = loadPartographyData(patientUuid, graphType);
|
|
199
|
+
return localData;
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const finalData = encounters.length > 0 ? encounters : localDataFallback.data || [];
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
data: finalData,
|
|
207
|
+
isLoading: isLoading || localDataFallback.isLoading,
|
|
208
|
+
error: error || localDataFallback.error,
|
|
209
|
+
mutate: async () => {
|
|
210
|
+
await mutate();
|
|
211
|
+
if (localDataFallback.mutate) {
|
|
212
|
+
await localDataFallback.mutate();
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function loadPartographyData(patientUuid: string, graphType: string): PartographyEncounter[] {
|
|
219
|
+
try {
|
|
220
|
+
const storageKey = generateStorageKey(patientUuid, graphType);
|
|
221
|
+
const localData = JSON.parse(localStorage.getItem(storageKey) || '[]');
|
|
222
|
+
|
|
223
|
+
const localEncounters = localData.map((item) => ({
|
|
224
|
+
uuid: item.id,
|
|
225
|
+
encounterDatetime: item.timestamp,
|
|
226
|
+
encounterType: { uuid: 'localStorage', display: 'Partography (Local Storage)' },
|
|
227
|
+
obs: [
|
|
228
|
+
{
|
|
229
|
+
uuid: `obs_${item.id}`,
|
|
230
|
+
concept: { uuid: 'partography-data', display: 'Partography Data' },
|
|
231
|
+
value: JSON.stringify({
|
|
232
|
+
graphType: item.graphType,
|
|
233
|
+
timestamp: item.timestamp,
|
|
234
|
+
data: item.data,
|
|
235
|
+
}),
|
|
236
|
+
obsDatetime: item.timestamp,
|
|
237
|
+
encounter: {
|
|
238
|
+
uuid: item.id,
|
|
239
|
+
encounterType: { uuid: 'localStorage', display: 'Partography (Local Storage)' },
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
}));
|
|
244
|
+
|
|
245
|
+
return localEncounters.sort(
|
|
246
|
+
(a, b) => new Date(b.encounterDatetime).getTime() - new Date(a.encounterDatetime).getTime(),
|
|
247
|
+
);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function createPartographyEncounter(
|
|
254
|
+
patientUuid: string,
|
|
255
|
+
graphType: string,
|
|
256
|
+
formData: any,
|
|
257
|
+
locationUuid?: string,
|
|
258
|
+
providerUuid?: string,
|
|
259
|
+
t?: (key: string, fallback?: string) => string,
|
|
260
|
+
): Promise<{ success: boolean; message: string; encounter?: PartographyEncounter }> {
|
|
261
|
+
try {
|
|
262
|
+
const observations = buildObservations(graphType, formData);
|
|
263
|
+
|
|
264
|
+
if (observations.length === 0) {
|
|
265
|
+
throw new Error(t?.('noValidObservations', 'No valid observations to save') || 'No valid observations to save');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const encounterTypeUuid = await getEncounterTypeForGraph(graphType);
|
|
269
|
+
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
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const finalLocationUuid = locationUuid || getDefaultLocationUuid();
|
|
279
|
+
|
|
280
|
+
const timeConfig = getTimeConfig();
|
|
281
|
+
const encounterDatetime = toOmrsIsoString(new Date(Date.now() - timeConfig.defaultEncounterOffset));
|
|
282
|
+
|
|
283
|
+
const encounterPayload: any = {
|
|
284
|
+
patient: patientUuid,
|
|
285
|
+
location: finalLocationUuid,
|
|
286
|
+
encounterDatetime: encounterDatetime,
|
|
287
|
+
obs: observations,
|
|
288
|
+
encounterType: encounterTypeUuid,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
let finalProviderUuid = providerUuid;
|
|
292
|
+
|
|
293
|
+
if (!finalProviderUuid) {
|
|
294
|
+
try {
|
|
295
|
+
const sessionResponse = await openmrsFetch(`${restBaseUrl}/session`);
|
|
296
|
+
if (sessionResponse.ok) {
|
|
297
|
+
const sessionData = await sessionResponse.json();
|
|
298
|
+
const currentUser = sessionData.user;
|
|
299
|
+
if (currentUser && currentUser.person) {
|
|
300
|
+
const providerResponse = await openmrsFetch(
|
|
301
|
+
`${restBaseUrl}/provider?person=${currentUser.person.uuid}&v=default`,
|
|
302
|
+
);
|
|
303
|
+
if (providerResponse.ok) {
|
|
304
|
+
const providerData = await providerResponse.json();
|
|
305
|
+
if (providerData.results && providerData.results.length > 0) {
|
|
306
|
+
finalProviderUuid = providerData.results[0].uuid;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {}
|
|
312
|
+
|
|
313
|
+
if (!finalProviderUuid) {
|
|
314
|
+
try {
|
|
315
|
+
const anyProviderResponse = await openmrsFetch(`${restBaseUrl}/provider?v=default&limit=1`);
|
|
316
|
+
if (anyProviderResponse.ok) {
|
|
317
|
+
const anyProviderData = await anyProviderResponse.json();
|
|
318
|
+
if (anyProviderData.results && anyProviderData.results.length > 0) {
|
|
319
|
+
finalProviderUuid = anyProviderData.results[0].uuid;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (finalProviderUuid) {
|
|
327
|
+
try {
|
|
328
|
+
const providerValidationResponse = await openmrsFetch(`${restBaseUrl}/provider/${finalProviderUuid}?v=default`);
|
|
329
|
+
if (providerValidationResponse.ok) {
|
|
330
|
+
encounterPayload.encounterProviders = [
|
|
331
|
+
{
|
|
332
|
+
provider: finalProviderUuid,
|
|
333
|
+
encounterRole: getDefaultEncounterProviderRole(),
|
|
334
|
+
voided: false,
|
|
335
|
+
},
|
|
336
|
+
];
|
|
337
|
+
}
|
|
338
|
+
} catch (validationError) {}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const response = await openmrsFetch(`${restBaseUrl}/encounter`, {
|
|
342
|
+
method: 'POST',
|
|
343
|
+
headers: { 'Content-Type': 'application/json' },
|
|
344
|
+
body: JSON.stringify(encounterPayload),
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if (!response.ok) {
|
|
348
|
+
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const responseText = await response.text();
|
|
352
|
+
if (responseText) {
|
|
353
|
+
try {
|
|
354
|
+
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
|
+
if (detailedError.error) {
|
|
461
|
+
if (detailedError.error.message) {
|
|
462
|
+
errorMessage += ` - ${detailedError.error.message}`;
|
|
463
|
+
}
|
|
464
|
+
if (detailedError.error.detail) {
|
|
465
|
+
errorMessage += ` (${detailedError.error.detail})`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
} catch (parseError) {
|
|
469
|
+
errorMessage += ` - Raw Response: ${responseText}`;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
} catch (e) {}
|
|
473
|
+
|
|
474
|
+
throw new Error(errorMessage);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const encounter = await response.json();
|
|
478
|
+
|
|
479
|
+
const cacheKey = generateCacheKey(patientUuid, graphType);
|
|
480
|
+
try {
|
|
481
|
+
const { mutate: globalMutate } = await import('swr');
|
|
482
|
+
await globalMutate(cacheKey);
|
|
483
|
+
const timeConfig = getTimeConfig();
|
|
484
|
+
setTimeout(async () => {
|
|
485
|
+
await globalMutate(cacheKey);
|
|
486
|
+
}, timeConfig.cacheInvalidationDelay);
|
|
487
|
+
} catch (mutateError) {}
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
await saveToLocalStorage(patientUuid, graphType, formData, encounter.uuid);
|
|
491
|
+
} catch (localError) {}
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
success: true,
|
|
495
|
+
message:
|
|
496
|
+
t?.('partographyDataSavedSuccessfully', 'Partography data saved successfully') ||
|
|
497
|
+
'Partography data saved successfully',
|
|
498
|
+
encounter,
|
|
499
|
+
};
|
|
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
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function saveToLocalStorage(
|
|
523
|
+
patientUuid: string,
|
|
524
|
+
graphType: string,
|
|
525
|
+
formData: any,
|
|
526
|
+
encounterUuid?: string,
|
|
527
|
+
): Promise<void> {
|
|
528
|
+
const entryId = encounterUuid || `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
529
|
+
const timestamp = toOmrsIsoString(new Date());
|
|
530
|
+
|
|
531
|
+
const dataEntry = {
|
|
532
|
+
id: entryId,
|
|
533
|
+
timestamp,
|
|
534
|
+
graphType,
|
|
535
|
+
data: formData,
|
|
536
|
+
encounterUuid,
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const storageKey = generateStorageKey(patientUuid, graphType);
|
|
540
|
+
const existingData = JSON.parse(localStorage.getItem(storageKey) || '[]');
|
|
541
|
+
|
|
542
|
+
existingData.push(dataEntry);
|
|
543
|
+
|
|
544
|
+
const storageConfig = getStorageConfig();
|
|
545
|
+
if (existingData.length > storageConfig.maxLocalEntries) {
|
|
546
|
+
existingData.splice(0, existingData.length - storageConfig.maxLocalEntries);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
localStorage.setItem(storageKey, JSON.stringify(existingData));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function buildObservations(graphType: string, formData: any): any[] {
|
|
553
|
+
const observations = [];
|
|
554
|
+
const timeConfig = getTimeConfig();
|
|
555
|
+
const obsDatetime = toOmrsIsoString(new Date(Date.now() - timeConfig.defaultEncounterOffset));
|
|
556
|
+
|
|
557
|
+
switch (graphType) {
|
|
558
|
+
case 'fetal-heart-rate':
|
|
559
|
+
if (formData.value || formData.measurementValue) {
|
|
560
|
+
observations.push({
|
|
561
|
+
concept: PARTOGRAPHY_CONCEPTS['fetal-heart-rate'],
|
|
562
|
+
value: parseFloat(formData.value || formData.measurementValue),
|
|
563
|
+
obsDatetime,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
break;
|
|
567
|
+
|
|
568
|
+
case 'cervical-dilation':
|
|
569
|
+
if (formData.value || formData.measurementValue) {
|
|
570
|
+
observations.push({
|
|
571
|
+
concept: PARTOGRAPHY_CONCEPTS['cervical-dilation'],
|
|
572
|
+
value: parseFloat(formData.value || formData.measurementValue),
|
|
573
|
+
obsDatetime,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (formData.amnioticFluid) {
|
|
578
|
+
observations.push({
|
|
579
|
+
concept: PARTOGRAPHY_CONCEPTS['amniotic-fluid'],
|
|
580
|
+
value: formData.amnioticFluid,
|
|
581
|
+
obsDatetime,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (formData.moulding) {
|
|
586
|
+
observations.push({
|
|
587
|
+
concept: PARTOGRAPHY_CONCEPTS['moulding'],
|
|
588
|
+
value: formData.moulding,
|
|
589
|
+
obsDatetime,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
break;
|
|
593
|
+
|
|
594
|
+
case 'descent-of-head':
|
|
595
|
+
if (formData.value || formData.measurementValue) {
|
|
596
|
+
let conceptValue = formData.value || formData.measurementValue;
|
|
597
|
+
|
|
598
|
+
if (formData.conceptUuid) {
|
|
599
|
+
conceptValue = formData.conceptUuid;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
observations.push({
|
|
603
|
+
concept: PARTOGRAPHY_CONCEPTS['descent-of-head'],
|
|
604
|
+
value: conceptValue,
|
|
605
|
+
obsDatetime,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
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),
|
|
625
|
+
obsDatetime,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
break;
|
|
629
|
+
|
|
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
|
+
});
|
|
644
|
+
}
|
|
645
|
+
break;
|
|
646
|
+
|
|
647
|
+
case 'temperature':
|
|
648
|
+
if (formData.value || formData.measurementValue) {
|
|
649
|
+
observations.push({
|
|
650
|
+
concept: PARTOGRAPHY_CONCEPTS['temperature'],
|
|
651
|
+
value: parseFloat(formData.value || formData.measurementValue),
|
|
652
|
+
obsDatetime,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
break;
|
|
656
|
+
|
|
657
|
+
case 'urine-analysis':
|
|
658
|
+
if (formData.proteinLevel) {
|
|
659
|
+
observations.push({
|
|
660
|
+
concept: PARTOGRAPHY_CONCEPTS['protein-level'],
|
|
661
|
+
value: formData.proteinLevel,
|
|
662
|
+
obsDatetime,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
if (formData.glucoseLevel) {
|
|
666
|
+
observations.push({
|
|
667
|
+
concept: PARTOGRAPHY_CONCEPTS['glucose-level'],
|
|
668
|
+
value: formData.glucoseLevel,
|
|
669
|
+
obsDatetime,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
if (formData.ketoneLevel) {
|
|
673
|
+
observations.push({
|
|
674
|
+
concept: PARTOGRAPHY_CONCEPTS['ketone-level'],
|
|
675
|
+
value: formData.ketoneLevel,
|
|
676
|
+
obsDatetime,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
break;
|
|
680
|
+
|
|
681
|
+
case 'drugs-fluids':
|
|
682
|
+
if (formData.medication) {
|
|
683
|
+
observations.push({
|
|
684
|
+
concept: PARTOGRAPHY_CONCEPTS['medication'],
|
|
685
|
+
value: formData.medication,
|
|
686
|
+
obsDatetime,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (formData.dosage) {
|
|
690
|
+
observations.push({
|
|
691
|
+
concept: PARTOGRAPHY_CONCEPTS['dosage'],
|
|
692
|
+
value: formData.dosage,
|
|
693
|
+
obsDatetime,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
break;
|
|
697
|
+
|
|
698
|
+
case 'progress-events':
|
|
699
|
+
if (formData.eventType) {
|
|
700
|
+
observations.push({
|
|
701
|
+
concept: PARTOGRAPHY_CONCEPTS['event-type'],
|
|
702
|
+
value: formData.eventType,
|
|
703
|
+
obsDatetime,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
if (formData.eventDescription) {
|
|
707
|
+
observations.push({
|
|
708
|
+
concept: PARTOGRAPHY_CONCEPTS['event-description'],
|
|
709
|
+
value: formData.eventDescription,
|
|
710
|
+
obsDatetime,
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
break;
|
|
714
|
+
|
|
715
|
+
default:
|
|
716
|
+
if (formData.value || formData.measurementValue) {
|
|
717
|
+
const conceptUuid = PARTOGRAPHY_CONCEPTS[graphType];
|
|
718
|
+
if (conceptUuid) {
|
|
719
|
+
observations.push({
|
|
720
|
+
concept: conceptUuid,
|
|
721
|
+
value: parseFloat(formData.value || formData.measurementValue),
|
|
722
|
+
obsDatetime,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return observations;
|
|
730
|
+
}
|
|
731
|
+
|
|
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
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function getGroupNameForGraph(graphType: string): string {
|
|
849
|
+
return getGraphTypeDisplayName(graphType);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export function createTestPartographyData(patientUuid: string) {
|
|
853
|
+
const testConfig = getTestDataConfig();
|
|
854
|
+
const testGraphTypes = testConfig.testGraphTypes;
|
|
855
|
+
const sampleDataPoints = testConfig.sampleDataPoints;
|
|
856
|
+
const valueIncrement = testConfig.valueIncrement;
|
|
857
|
+
const bloodPressureDecrement = testConfig.bloodPressureDecrement;
|
|
858
|
+
|
|
859
|
+
testGraphTypes.forEach(async (graphType, index) => {
|
|
860
|
+
const enhancedDataPoints = sampleDataPoints.map((point) => ({
|
|
861
|
+
...point,
|
|
862
|
+
value: point.value + index * valueIncrement,
|
|
863
|
+
}));
|
|
864
|
+
|
|
865
|
+
for (const dataPoint of enhancedDataPoints) {
|
|
866
|
+
const formData =
|
|
867
|
+
graphType === 'blood-pressure'
|
|
868
|
+
? {
|
|
869
|
+
systolic: dataPoint.value,
|
|
870
|
+
diastolic: dataPoint.value - bloodPressureDecrement,
|
|
871
|
+
time: dataPoint.time,
|
|
872
|
+
}
|
|
873
|
+
: { value: dataPoint.value, time: dataPoint.time };
|
|
874
|
+
|
|
875
|
+
try {
|
|
876
|
+
await saveToLocalStorage(patientUuid, graphType, formData);
|
|
877
|
+
} catch (error) {}
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
export function transformEncounterToTableData(
|
|
883
|
+
encounters: PartographyEncounter[],
|
|
884
|
+
graphType: string,
|
|
885
|
+
t?: (key: string, fallback?: string) => string,
|
|
886
|
+
): any[] {
|
|
887
|
+
const tableData = [];
|
|
888
|
+
|
|
889
|
+
const getUnitForGraphType = (type: string): string => {
|
|
890
|
+
return getPartographyUnit(type as PartographyGraphType) || '';
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
interface ObservationConcept {
|
|
894
|
+
uuid: string;
|
|
895
|
+
display?: string;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
interface ObservationForMapping {
|
|
899
|
+
concept?: ObservationConcept;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const mapObservationToGraphType = (obs: ObservationForMapping, targetGraphType: string): boolean => {
|
|
903
|
+
return doesObservationMapToGraphType(obs.concept?.uuid || '', targetGraphType);
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
encounters.forEach((encounter, index) => {
|
|
907
|
+
const encounterDate = new Date(encounter.encounterDatetime);
|
|
908
|
+
const dateTime = `${encounterDate.toLocaleDateString()} — ${encounterDate.toLocaleTimeString('en-US', {
|
|
909
|
+
hour: '2-digit',
|
|
910
|
+
minute: '2-digit',
|
|
911
|
+
hour12: false,
|
|
912
|
+
})}`;
|
|
913
|
+
|
|
914
|
+
encounter.obs.forEach((obs, obsIndex) => {
|
|
915
|
+
try {
|
|
916
|
+
if (typeof obs.value === 'string') {
|
|
917
|
+
const parsedData = JSON.parse(obs.value);
|
|
918
|
+
|
|
919
|
+
if (parsedData.graphType === graphType && parsedData.data) {
|
|
920
|
+
const formData = parsedData.data;
|
|
921
|
+
let value = formData.value || formData.measurementValue;
|
|
922
|
+
|
|
923
|
+
if (graphType === 'descent-of-head' && formData.conceptUuid) {
|
|
924
|
+
value = formData.conceptUuid;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (value) {
|
|
928
|
+
let displayValue;
|
|
929
|
+
|
|
930
|
+
if (graphType === 'descent-of-head') {
|
|
931
|
+
let valueToMap;
|
|
932
|
+
if (typeof value === 'object' && value !== null) {
|
|
933
|
+
const valueObj = value as any;
|
|
934
|
+
if (valueObj.conceptUuid) {
|
|
935
|
+
valueToMap = valueObj.conceptUuid;
|
|
936
|
+
} else if (valueObj.value) {
|
|
937
|
+
valueToMap = valueObj.value;
|
|
938
|
+
} else {
|
|
939
|
+
valueToMap = String(value);
|
|
940
|
+
}
|
|
941
|
+
} else {
|
|
942
|
+
valueToMap = String(value);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
displayValue = getStationDisplay(valueToMap);
|
|
946
|
+
} else {
|
|
947
|
+
const numericValue = parseFloat(String(value));
|
|
948
|
+
if (!isNaN(numericValue)) {
|
|
949
|
+
displayValue = numericValue.toFixed(1);
|
|
950
|
+
} else {
|
|
951
|
+
displayValue = String(value);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const rowData = {
|
|
956
|
+
id: `${graphType}-${index}-${obsIndex}`,
|
|
957
|
+
dateTime,
|
|
958
|
+
value: displayValue,
|
|
959
|
+
unit: getUnitForGraphType(graphType),
|
|
960
|
+
};
|
|
961
|
+
tableData.push(rowData);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
} else {
|
|
965
|
+
throw new Error('Not a JSON string');
|
|
966
|
+
}
|
|
967
|
+
} catch (e) {
|
|
968
|
+
if (mapObservationToGraphType(obs, graphType)) {
|
|
969
|
+
let conceptName = obs.concept?.display || t?.('unknown', 'Unknown') || 'Unknown';
|
|
970
|
+
let unit = getUnitForGraphType(graphType);
|
|
971
|
+
|
|
972
|
+
if (graphType === 'progress-events') {
|
|
973
|
+
const eventInfo = getProgressEventInfo(obs.concept?.uuid || '');
|
|
974
|
+
if (eventInfo) {
|
|
975
|
+
conceptName = eventInfo.name;
|
|
976
|
+
unit = eventInfo.unit;
|
|
977
|
+
} else {
|
|
978
|
+
conceptName = obs.concept?.display || t?.('progressEvent', 'Progress Event') || 'Progress Event';
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
let displayValue;
|
|
983
|
+
if (graphType === 'descent-of-head') {
|
|
984
|
+
let valueToMap;
|
|
985
|
+
if (typeof obs.value === 'object' && obs.value !== null) {
|
|
986
|
+
const valueObj = obs.value as any;
|
|
987
|
+
if (valueObj.conceptUuid) {
|
|
988
|
+
valueToMap = valueObj.conceptUuid;
|
|
989
|
+
} else if (valueObj.value) {
|
|
990
|
+
valueToMap = valueObj.value;
|
|
991
|
+
} else {
|
|
992
|
+
valueToMap = String(obs.value);
|
|
993
|
+
}
|
|
994
|
+
} else {
|
|
995
|
+
valueToMap = String(obs.value);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
displayValue = getStationDisplay(valueToMap);
|
|
999
|
+
} else {
|
|
1000
|
+
const numericValue = parseFloat(String(obs.value));
|
|
1001
|
+
if (!isNaN(numericValue)) {
|
|
1002
|
+
displayValue = numericValue.toFixed(1);
|
|
1003
|
+
} else {
|
|
1004
|
+
displayValue = String(obs.value);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const rowData = {
|
|
1009
|
+
id: `${graphType}-${index}-${obsIndex}`,
|
|
1010
|
+
dateTime,
|
|
1011
|
+
measurement: conceptName,
|
|
1012
|
+
value: displayValue,
|
|
1013
|
+
unit: unit,
|
|
1014
|
+
};
|
|
1015
|
+
tableData.push(rowData);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
return tableData.sort(
|
|
1022
|
+
(a, b) => new Date(a.dateTime.split(' — ')[0]).getTime() - new Date(b.dateTime.split(' — ')[0]).getTime(),
|
|
1023
|
+
);
|
|
1024
|
+
}
|