@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.
Files changed (39) hide show
  1. package/.turbo/turbo-build.log +6 -6
  2. package/dist/127.js +1 -1
  3. package/dist/152.js +3 -3
  4. package/dist/152.js.map +1 -1
  5. package/dist/481.js +66 -0
  6. package/dist/481.js.map +1 -0
  7. package/dist/671.js +1 -1
  8. package/dist/671.js.map +1 -1
  9. package/dist/941.js +1 -0
  10. package/dist/941.js.map +1 -0
  11. package/dist/kenyaemr-esm-patient-clinical-view-app.js +2 -2
  12. package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +55 -55
  13. package/dist/kenyaemr-esm-patient-clinical-view-app.js.map +1 -1
  14. package/dist/main.js +87 -14
  15. package/dist/main.js.map +1 -1
  16. package/dist/routes.json +1 -1
  17. package/package.json +1 -1
  18. package/src/config-schema.ts +144 -0
  19. package/src/index.ts +2 -2
  20. package/src/maternal-and-child-health/partography/labour-delivery.scss +6 -7
  21. package/src/maternal-and-child-health/partography/partograph.component.tsx +487 -151
  22. package/src/maternal-and-child-health/partography/partography-data-form.component.tsx +434 -0
  23. package/src/maternal-and-child-health/partography/partography-data-form.scss +50 -0
  24. package/src/maternal-and-child-health/partography/partography-link.component.tsx +21 -0
  25. package/src/maternal-and-child-health/partography/partography.resource.ts +1024 -0
  26. package/src/maternal-and-child-health/partography/partography.scss +378 -0
  27. package/src/maternal-and-child-health/partography/types/index.ts +980 -0
  28. package/translations/en.json +11 -1
  29. package/dist/287.js +0 -1
  30. package/dist/287.js.map +0 -1
  31. package/dist/98.js +0 -1
  32. package/dist/98.js.map +0 -1
  33. package/src/maternal-and-child-health/partography/cervical-dilation.component.tsx +0 -16
  34. package/src/maternal-and-child-health/partography/contraction-level.component.tsx +0 -16
  35. package/src/maternal-and-child-health/partography/descent-of-head.component.tsx +0 -16
  36. package/src/maternal-and-child-health/partography/foetal-heart-rate.component.tsx +0 -17
  37. package/src/maternal-and-child-health/partography/membrane-amniotic-fluid-moulding.component.tsx +0 -17
  38. package/src/maternal-and-child-health/partography/partograph-chart.scss +0 -94
  39. 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
- Table,
8
- TableBody,
9
- TableCell,
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
- Tile,
15
- Button,
16
- Checkbox,
17
- RadioButton,
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, ChartLineSmooth } from '@carbon/react/icons';
30
- import { EmptyDataIllustration, ErrorState, CardHeader, EmptyState } from '@openmrs/esm-patient-common-lib';
31
- import { formatDate, isDesktop, launchWorkspace, parseDate, useLayoutType } from '@openmrs/esm-framework';
32
- import styles from './labour-delivery.scss';
33
- import { usePartograph } from '../../hooks/usePartograph';
34
- import dayjs from 'dayjs';
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
- CervicalDilation as cervicalDilation,
37
- DeviceRecorded,
38
- FetalHeartRate,
39
- PartographEncounterFormUuid,
40
- SurgicalProcedure,
41
- descentOfHeadObj,
42
- } from '../../utils/constants';
43
- import PartographChart from './partograph-chart';
44
- import FoetalHeartRate from './foetal-heart-rate.component';
45
- import MembraneAmnioticFluidAndMoulding from './membrane-amniotic-fluid-moulding.component';
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 [chartView, setChartView] = React.useState<boolean>();
59
- const { encounters = [], isLoading, isValidating, error, mutate } = usePartograph(patientUuid);
60
- const headerTitle = t('partograph', 'Partograph');
61
- const displayText = t('partographData', 'Vital Components');
62
- const headers = [
63
- {
64
- header: t('date', 'Date'),
65
- key: 'date',
66
- },
67
- {
68
- header: t('timeRecorded', 'Time Recorded'),
69
- key: 'timeRecorded',
70
- },
71
- {
72
- header: t('fetalHeartRate', 'Fetal Heart Rate'),
73
- key: 'fetalHeartRate',
74
- },
75
- {
76
- header: t('cervicalDilation', 'Cervical Dilation cm'),
77
- key: 'cervicalDilation',
78
- },
79
- {
80
- header: t('descentOfHead', 'Descent of Head'),
81
- key: 'descentOfHead',
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
- if (isLoading) {
135
- return <DataTableSkeleton rowCount={5} />;
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
- if (error) {
139
- return <ErrorState headerTitle={headerTitle} error={error} />;
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
- return (
143
- <div className={styles.expandedTabsParentContainer}>
144
- <div className={styles.expandedTabsContainer}>
145
- <Tabs>
146
- <TabList aria-label={t('tabList', 'Tab List')}>
147
- <Tab>{t('foetalHeartRate', 'Foetal Heart Rate')}</Tab>
148
- <Tab>{t('membraneAmnioticFluidAndMoulding', 'Membrane Amniotic Fluid & Moulding')}</Tab>
149
- <Tab>{t('cervicalDilation', 'Cervical Dilation')}</Tab>
150
- <Tab>{t('descentOfHead', 'Descent of Head')}</Tab>
151
- <Tab>{t('contractionLevel', 'Contraction level')}</Tab>
152
- </TabList>
153
- <TabPanels>
154
- <TabPanel className={styles.orderTabs}>
155
- <FoetalHeartRate />
156
- </TabPanel>
157
- <TabPanel className={styles.orderTabs}>
158
- <MembraneAmnioticFluidAndMoulding />
159
- </TabPanel>
160
- <TabPanel className={styles.orderTabs}>
161
- <CervicalDilation />
162
- </TabPanel>
163
- <TabPanel className={styles.orderTabs}>
164
- <DescentOfHead />
165
- </TabPanel>
166
- <TabPanel className={styles.orderTabs}>
167
- <ContractionLevel />
168
- </TabPanel>
169
- </TabPanels>
170
- </Tabs>
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;