@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
|
@@ -1,175 +1,511 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import {
|
|
4
|
-
DataTable,
|
|
5
|
-
DataTableSkeleton,
|
|
6
4
|
Layer,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Grid,
|
|
6
|
+
Column,
|
|
7
|
+
Tag,
|
|
8
|
+
Tabs,
|
|
9
|
+
TabList,
|
|
10
|
+
Tab,
|
|
11
|
+
TabPanels,
|
|
12
|
+
TabPanel,
|
|
13
|
+
Button,
|
|
14
|
+
DataTable,
|
|
10
15
|
TableContainer,
|
|
16
|
+
Table,
|
|
11
17
|
TableHead,
|
|
12
|
-
TableHeader,
|
|
13
18
|
TableRow,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
RadioButtonGroup,
|
|
19
|
-
Stack,
|
|
20
|
-
Tab,
|
|
21
|
-
TabListVertical,
|
|
22
|
-
TabPanel,
|
|
23
|
-
TabPanels,
|
|
24
|
-
TabsVertical,
|
|
25
|
-
TextInput,
|
|
26
|
-
Tabs,
|
|
27
|
-
TabList,
|
|
19
|
+
TableHeader,
|
|
20
|
+
TableBody,
|
|
21
|
+
TableCell,
|
|
22
|
+
Pagination,
|
|
28
23
|
} from '@carbon/react';
|
|
29
|
-
import { Add,
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import styles
|
|
33
|
-
import
|
|
34
|
-
import
|
|
24
|
+
import { Add, ChartColumn, Table as TableIcon } from '@carbon/react/icons';
|
|
25
|
+
import { LineChart } from '@carbon/charts-react';
|
|
26
|
+
import { useSession, useLayoutType } from '@openmrs/esm-framework';
|
|
27
|
+
import '@carbon/charts/styles.css';
|
|
28
|
+
import styles from './partography.scss';
|
|
29
|
+
import PartographyDataForm from './partography-data-form.component';
|
|
35
30
|
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
import CervicalDilation from './cervical-dilation.component';
|
|
47
|
-
import DescentOfHead from './descent-of-head.component';
|
|
48
|
-
import ContractionLevel from './contraction-level.component';
|
|
49
|
-
|
|
50
|
-
interface PartographyProps {
|
|
51
|
-
patientUuid: string;
|
|
52
|
-
filter?: (encounter: any) => boolean;
|
|
31
|
+
usePartographyData,
|
|
32
|
+
createPartographyEncounter,
|
|
33
|
+
transformEncounterToChartData,
|
|
34
|
+
transformEncounterToTableData,
|
|
35
|
+
} from './partography.resource';
|
|
36
|
+
import { getTranslatedPartographyGraphs, getPartographyTableHeaders, getColorForGraph } from './types/index';
|
|
37
|
+
|
|
38
|
+
enum ScaleTypes {
|
|
39
|
+
LABELS = 'labels',
|
|
40
|
+
LINEAR = 'linear',
|
|
53
41
|
}
|
|
54
42
|
|
|
43
|
+
type PartographyProps = {
|
|
44
|
+
patientUuid: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
55
47
|
const Partograph: React.FC<PartographyProps> = ({ patientUuid }) => {
|
|
56
48
|
const { t } = useTranslation();
|
|
49
|
+
|
|
50
|
+
const session = useSession();
|
|
57
51
|
const layout = useLayoutType();
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
},
|
|
83
|
-
];
|
|
84
|
-
const tableRows =
|
|
85
|
-
encounters.map((encounter) => {
|
|
86
|
-
const groupMembers = encounter.groupMembers;
|
|
87
|
-
const groupmembersObj = groupMembers.reduce((acc, curr) => {
|
|
88
|
-
acc[curr.concept.uuid] =
|
|
89
|
-
typeof curr.value === 'string' || typeof curr.value === 'number' ? curr.value : curr.value.uuid;
|
|
90
|
-
return acc;
|
|
91
|
-
});
|
|
92
|
-
return {
|
|
93
|
-
id: `${encounter.uuid}`,
|
|
94
|
-
date: formatDate(parseDate(encounter.obsDatetime.toString()), { mode: 'wide', time: true }),
|
|
95
|
-
timeRecorded: dayjs(new Date(groupmembersObj[DeviceRecorded])).format('HH:mm'),
|
|
96
|
-
fetalHeartRate: groupmembersObj[FetalHeartRate],
|
|
97
|
-
cervicalDilation: groupmembersObj[cervicalDilation],
|
|
98
|
-
descentOfHead: descentOfHeadObj[groupmembersObj[SurgicalProcedure]],
|
|
99
|
-
contractionFrequency: '--', // TODO: get from obsGroup 163750AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
100
|
-
contractionDuration: '--', // TODO: get from obsGroup 163750AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
101
|
-
};
|
|
102
|
-
}) ?? [];
|
|
103
|
-
|
|
104
|
-
const chartData =
|
|
105
|
-
encounters.map((encounter) => {
|
|
106
|
-
const groupMembers = encounter.groupMembers;
|
|
107
|
-
const groupmembersObj = groupMembers.reduce((acc, curr) => {
|
|
108
|
-
acc[curr.concept.uuid] =
|
|
109
|
-
typeof curr.value === 'string' || typeof curr.value === 'number' ? curr.value : curr.value.uuid;
|
|
110
|
-
return acc;
|
|
111
|
-
});
|
|
112
|
-
return {
|
|
113
|
-
id: `${encounter.uuid}`,
|
|
114
|
-
date: encounter.obsDatetime,
|
|
115
|
-
fetalHeartRate: groupmembersObj[FetalHeartRate],
|
|
116
|
-
cervicalDilation: groupmembersObj[cervicalDilation],
|
|
117
|
-
descentOfHead: descentOfHeadObj[groupmembersObj[SurgicalProcedure]],
|
|
118
|
-
};
|
|
119
|
-
}) ?? [];
|
|
120
|
-
|
|
121
|
-
const handleAddHistory = () => {
|
|
122
|
-
launchWorkspace('patient-form-entry-workspace', {
|
|
123
|
-
workspaceTitle: headerTitle,
|
|
124
|
-
mutateForm: () => {
|
|
125
|
-
mutate();
|
|
126
|
-
},
|
|
127
|
-
formInfo: {
|
|
128
|
-
encounterUuid: '',
|
|
129
|
-
formUuid: PartographEncounterFormUuid,
|
|
130
|
-
},
|
|
52
|
+
const isTablet = layout === 'tablet';
|
|
53
|
+
const controlSize = isTablet ? 'md' : 'sm';
|
|
54
|
+
const [selectedTab, setSelectedTab] = useState(0);
|
|
55
|
+
const [isFormOpen, setIsFormOpen] = useState(false);
|
|
56
|
+
const [selectedGraphType, setSelectedGraphType] = useState<string>('');
|
|
57
|
+
const [graphData, setGraphData] = useState<Record<string, any[]>>({});
|
|
58
|
+
const [viewMode, setViewMode] = useState<Record<string, 'graph' | 'table'>>({});
|
|
59
|
+
const [currentPage, setCurrentPage] = useState<Record<string, number>>({});
|
|
60
|
+
const [pageSize, setPageSize] = useState<Record<string, number>>({});
|
|
61
|
+
const [isLoading, setIsLoading] = useState<Record<string, boolean>>({});
|
|
62
|
+
|
|
63
|
+
const partographGraphs = useMemo(() => getTranslatedPartographyGraphs(t), [t]);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const initialViewMode = {};
|
|
67
|
+
const initialCurrentPage = {};
|
|
68
|
+
const initialPageSize = {};
|
|
69
|
+
const initialLoading = {};
|
|
70
|
+
|
|
71
|
+
partographGraphs.forEach((graph) => {
|
|
72
|
+
initialViewMode[graph.id] = 'graph';
|
|
73
|
+
initialCurrentPage[graph.id] = 1;
|
|
74
|
+
initialPageSize[graph.id] = 5;
|
|
75
|
+
initialLoading[graph.id] = true;
|
|
131
76
|
});
|
|
77
|
+
|
|
78
|
+
setViewMode(initialViewMode);
|
|
79
|
+
setCurrentPage(initialCurrentPage);
|
|
80
|
+
setPageSize(initialPageSize);
|
|
81
|
+
setIsLoading(initialLoading);
|
|
82
|
+
}, [partographGraphs]);
|
|
83
|
+
|
|
84
|
+
const useGraphData = (graphType: string) => {
|
|
85
|
+
const { data: encounters, isLoading, mutate } = usePartographyData(patientUuid || '', graphType);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!isLoading) {
|
|
89
|
+
const chartData = transformEncounterToChartData(encounters, graphType);
|
|
90
|
+
|
|
91
|
+
setGraphData((prevData) => ({
|
|
92
|
+
...prevData,
|
|
93
|
+
[graphType]: chartData,
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
setIsLoading((prevLoading) => ({
|
|
97
|
+
...prevLoading,
|
|
98
|
+
[graphType]: false,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
}, [encounters, isLoading, graphType]);
|
|
102
|
+
|
|
103
|
+
return { encounters, isLoading, mutate };
|
|
132
104
|
};
|
|
133
105
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
106
|
+
const fetalHeartRateData = useGraphData('fetal-heart-rate');
|
|
107
|
+
const cervicalDilationData = useGraphData('cervical-dilation');
|
|
108
|
+
const descentOfHeadData = useGraphData('descent-of-head');
|
|
109
|
+
const uterineContractionsData = useGraphData('uterine-contractions');
|
|
110
|
+
const maternalPulseData = useGraphData('maternal-pulse');
|
|
111
|
+
const bloodPressureData = useGraphData('blood-pressure');
|
|
112
|
+
const temperatureData = useGraphData('temperature');
|
|
113
|
+
const urineAnalysisData = useGraphData('urine-analysis');
|
|
114
|
+
const drugsFluidsData = useGraphData('drugs-fluids');
|
|
115
|
+
const progressEventsData = useGraphData('progress-events');
|
|
137
116
|
|
|
138
|
-
|
|
139
|
-
|
|
117
|
+
const graphDataHooks = {
|
|
118
|
+
'fetal-heart-rate': fetalHeartRateData,
|
|
119
|
+
'cervical-dilation': cervicalDilationData,
|
|
120
|
+
'descent-of-head': descentOfHeadData,
|
|
121
|
+
'uterine-contractions': uterineContractionsData,
|
|
122
|
+
'maternal-pulse': maternalPulseData,
|
|
123
|
+
'blood-pressure': bloodPressureData,
|
|
124
|
+
temperature: temperatureData,
|
|
125
|
+
'urine-analysis': urineAnalysisData,
|
|
126
|
+
'drugs-fluids': drugsFluidsData,
|
|
127
|
+
'progress-events': progressEventsData,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (!patientUuid) {
|
|
131
|
+
return (
|
|
132
|
+
<div className={styles.partographyContainer}>
|
|
133
|
+
<Layer>
|
|
134
|
+
<Grid>
|
|
135
|
+
<Column lg={16} md={8} sm={4}>
|
|
136
|
+
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
|
137
|
+
<h4>{t('noPatientSelected', 'No patient selected')}</h4>
|
|
138
|
+
<p>{t('selectPatientMessage', 'Please select a patient to view partography data.')}</p>
|
|
139
|
+
</div>
|
|
140
|
+
</Column>
|
|
141
|
+
</Grid>
|
|
142
|
+
</Layer>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
140
145
|
}
|
|
141
146
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
147
|
+
const handleAddDataPoint = (graphId: string) => {
|
|
148
|
+
setSelectedGraphType(graphId);
|
|
149
|
+
setIsFormOpen(true);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const handleFormSubmit = async (formData: any) => {
|
|
153
|
+
try {
|
|
154
|
+
setIsLoading((prev) => ({
|
|
155
|
+
...prev,
|
|
156
|
+
[formData.graphType]: true,
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
const encounterResult = await createPartographyEncounter(
|
|
160
|
+
patientUuid,
|
|
161
|
+
formData.graphType,
|
|
162
|
+
formData,
|
|
163
|
+
session?.sessionLocation?.uuid,
|
|
164
|
+
session?.user?.uuid,
|
|
165
|
+
t,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (!encounterResult.success) {
|
|
169
|
+
throw new Error(encounterResult.message);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const currentHook = graphDataHooks[formData.graphType];
|
|
173
|
+
if (currentHook?.mutate) {
|
|
174
|
+
await currentHook.mutate();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
setIsFormOpen(false);
|
|
178
|
+
setSelectedGraphType('');
|
|
179
|
+
} catch (error) {
|
|
180
|
+
setIsLoading((prev) => ({
|
|
181
|
+
...prev,
|
|
182
|
+
[formData.graphType]: false,
|
|
183
|
+
}));
|
|
184
|
+
alert(`Failed to save partography data: ${error.message}. Please try again.`);
|
|
185
|
+
} finally {
|
|
186
|
+
setIsLoading((prev) => ({
|
|
187
|
+
...prev,
|
|
188
|
+
[formData.graphType]: false,
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const handleFormClose = () => {
|
|
194
|
+
setIsFormOpen(false);
|
|
195
|
+
setSelectedGraphType('');
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const handleViewModeChange = (graphId: string, mode: 'graph' | 'table') => {
|
|
199
|
+
setViewMode((prev) => ({
|
|
200
|
+
...prev,
|
|
201
|
+
[graphId]: mode,
|
|
202
|
+
}));
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const handlePageChange = (graphId: string, page: number) => {
|
|
206
|
+
setCurrentPage((prev) => ({
|
|
207
|
+
...prev,
|
|
208
|
+
[graphId]: page,
|
|
209
|
+
}));
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const handlePageSizeChange = (graphId: string, size: number) => {
|
|
213
|
+
setPageSize((prev) => ({
|
|
214
|
+
...prev,
|
|
215
|
+
[graphId]: size,
|
|
216
|
+
}));
|
|
217
|
+
setCurrentPage((prev) => ({
|
|
218
|
+
...prev,
|
|
219
|
+
[graphId]: 1,
|
|
220
|
+
}));
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const getTableData = (graph) => {
|
|
224
|
+
const currentHook = graphDataHooks[graph.id];
|
|
225
|
+
|
|
226
|
+
if (!currentHook?.encounters) {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const transformedData = transformEncounterToTableData(currentHook.encounters, graph.id, t);
|
|
231
|
+
|
|
232
|
+
return transformedData;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const getValueStatus = (value: number, graph) => {
|
|
236
|
+
if (!value || typeof value !== 'number') {
|
|
237
|
+
return 'normal';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const normalRanges = {
|
|
241
|
+
'fetal-heart-rate': { min: 110, max: 160 },
|
|
242
|
+
'maternal-pulse': { min: 60, max: 100 },
|
|
243
|
+
temperature: { min: 36, max: 37.5 },
|
|
244
|
+
'blood-pressure': { min: 90, max: 140 },
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const range = normalRanges[graph.id];
|
|
248
|
+
if (!range) {
|
|
249
|
+
return 'normal';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (value < range.min) {
|
|
253
|
+
return 'low';
|
|
254
|
+
}
|
|
255
|
+
if (value > range.max) {
|
|
256
|
+
return 'high';
|
|
257
|
+
}
|
|
258
|
+
return 'normal';
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const renderGraph = (graph) => {
|
|
262
|
+
const chartData = graphData[graph.id] || [];
|
|
263
|
+
const currentViewMode = viewMode[graph.id] || 'graph';
|
|
264
|
+
const tableData = getTableData(graph);
|
|
265
|
+
const currentPageNum = currentPage[graph.id] || 1;
|
|
266
|
+
const currentPageSize = pageSize[graph.id] || 5;
|
|
267
|
+
const isGraphLoading = isLoading[graph.id] || false;
|
|
268
|
+
|
|
269
|
+
const totalItems = tableData.length;
|
|
270
|
+
const startIndex = (currentPageNum - 1) * currentPageSize;
|
|
271
|
+
const endIndex = startIndex + currentPageSize;
|
|
272
|
+
const paginatedData = tableData.slice(startIndex, endIndex);
|
|
273
|
+
|
|
274
|
+
const chartOptions = {
|
|
275
|
+
title: graph.title,
|
|
276
|
+
axes: {
|
|
277
|
+
bottom: {
|
|
278
|
+
title: t('time', 'Time'),
|
|
279
|
+
mapsTo: 'time',
|
|
280
|
+
scaleType: ScaleTypes.LABELS,
|
|
281
|
+
},
|
|
282
|
+
left: {
|
|
283
|
+
title: graph.yAxisLabel,
|
|
284
|
+
mapsTo: 'value',
|
|
285
|
+
scaleType: ScaleTypes.LINEAR,
|
|
286
|
+
domain: [graph.yMin, graph.yMax],
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
curve: 'curveMonotoneX',
|
|
290
|
+
height: '500px',
|
|
291
|
+
color: {
|
|
292
|
+
scale: {
|
|
293
|
+
[chartData[0]?.group]: getColorForGraph(graph.color),
|
|
294
|
+
Systolic: '#ff6b6b',
|
|
295
|
+
Diastolic: '#4ecdc4',
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
points: {
|
|
299
|
+
enabled: true,
|
|
300
|
+
radius: 4,
|
|
301
|
+
},
|
|
302
|
+
grid: {
|
|
303
|
+
x: {
|
|
304
|
+
enabled: true,
|
|
305
|
+
},
|
|
306
|
+
y: {
|
|
307
|
+
enabled: true,
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<div className={styles.graphContainer} key={graph.id}>
|
|
314
|
+
<div className={styles.graphHeader}>
|
|
315
|
+
<div className={styles.graphHeaderLeft}>
|
|
316
|
+
<h6>{graph.title}</h6>
|
|
317
|
+
<Tag type="outline">{graph.normalRange}</Tag>
|
|
318
|
+
</div>
|
|
319
|
+
<div className={styles.graphHeaderRight}>
|
|
320
|
+
<div className={styles.viewSwitcher}>
|
|
321
|
+
<Button
|
|
322
|
+
kind={currentViewMode === 'graph' ? 'primary' : 'secondary'}
|
|
323
|
+
size={controlSize}
|
|
324
|
+
hasIconOnly
|
|
325
|
+
iconDescription={t('graphView', 'Graph View')}
|
|
326
|
+
onClick={() => handleViewModeChange(graph.id, 'graph')}
|
|
327
|
+
className={styles.viewButton}>
|
|
328
|
+
<ChartColumn />
|
|
329
|
+
</Button>
|
|
330
|
+
<Button
|
|
331
|
+
kind={currentViewMode === 'table' ? 'primary' : 'secondary'}
|
|
332
|
+
size={controlSize}
|
|
333
|
+
hasIconOnly
|
|
334
|
+
iconDescription={t('tableView', 'Table View')}
|
|
335
|
+
onClick={() => handleViewModeChange(graph.id, 'table')}
|
|
336
|
+
className={styles.viewButton}>
|
|
337
|
+
<TableIcon />
|
|
338
|
+
</Button>
|
|
339
|
+
</div>
|
|
340
|
+
<Button kind="primary" size={controlSize} renderIcon={Add} onClick={() => handleAddDataPoint(graph.id)}>
|
|
341
|
+
{t('add', 'Add')}
|
|
342
|
+
</Button>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
<p className={styles.graphDescription}>{graph.description}</p>
|
|
346
|
+
|
|
347
|
+
{currentViewMode === 'graph' ? (
|
|
348
|
+
<>
|
|
349
|
+
<div className={styles.chartContainer}>
|
|
350
|
+
{isGraphLoading ? (
|
|
351
|
+
<div className={styles.loadingContainer}>
|
|
352
|
+
<p>{t('loadingData', 'Loading data...')}</p>
|
|
353
|
+
</div>
|
|
354
|
+
) : chartData.length > 0 ? (
|
|
355
|
+
<LineChart data={chartData} options={chartOptions} />
|
|
356
|
+
) : (
|
|
357
|
+
<div className={styles.emptyState}>
|
|
358
|
+
<p>{t('noDataAvailable', 'No data available for this graph')}</p>
|
|
359
|
+
<Button
|
|
360
|
+
kind="primary"
|
|
361
|
+
size={controlSize}
|
|
362
|
+
renderIcon={Add}
|
|
363
|
+
onClick={() => handleAddDataPoint(graph.id)}>
|
|
364
|
+
{t('addFirstDataPoint', 'Add first data point')}
|
|
365
|
+
</Button>
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
{chartData.length > 0 && !isGraphLoading && (
|
|
370
|
+
<div className={styles.chartStats}>
|
|
371
|
+
<div className={styles.statItem}>
|
|
372
|
+
<span className={styles.statLabel}>{t('latest', 'Latest')}:</span>
|
|
373
|
+
<span className={styles.statValue}>
|
|
374
|
+
{chartData[chartData.length - 1]?.value?.toFixed(1)} {graph.yAxisLabel}
|
|
375
|
+
</span>
|
|
376
|
+
</div>
|
|
377
|
+
<div className={styles.statItem}>
|
|
378
|
+
<span className={styles.statLabel}>{t('average', 'Average')}:</span>
|
|
379
|
+
<span className={styles.statValue}>
|
|
380
|
+
{(chartData.reduce((sum, item) => sum + item.value, 0) / chartData.length).toFixed(1)}{' '}
|
|
381
|
+
{graph.yAxisLabel}
|
|
382
|
+
</span>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
)}
|
|
386
|
+
</>
|
|
387
|
+
) : (
|
|
388
|
+
<div className={styles.tableContainer}>
|
|
389
|
+
{isGraphLoading ? (
|
|
390
|
+
<div className={styles.loadingContainer}>
|
|
391
|
+
<p>{t('loadingData', 'Loading data...')}</p>
|
|
392
|
+
</div>
|
|
393
|
+
) : paginatedData.length > 0 ? (
|
|
394
|
+
<>
|
|
395
|
+
<DataTable rows={paginatedData} headers={getPartographyTableHeaders(t)}>
|
|
396
|
+
{({ rows, headers, getTableProps, getHeaderProps, getRowProps }) => (
|
|
397
|
+
<TableContainer title="" description="">
|
|
398
|
+
<Table {...getTableProps()} size="sm">
|
|
399
|
+
<TableHead>
|
|
400
|
+
<TableRow>
|
|
401
|
+
{headers.map((header) => (
|
|
402
|
+
<TableHeader {...getHeaderProps({ header })} key={header.key}>
|
|
403
|
+
{header.header}
|
|
404
|
+
</TableHeader>
|
|
405
|
+
))}
|
|
406
|
+
</TableRow>
|
|
407
|
+
</TableHead>
|
|
408
|
+
<TableBody>
|
|
409
|
+
{rows.map((row) => (
|
|
410
|
+
<TableRow {...getRowProps({ row })} key={row.id}>
|
|
411
|
+
{row.cells.map((cell) => {
|
|
412
|
+
let cellContent = cell.value;
|
|
413
|
+
|
|
414
|
+
if (cell.info.header === 'value' && row.cells.find((c) => c.info.header === 'value')) {
|
|
415
|
+
const cellValue = cell.value;
|
|
416
|
+
const status = getValueStatus(parseFloat(cellValue), graph);
|
|
417
|
+
const statusClass =
|
|
418
|
+
status === 'high' ? styles.highValue : status === 'low' ? styles.lowValue : '';
|
|
419
|
+
cellContent = (
|
|
420
|
+
<span className={statusClass}>
|
|
421
|
+
{cellValue}
|
|
422
|
+
{status === 'high' && <span className={styles.arrow}> ↑</span>}
|
|
423
|
+
{status === 'low' && <span className={styles.arrow}> ↓</span>}
|
|
424
|
+
</span>
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return <TableCell key={cell.id}>{cellContent}</TableCell>;
|
|
429
|
+
})}
|
|
430
|
+
</TableRow>
|
|
431
|
+
))}
|
|
432
|
+
</TableBody>
|
|
433
|
+
</Table>
|
|
434
|
+
</TableContainer>
|
|
435
|
+
)}
|
|
436
|
+
</DataTable>
|
|
437
|
+
|
|
438
|
+
{totalItems > 0 && (
|
|
439
|
+
<Pagination
|
|
440
|
+
page={currentPageNum}
|
|
441
|
+
totalItems={totalItems}
|
|
442
|
+
pageSize={currentPageSize}
|
|
443
|
+
pageSizes={[5, 10, 20, 50]}
|
|
444
|
+
onChange={(event) => {
|
|
445
|
+
handlePageChange(graph.id, event.page);
|
|
446
|
+
if (event.pageSize !== currentPageSize) {
|
|
447
|
+
handlePageSizeChange(graph.id, event.pageSize);
|
|
448
|
+
}
|
|
449
|
+
}}
|
|
450
|
+
size={controlSize}
|
|
451
|
+
/>
|
|
452
|
+
)}
|
|
453
|
+
<div className={styles.tableStats}>
|
|
454
|
+
<span className={styles.recordCount}>
|
|
455
|
+
{t('showingResults', 'Showing {{start}}-{{end}} of {{total}} {{itemType}}', {
|
|
456
|
+
start: totalItems === 0 ? 0 : startIndex + 1,
|
|
457
|
+
end: Math.min(endIndex, totalItems),
|
|
458
|
+
total: totalItems,
|
|
459
|
+
itemType: totalItems === 1 ? t('record', 'record') : t('records', 'records'),
|
|
460
|
+
})}
|
|
461
|
+
</span>
|
|
462
|
+
</div>
|
|
463
|
+
</>
|
|
464
|
+
) : (
|
|
465
|
+
<div className={styles.emptyState}>
|
|
466
|
+
<p>{t('noDataAvailable', 'No data available for this graph')}</p>
|
|
467
|
+
<Button kind="primary" size={controlSize} renderIcon={Add} onClick={() => handleAddDataPoint(graph.id)}>
|
|
468
|
+
{t('addFirstDataPoint', 'Add first data point')}
|
|
469
|
+
</Button>
|
|
470
|
+
</div>
|
|
471
|
+
)}
|
|
472
|
+
</div>
|
|
473
|
+
)}
|
|
171
474
|
</div>
|
|
475
|
+
);
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
<div className={styles.partographyContainer}>
|
|
480
|
+
<Layer>
|
|
481
|
+
<Grid>
|
|
482
|
+
<Column lg={16} md={8} sm={4}>
|
|
483
|
+
<Tabs selectedIndex={selectedTab} onChange={(data) => setSelectedTab(data.selectedIndex)}>
|
|
484
|
+
<TabList className={styles.tabList} aria-label="Partography graphs">
|
|
485
|
+
{partographGraphs.map((graph) => (
|
|
486
|
+
<Tab key={graph.id}>{graph.title}</Tab>
|
|
487
|
+
))}
|
|
488
|
+
</TabList>
|
|
489
|
+
<TabPanels>
|
|
490
|
+
{partographGraphs.map((graph) => (
|
|
491
|
+
<TabPanel key={graph.id}>{renderGraph(graph)}</TabPanel>
|
|
492
|
+
))}
|
|
493
|
+
</TabPanels>
|
|
494
|
+
</Tabs>
|
|
495
|
+
{isFormOpen && (
|
|
496
|
+
<PartographyDataForm
|
|
497
|
+
isOpen={isFormOpen}
|
|
498
|
+
onClose={handleFormClose}
|
|
499
|
+
onSubmit={handleFormSubmit}
|
|
500
|
+
graphType={selectedGraphType}
|
|
501
|
+
graphTitle={partographGraphs.find((g) => g.id === selectedGraphType)?.title || ''}
|
|
502
|
+
/>
|
|
503
|
+
)}
|
|
504
|
+
</Column>
|
|
505
|
+
</Grid>
|
|
506
|
+
</Layer>
|
|
172
507
|
</div>
|
|
173
508
|
);
|
|
174
509
|
};
|
|
510
|
+
|
|
175
511
|
export default Partograph;
|