@kenyaemr/esm-patient-clinical-view-app 5.4.2-pre.2714 → 5.4.2-pre.2722

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 (66) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/dist/805.js +1 -0
  3. package/dist/805.js.map +1 -0
  4. package/dist/kenyaemr-esm-patient-clinical-view-app.js +2 -2
  5. package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +27 -27
  6. package/dist/main.js +27 -27
  7. package/dist/main.js.map +1 -1
  8. package/dist/routes.json +1 -1
  9. package/package.json +1 -1
  10. package/src/config-schema.ts +97 -0
  11. package/src/contact-list/contact-tracing-history.component.tsx +18 -15
  12. package/src/maternal-and-child-health/partography/components/pulse-bp-graph.component.tsx +1 -0
  13. package/src/maternal-and-child-health/partography/components/temperature-graph.component.tsx +218 -0
  14. package/src/maternal-and-child-health/partography/components/uterine-contractions-graph.component.tsx +209 -0
  15. package/src/maternal-and-child-health/partography/forms/cervical-contractions-form.component.tsx +211 -0
  16. package/src/maternal-and-child-health/partography/forms/cervix-form.component.tsx +354 -0
  17. package/src/maternal-and-child-health/partography/forms/drugs-iv-fluids-form.component.tsx +321 -0
  18. package/src/maternal-and-child-health/partography/forms/fetal-heart-rate-form.component.tsx +275 -0
  19. package/src/maternal-and-child-health/partography/forms/index.ts +9 -0
  20. package/src/maternal-and-child-health/partography/forms/membrane-amniotic-fluid-form.component.tsx +330 -0
  21. package/src/maternal-and-child-health/partography/forms/oxytocin-form.component.tsx +207 -0
  22. package/src/maternal-and-child-health/partography/forms/pulse-bp-form.component.tsx +174 -0
  23. package/src/maternal-and-child-health/partography/forms/temperature-form.component.tsx +210 -0
  24. package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.component.tsx +218 -0
  25. package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.scss +107 -0
  26. package/src/maternal-and-child-health/partography/forms/time-picker-with-clock.component.tsx +174 -0
  27. package/src/maternal-and-child-health/partography/forms/time-picker-with-clock.scss +178 -0
  28. package/src/maternal-and-child-health/partography/forms/urine-test-form.component.tsx +255 -0
  29. package/src/maternal-and-child-health/partography/forms/useCervixData.ts +16 -0
  30. package/src/maternal-and-child-health/partography/graphs/cervical-contractions-graph.component.tsx +266 -0
  31. package/src/maternal-and-child-health/partography/graphs/cervix-graph.component.tsx +429 -0
  32. package/src/maternal-and-child-health/partography/graphs/drugs-iv-fluids-graph-wrapper.component.tsx +163 -0
  33. package/src/maternal-and-child-health/partography/graphs/drugs-iv-fluids-graph.component.tsx +82 -0
  34. package/src/maternal-and-child-health/partography/graphs/fetal-heart-rate-graph.component.tsx +359 -0
  35. package/src/maternal-and-child-health/partography/graphs/index.ts +10 -0
  36. package/src/maternal-and-child-health/partography/graphs/membrane-amniotic-fluid-graph.component.tsx +266 -0
  37. package/src/maternal-and-child-health/partography/graphs/oxytocin-graph-wrapper.component.tsx +190 -0
  38. package/src/maternal-and-child-health/partography/graphs/oxytocin-graph.component.tsx +126 -0
  39. package/src/maternal-and-child-health/partography/graphs/partograph-graph.component.tsx +266 -0
  40. package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph-wrapper.component.tsx +298 -0
  41. package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph.component.tsx +267 -0
  42. package/src/maternal-and-child-health/partography/graphs/temperature-graph.component.tsx +242 -0
  43. package/src/maternal-and-child-health/partography/graphs/urine-test-graph.component.tsx +246 -0
  44. package/src/maternal-and-child-health/partography/partograph.component.tsx +2141 -118
  45. package/src/maternal-and-child-health/partography/partography-dashboard.meta.ts +8 -0
  46. package/src/maternal-and-child-health/partography/partography-data-form.scss +163 -0
  47. package/src/maternal-and-child-health/partography/partography.resource.ts +233 -326
  48. package/src/maternal-and-child-health/partography/partography.scss +1341 -3
  49. package/src/maternal-and-child-health/partography/resources/blood-pressure.resource.ts +96 -0
  50. package/src/maternal-and-child-health/partography/resources/cervical-dilation.resource.ts +109 -0
  51. package/src/maternal-and-child-health/partography/resources/cervix.resource.ts +362 -0
  52. package/src/maternal-and-child-health/partography/resources/descent-of-head.resource.ts +101 -0
  53. package/src/maternal-and-child-health/partography/resources/drugs-fluids.resource.ts +88 -0
  54. package/src/maternal-and-child-health/partography/resources/fetal-heart-rate.resource.ts +122 -0
  55. package/src/maternal-and-child-health/partography/resources/maternal-pulse.resource.ts +77 -0
  56. package/src/maternal-and-child-health/partography/resources/membrane-amniotic-fluid.resource.ts +108 -0
  57. package/src/maternal-and-child-health/partography/resources/oxytocin.resource.ts +159 -0
  58. package/src/maternal-and-child-health/partography/resources/progress-events.resource.ts +6 -0
  59. package/src/maternal-and-child-health/partography/resources/pulse-bp-combined.resource.ts +53 -0
  60. package/src/maternal-and-child-health/partography/resources/temperature.resource.ts +84 -0
  61. package/src/maternal-and-child-health/partography/resources/uterine-contractions.resource.ts +173 -0
  62. package/src/maternal-and-child-health/partography/table/temperature-table.component.tsx +99 -0
  63. package/src/maternal-and-child-health/partography/table/uterine-contractions-table.component.tsx +86 -0
  64. package/src/maternal-and-child-health/partography/types/index.ts +319 -101
  65. package/dist/397.js +0 -1
  66. package/dist/397.js.map +0 -1
@@ -0,0 +1,255 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useForm, Controller } from 'react-hook-form';
4
+ import { Button, Modal, Grid, Column, Select, SelectItem, NumberInput } from '@carbon/react';
5
+ import TimePickerDropdown from './time-picker-dropdown.component';
6
+ import styles from '../partography-data-form.scss';
7
+
8
+ type UrineTestFormData = {
9
+ protein: string;
10
+ acetone: string;
11
+ volume: string;
12
+ timeSampleCollected: string;
13
+ timeResultsReturned: string;
14
+ };
15
+
16
+ type UrineTestFormProps = {
17
+ isOpen: boolean;
18
+ onClose: () => void;
19
+ onSubmit: (data: {
20
+ timeSlot: string;
21
+ exactTime: string;
22
+ protein: string;
23
+ acetone: string;
24
+ volume: number;
25
+ timeSampleCollected: string;
26
+ timeResultsReturned: string;
27
+ }) => void;
28
+ onDataSaved?: () => void;
29
+ existingTimeEntries?: Array<{ hour: number; time: string }>;
30
+ patient?: {
31
+ uuid: string;
32
+ name: string;
33
+ gender: string;
34
+ age: string;
35
+ };
36
+ };
37
+
38
+ const UrineTestForm: React.FC<UrineTestFormProps> = ({
39
+ isOpen,
40
+ onClose,
41
+ onSubmit,
42
+ onDataSaved,
43
+ existingTimeEntries = [],
44
+ patient,
45
+ }) => {
46
+ const { t } = useTranslation();
47
+
48
+ const {
49
+ control,
50
+ handleSubmit,
51
+ reset,
52
+ setError,
53
+ clearErrors,
54
+ formState: { errors },
55
+ } = useForm<UrineTestFormData>({
56
+ defaultValues: {
57
+ protein: '',
58
+ acetone: '',
59
+ volume: '',
60
+ timeSampleCollected: '',
61
+ timeResultsReturned: '',
62
+ },
63
+ });
64
+
65
+ const onSubmitForm = async (data: UrineTestFormData) => {
66
+ const volumeValue = parseFloat(data.volume);
67
+ clearErrors();
68
+ if (!data.protein) {
69
+ setError('protein', {
70
+ type: 'manual',
71
+ message: t('proteinRequired', 'Please select protein level'),
72
+ });
73
+ return;
74
+ }
75
+ if (!data.acetone) {
76
+ setError('acetone', {
77
+ type: 'manual',
78
+ message: t('acetoneRequired', 'Please select acetone level'),
79
+ });
80
+ return;
81
+ }
82
+ if (!data.volume || isNaN(volumeValue) || volumeValue < 0) {
83
+ setError('volume', {
84
+ type: 'manual',
85
+ message: t('volumeValidation', 'Please enter a valid volume'),
86
+ });
87
+ return;
88
+ }
89
+ if (!data.timeSampleCollected) {
90
+ setError('timeSampleCollected', {
91
+ type: 'manual',
92
+ message: t('timeSampleCollectedRequired', 'Please enter sample collection time'),
93
+ });
94
+ return;
95
+ }
96
+ if (!data.timeResultsReturned) {
97
+ setError('timeResultsReturned', {
98
+ type: 'manual',
99
+ message: t('timeResultsReturnedRequired', 'Please enter results return time'),
100
+ });
101
+ return;
102
+ }
103
+
104
+ onSubmit({
105
+ timeSlot: data.timeSampleCollected,
106
+ exactTime: data.timeSampleCollected,
107
+ protein: data.protein,
108
+ acetone: data.acetone,
109
+ volume: volumeValue,
110
+ timeSampleCollected: data.timeSampleCollected,
111
+ timeResultsReturned: data.timeResultsReturned,
112
+ });
113
+
114
+ if (onDataSaved) {
115
+ onDataSaved();
116
+ }
117
+
118
+ reset();
119
+ };
120
+
121
+ const handleClose = () => {
122
+ reset();
123
+ onClose();
124
+ };
125
+
126
+ const proteinOptions = [
127
+ { value: '0', label: '0' },
128
+ { value: '+', label: '+' },
129
+ { value: '++', label: '++' },
130
+ { value: '+++', label: '+++' },
131
+ ];
132
+
133
+ const acetoneOptions = [
134
+ { value: '0', label: '0' },
135
+ { value: '+', label: '+' },
136
+ { value: '++', label: '++' },
137
+ { value: '+++', label: '+++' },
138
+ ];
139
+
140
+ return (
141
+ <Modal
142
+ open={isOpen}
143
+ onRequestClose={handleClose}
144
+ modalHeading={t('addUrineTestData', 'Urine Test')}
145
+ modalLabel={patient ? `${patient.name}, ${patient.gender}, ${patient.age}` : 'Patient Information'}
146
+ primaryButtonText={t('save', 'Save')}
147
+ secondaryButtonText={t('cancel', 'Cancel')}
148
+ onRequestSubmit={handleSubmit(onSubmitForm)}
149
+ className={styles.modal}>
150
+ <form>
151
+ <Grid>
152
+ <Column lg={8} md={4} sm={4}>
153
+ <Controller
154
+ name="protein"
155
+ control={control}
156
+ render={({ field }) => (
157
+ <Select
158
+ id="protein"
159
+ labelText={t('protein', 'Protein')}
160
+ value={field.value}
161
+ onChange={(e) => field.onChange(e.target.value)}
162
+ invalid={!!errors.protein}
163
+ invalidText={errors.protein?.message}>
164
+ <SelectItem value="" text={t('selectProtein', 'Select protein level')} />
165
+ {proteinOptions.map((option) => (
166
+ <SelectItem key={option.value} value={option.value} text={option.label} />
167
+ ))}
168
+ </Select>
169
+ )}
170
+ />
171
+ </Column>
172
+
173
+ <Column lg={8} md={4} sm={4}>
174
+ <Controller
175
+ name="timeSampleCollected"
176
+ control={control}
177
+ render={({ field }) => (
178
+ <TimePickerDropdown
179
+ id="timeSampleCollected"
180
+ labelText={t('timeSampleCollected', 'Time Sample Collected')}
181
+ value={field.value}
182
+ onChange={field.onChange}
183
+ invalid={!!errors.timeSampleCollected}
184
+ invalidText={errors.timeSampleCollected?.message}
185
+ existingTimeEntries={existingTimeEntries}
186
+ />
187
+ )}
188
+ />
189
+ </Column>
190
+
191
+ <Column lg={16} md={8} sm={4}>
192
+ <Controller
193
+ name="acetone"
194
+ control={control}
195
+ render={({ field }) => (
196
+ <Select
197
+ id="acetone"
198
+ labelText={t('acetone', 'Acetone')}
199
+ value={field.value}
200
+ onChange={(e) => field.onChange(e.target.value)}
201
+ invalid={!!errors.acetone}
202
+ invalidText={errors.acetone?.message}>
203
+ <SelectItem value="" text={t('selectAcetone', 'Pull from Lab module')} />
204
+ {acetoneOptions.map((option) => (
205
+ <SelectItem key={option.value} value={option.value} text={option.label} />
206
+ ))}
207
+ </Select>
208
+ )}
209
+ />
210
+ </Column>
211
+
212
+ <Column lg={8} md={4} sm={4}>
213
+ <Controller
214
+ name="volume"
215
+ control={control}
216
+ render={({ field }) => (
217
+ <NumberInput
218
+ id="volume"
219
+ label={t('volume', 'Volume Produced ml')}
220
+ helperText={t('volumeHelper', 'Enter the Dilation')}
221
+ min={0}
222
+ step={1}
223
+ value={field.value}
224
+ onChange={(e, { value }) => field.onChange(value)}
225
+ invalid={!!errors.volume}
226
+ invalidText={errors.volume?.message}
227
+ />
228
+ )}
229
+ />
230
+ </Column>
231
+
232
+ <Column lg={8} md={4} sm={4}>
233
+ <Controller
234
+ name="timeResultsReturned"
235
+ control={control}
236
+ render={({ field }) => (
237
+ <TimePickerDropdown
238
+ id="timeResultsReturned"
239
+ labelText={t('timeResultsReturned', 'Time Results Returned')}
240
+ value={field.value}
241
+ onChange={field.onChange}
242
+ invalid={!!errors.timeResultsReturned}
243
+ invalidText={errors.timeResultsReturned?.message}
244
+ existingTimeEntries={existingTimeEntries}
245
+ />
246
+ )}
247
+ />
248
+ </Column>
249
+ </Grid>
250
+ </form>
251
+ </Modal>
252
+ );
253
+ };
254
+
255
+ export default UrineTestForm;
@@ -0,0 +1,16 @@
1
+ import { useCervixData } from '../resources/cervix.resource';
2
+ export function useCervixFormData(patientUuid: string) {
3
+ return useCervixData(patientUuid);
4
+ }
5
+ export type {
6
+ CervixFormData,
7
+ CervixObservation,
8
+ CervixEncounter,
9
+ SaveCervixDataResponse,
10
+ } from '../resources/cervix.resource';
11
+
12
+ export {
13
+ saveCervixFormData,
14
+ deleteCervixEncounter,
15
+ MCH_PARTOGRAPHY_ENCOUNTER_UUID,
16
+ } from '../resources/cervix.resource';
@@ -0,0 +1,266 @@
1
+ import React from 'react';
2
+ import { Pagination, Button } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Add, ChartColumn, Table as TableIcon } from '@carbon/react/icons';
5
+ import styles from '../partography.scss';
6
+ import { usePaginationInfo } from '@openmrs/esm-patient-common-lib';
7
+
8
+ interface CervicalContractionsData {
9
+ timeSlot: string;
10
+ contractionLevel: string;
11
+ contractionCount: string;
12
+ date?: string;
13
+ id?: string;
14
+ }
15
+
16
+ interface TableData {
17
+ id: string;
18
+ date: string;
19
+ timeSlot: string;
20
+ contractionCount: string;
21
+ contractionLevel: string;
22
+ }
23
+
24
+ interface Patient {
25
+ uuid: string;
26
+ name: string;
27
+ gender: string;
28
+ age: string;
29
+ }
30
+
31
+ interface CervicalContractionsGraphProps {
32
+ data?: CervicalContractionsData[];
33
+ tableData?: TableData[];
34
+ viewMode?: 'graph' | 'table';
35
+ currentPage?: number;
36
+ pageSize?: number;
37
+ totalItems?: number;
38
+ controlSize?: 'sm' | 'md';
39
+ onAddData?: () => void;
40
+ onViewModeChange?: (mode: 'graph' | 'table') => void;
41
+ onPageChange?: (page: number) => void;
42
+ onPageSizeChange?: (size: number) => void;
43
+ isAddButtonDisabled?: boolean;
44
+ patient?: Patient;
45
+ }
46
+
47
+ const CervicalContractionsGraph: React.FC<CervicalContractionsGraphProps> = ({
48
+ data = [],
49
+ tableData = [],
50
+ viewMode = 'graph',
51
+ currentPage = 1,
52
+ pageSize = 5,
53
+ totalItems = 0,
54
+ controlSize = 'sm',
55
+ onAddData,
56
+ onViewModeChange,
57
+ onPageChange,
58
+ onPageSizeChange,
59
+ isAddButtonDisabled = false,
60
+ patient,
61
+ }) => {
62
+ const { t } = useTranslation();
63
+
64
+ const paginatedTableData =
65
+ viewMode === 'table' && tableData && tableData.length > 0
66
+ ? tableData.slice((currentPage - 1) * pageSize, currentPage * pageSize)
67
+ : tableData;
68
+
69
+ const formatDate = (dateStr: string) => {
70
+ if (!dateStr) {
71
+ return '';
72
+ }
73
+ const date = new Date(dateStr);
74
+ if (isNaN(date.getTime())) {
75
+ return dateStr;
76
+ }
77
+ return date.toLocaleDateString('en-GB', {
78
+ day: '2-digit',
79
+ month: 'short',
80
+ year: 'numeric',
81
+ });
82
+ };
83
+ const yAxisLabels = ['5', '4', '3', '2', '1'];
84
+ const totalPages = Math.ceil((totalItems || 0) / (pageSize || 1)) || 1;
85
+ const { pageSizes: calculatedPageSizes, itemsDisplayed } = usePaginationInfo(
86
+ pageSize,
87
+ totalPages,
88
+ currentPage,
89
+ totalItems || 0,
90
+ );
91
+ return (
92
+ <div className={styles.fetalHeartRateSection}>
93
+ <div className={styles.fetalHeartRateContainer}>
94
+ <div className={styles.fetalHeartRateHeader}>
95
+ <div className={styles.fetalHeartRateHeaderLeft}>
96
+ <h3 className={styles.fetalHeartRateTitle}>Cervical Contractions</h3>
97
+ <div className={styles.fetalHeartRateControls}>
98
+ <span className={styles.legendText}>
99
+ Contractions per 10 min | Bar Heights: 0=None, 2=Mild, 3=Moderate, 5=Strong
100
+ </span>
101
+ </div>
102
+ </div>
103
+ <div className={styles.fetalHeartRateHeaderRight}>
104
+ <div className={styles.fetalHeartRateActions}>
105
+ <div className={styles.viewSwitcher}>
106
+ <Button
107
+ kind={viewMode === 'graph' ? 'primary' : 'secondary'}
108
+ size={controlSize}
109
+ hasIconOnly
110
+ iconDescription={t('graphView', 'Graph View')}
111
+ onClick={() => onViewModeChange?.('graph')}
112
+ className={styles.viewButton}>
113
+ <ChartColumn />
114
+ </Button>
115
+ <Button
116
+ kind={viewMode === 'table' ? 'primary' : 'secondary'}
117
+ size={controlSize}
118
+ hasIconOnly
119
+ iconDescription={t('tableView', 'Table View')}
120
+ onClick={() => onViewModeChange?.('table')}
121
+ className={styles.viewButton}>
122
+ <TableIcon />
123
+ </Button>
124
+ </div>
125
+ <Button
126
+ kind="primary"
127
+ size={controlSize}
128
+ renderIcon={Add}
129
+ iconDescription="Add cervical contractions data"
130
+ disabled={isAddButtonDisabled}
131
+ onClick={() => onAddData?.()}
132
+ className={styles.addButton}>
133
+ Add
134
+ </Button>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ {viewMode === 'graph' ? (
140
+ <div className={styles.contractionsGrid} style={{ overflowX: 'auto', width: '100%' }}>
141
+ <div
142
+ className={styles.contractionsContainer}
143
+ style={{ minWidth: Math.max(1100, 70 * Math.max(13, data.length) + 60) }}>
144
+ <div className={styles.contractionsTimeHeaders} style={{ display: 'flex' }}>
145
+ <div className={styles.contractionsYAxisLabel} style={{ minWidth: 60, fontWeight: 600 }}>
146
+ {t('time', 'Time')}
147
+ </div>
148
+ {Array.from({ length: Math.max(13, data.length) }, (_, colIndex) => (
149
+ <div
150
+ key={`header-${colIndex}`}
151
+ className={styles.contractionsTimeHeader}
152
+ style={{ minWidth: 70, textAlign: 'center', fontWeight: 600 }}>
153
+ {data[colIndex]?.timeSlot || ''}
154
+ </div>
155
+ ))}
156
+ </div>
157
+ {yAxisLabels.map((label, rowIndex) => (
158
+ <div key={label} className={styles.contractionsGridRow} style={{ display: 'flex' }}>
159
+ <div className={styles.contractionsYAxisLabel} style={{ minWidth: 60 }}>
160
+ {label}
161
+ </div>
162
+ {Array.from({ length: Math.max(13, data.length) }, (_, colIndex) => {
163
+ const dataForTimeSlot = data?.[colIndex];
164
+ const yPosition = parseInt(label);
165
+ const contractionCount = dataForTimeSlot ? parseInt(dataForTimeSlot.contractionCount) : 0;
166
+ const contractionLevel = dataForTimeSlot?.contractionLevel || 'none';
167
+
168
+ const shouldFill = contractionCount > 0 && yPosition <= contractionCount;
169
+
170
+ const getColorClass = (level: string) => {
171
+ switch (level) {
172
+ case 'mild':
173
+ return 'contractionBarMild';
174
+ case 'moderate':
175
+ return 'contractionBarModerate';
176
+ case 'strong':
177
+ return 'contractionBarStrong';
178
+ default:
179
+ return 'contractionBarNone';
180
+ }
181
+ };
182
+
183
+ return (
184
+ <div
185
+ key={`${label}-${colIndex}`}
186
+ className={`${styles.contractionsGridCell} ${
187
+ shouldFill ? styles[getColorClass(contractionLevel)] : ''
188
+ }`}
189
+ data-y={label}
190
+ data-x={colIndex}
191
+ data-filled={shouldFill}
192
+ data-count={contractionCount}
193
+ data-level={contractionLevel}
194
+ style={{ minWidth: 70 }}>
195
+ {shouldFill && (
196
+ <div className={`${styles.contractionBar} ${styles[getColorClass(contractionLevel)]}`} />
197
+ )}
198
+ </div>
199
+ );
200
+ })}
201
+ </div>
202
+ ))}
203
+ <div className={styles.contractionsYAxisTitle}>
204
+ <span>{t('contractionsPerTenMin', 'Contractions per 10 min')}</span>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ ) : (
209
+ <div className={styles.tableContainer}>
210
+ {tableData && tableData.length > 0 ? (
211
+ <>
212
+ <div className={styles.contractionsTable}>
213
+ <table className={styles.dataTable}>
214
+ <thead>
215
+ <tr>
216
+ <th>{t('date', 'Date')}</th>
217
+ <th>{t('timeSlot', 'Time Slot')}</th>
218
+ <th>{t('contractionCount', 'Number of Contractions')}</th>
219
+ <th>{t('contractionLevel', 'Contraction Level')}</th>
220
+ </tr>
221
+ </thead>
222
+ <tbody>
223
+ {paginatedTableData.map((row) => (
224
+ <tr key={row.id}>
225
+ <td>{formatDate(row.date)}</td>
226
+ <td>{row.timeSlot}</td>
227
+ <td>{row.contractionCount}</td>
228
+ <td>{row.contractionLevel || ''}</td>
229
+ </tr>
230
+ ))}
231
+ </tbody>
232
+ </table>
233
+ </div>
234
+ {totalItems > 0 && (
235
+ <>
236
+ <Pagination
237
+ page={currentPage}
238
+ pageSize={pageSize}
239
+ totalItems={totalItems}
240
+ pageSizes={calculatedPageSizes}
241
+ onChange={({ page, pageSize: newSize }) => {
242
+ onPageChange?.(page);
243
+ onPageSizeChange?.(newSize);
244
+ }}
245
+ size={controlSize}
246
+ className={styles.pagination}
247
+ />
248
+ <div className={styles.tableStats}>
249
+ <span className={styles.recordCount}>{itemsDisplayed}</span>
250
+ </div>
251
+ </>
252
+ )}
253
+ </>
254
+ ) : (
255
+ <div className={styles.emptyState}>
256
+ <p>{t('noContractionData', 'No contraction data available')}</p>
257
+ </div>
258
+ )}
259
+ </div>
260
+ )}
261
+ </div>
262
+ </div>
263
+ );
264
+ };
265
+
266
+ export default CervicalContractionsGraph;