@visns-studio/visns-components 5.14.9 → 5.14.10
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/package.json +2 -1
- package/src/components/generic/GenericReport.jsx +993 -114
- package/src/components/generic/GroupedReportRenderer.jsx +91 -0
- package/src/components/generic/SectionGroupedReport.jsx +388 -0
- package/src/components/generic/groupedReport.css +307 -0
- package/src/components/generic/shared/groupingUtils.js +118 -0
- package/src/components/styles/GenericReport.module.scss +204 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import SectionGroupedReport from './SectionGroupedReport';
|
|
4
|
+
import './groupedReport.css';
|
|
5
|
+
|
|
6
|
+
const GroupedReportRenderer = ({ data, config, columns, onExport }) => {
|
|
7
|
+
const renderByType = () => {
|
|
8
|
+
switch (config.type) {
|
|
9
|
+
case 'sections':
|
|
10
|
+
return (
|
|
11
|
+
<SectionGroupedReport
|
|
12
|
+
data={data}
|
|
13
|
+
config={config}
|
|
14
|
+
columns={columns}
|
|
15
|
+
onExport={onExport}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
// Future grouping types can be added here
|
|
19
|
+
case 'accordion':
|
|
20
|
+
case 'cards':
|
|
21
|
+
case 'nested_table':
|
|
22
|
+
default:
|
|
23
|
+
return (
|
|
24
|
+
<SectionGroupedReport
|
|
25
|
+
data={data}
|
|
26
|
+
config={config}
|
|
27
|
+
columns={columns}
|
|
28
|
+
onExport={onExport}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (!data.grouped || !data.groups) {
|
|
35
|
+
return (
|
|
36
|
+
<div className="grouped-report-error">
|
|
37
|
+
<p>Error: Invalid grouped data structure</p>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className={`grouped-report grouped-report--${config.type || 'sections'}`}>
|
|
44
|
+
{data.groups.length === 0 ? (
|
|
45
|
+
<div className="grouped-report-empty">
|
|
46
|
+
<p>No data available for the selected criteria.</p>
|
|
47
|
+
</div>
|
|
48
|
+
) : (
|
|
49
|
+
renderByType()
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
GroupedReportRenderer.propTypes = {
|
|
56
|
+
data: PropTypes.shape({
|
|
57
|
+
grouped: PropTypes.bool.isRequired,
|
|
58
|
+
groups: PropTypes.arrayOf(PropTypes.shape({
|
|
59
|
+
groupName: PropTypes.string.isRequired,
|
|
60
|
+
groupDisplayName: PropTypes.string.isRequired,
|
|
61
|
+
rows: PropTypes.array.isRequired,
|
|
62
|
+
emptyRows: PropTypes.array.isRequired,
|
|
63
|
+
totalRows: PropTypes.number.isRequired,
|
|
64
|
+
maxRows: PropTypes.number.isRequired,
|
|
65
|
+
hasData: PropTypes.bool.isRequired
|
|
66
|
+
})).isRequired,
|
|
67
|
+
totalGroups: PropTypes.number.isRequired,
|
|
68
|
+
totalRecords: PropTypes.number.isRequired
|
|
69
|
+
}).isRequired,
|
|
70
|
+
config: PropTypes.shape({
|
|
71
|
+
type: PropTypes.string,
|
|
72
|
+
groupDisplayName: PropTypes.string,
|
|
73
|
+
showGroupTotals: PropTypes.bool,
|
|
74
|
+
groupHeaderStyle: PropTypes.string,
|
|
75
|
+
emptyRowStyle: PropTypes.string
|
|
76
|
+
}).isRequired,
|
|
77
|
+
columns: PropTypes.arrayOf(PropTypes.shape({
|
|
78
|
+
table: PropTypes.string.isRequired,
|
|
79
|
+
column: PropTypes.string.isRequired,
|
|
80
|
+
displayName: PropTypes.string.isRequired,
|
|
81
|
+
required: PropTypes.bool,
|
|
82
|
+
type: PropTypes.string
|
|
83
|
+
})).isRequired,
|
|
84
|
+
onExport: PropTypes.func
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
GroupedReportRenderer.defaultProps = {
|
|
88
|
+
onExport: null
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default GroupedReportRenderer;
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { ChevronDown, ChevronRight, MessageSquare } from 'lucide-react';
|
|
4
|
+
import { formatCellValue, getNestedValue } from './shared/groupingUtils';
|
|
5
|
+
import './groupedReport.css';
|
|
6
|
+
|
|
7
|
+
const SectionGroupedReport = ({ data, config, columns, onExport }) => {
|
|
8
|
+
const [collapsedGroups, setCollapsedGroups] = useState(new Set());
|
|
9
|
+
|
|
10
|
+
const toggleGroupCollapse = (groupName) => {
|
|
11
|
+
const newCollapsed = new Set(collapsedGroups);
|
|
12
|
+
if (newCollapsed.has(groupName)) {
|
|
13
|
+
newCollapsed.delete(groupName);
|
|
14
|
+
} else {
|
|
15
|
+
newCollapsed.add(groupName);
|
|
16
|
+
}
|
|
17
|
+
setCollapsedGroups(newCollapsed);
|
|
18
|
+
};
|
|
19
|
+
// Helper function to get nested object property
|
|
20
|
+
const getNestedProperty = (obj, path) => {
|
|
21
|
+
return path.split('.').reduce((current, key) => {
|
|
22
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
23
|
+
}, obj);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const renderCellValue = (row, column) => {
|
|
27
|
+
if (row.__empty_row__) {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Try multiple field access patterns to find the data
|
|
32
|
+
let value;
|
|
33
|
+
|
|
34
|
+
// First try displayName with backticks (as seen in debug)
|
|
35
|
+
if (column.displayName && row[`\`${column.displayName}\``] !== undefined) {
|
|
36
|
+
value = row[`\`${column.displayName}\``];
|
|
37
|
+
}
|
|
38
|
+
// Then try displayName without backticks
|
|
39
|
+
else if (column.displayName && row[column.displayName] !== undefined) {
|
|
40
|
+
value = row[column.displayName];
|
|
41
|
+
}
|
|
42
|
+
// Then try table_column format
|
|
43
|
+
else if (column.table && row[`${column.table}_${column.column}`] !== undefined) {
|
|
44
|
+
value = row[`${column.table}_${column.column}`];
|
|
45
|
+
}
|
|
46
|
+
// Try column with table prefix and backticks
|
|
47
|
+
else if (column.table && row[`\`${column.table}_${column.column}\``] !== undefined) {
|
|
48
|
+
value = row[`\`${column.table}_${column.column}\``];
|
|
49
|
+
}
|
|
50
|
+
// Then try just column name
|
|
51
|
+
else if (row[column.column] !== undefined) {
|
|
52
|
+
value = row[column.column];
|
|
53
|
+
}
|
|
54
|
+
// Try column with backticks
|
|
55
|
+
else if (row[`\`${column.column}\``] !== undefined) {
|
|
56
|
+
value = row[`\`${column.column}\``];
|
|
57
|
+
}
|
|
58
|
+
// Handle calculated fields
|
|
59
|
+
else if (column.id && row[column.id] !== undefined) {
|
|
60
|
+
value = row[column.id];
|
|
61
|
+
}
|
|
62
|
+
// Try nested property access based on common patterns
|
|
63
|
+
let nestedPatterns = [];
|
|
64
|
+
if (column.table && column.column) {
|
|
65
|
+
// Try patterns like lead.client.name, users.name, etc.
|
|
66
|
+
nestedPatterns = [
|
|
67
|
+
`${column.table}.${column.column}`,
|
|
68
|
+
`${column.table}.${column.column}.name`,
|
|
69
|
+
`${column.table}.${column.column}.label`,
|
|
70
|
+
column.displayName === 'User' ? 'lead.client.name' : null,
|
|
71
|
+
column.displayName === 'Project' ? 'lead.project_name' : null,
|
|
72
|
+
column.displayName === 'Start Date' ? 'lead.start_date' : null,
|
|
73
|
+
column.displayName === 'End Date' ? 'lead.end_date' : null,
|
|
74
|
+
column.displayName === 'Database Updated' ? 'lead.updated_at' : null,
|
|
75
|
+
column.displayName === 'Comments' ? 'lead.comments' : null,
|
|
76
|
+
// Try alternative field patterns
|
|
77
|
+
column.column === 'name' && column.table === 'users' ? 'lead.client.name' : null,
|
|
78
|
+
column.column === 'updated_at' && column.table === 'leads' ? 'lead.updated_at' : null
|
|
79
|
+
].filter(Boolean);
|
|
80
|
+
|
|
81
|
+
for (const pattern of nestedPatterns) {
|
|
82
|
+
const nestedValue = getNestedProperty(row, pattern);
|
|
83
|
+
if (nestedValue !== undefined) {
|
|
84
|
+
value = nestedValue;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Special handling for Comments field (appears in multiple formats)
|
|
91
|
+
if (value === undefined && column.displayName === 'Comments') {
|
|
92
|
+
value = row['Comments'] || row['comments'] || row['latest_comment'] || '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Format Comments field with enhanced styling
|
|
96
|
+
if (column.displayName === 'Comments' && value) {
|
|
97
|
+
const comments = value.split('• ').filter(comment => comment.trim());
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div style={{
|
|
101
|
+
display: 'flex',
|
|
102
|
+
alignItems: 'flex-start',
|
|
103
|
+
gap: '8px',
|
|
104
|
+
maxWidth: '300px'
|
|
105
|
+
}}>
|
|
106
|
+
<MessageSquare
|
|
107
|
+
size={16}
|
|
108
|
+
style={{
|
|
109
|
+
marginTop: '4px',
|
|
110
|
+
color: '#3b82f6',
|
|
111
|
+
flexShrink: 0
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
<div style={{
|
|
115
|
+
flex: 1,
|
|
116
|
+
fontSize: '13px',
|
|
117
|
+
lineHeight: '1.5'
|
|
118
|
+
}}>
|
|
119
|
+
{comments.length > 1 ? (
|
|
120
|
+
<div>
|
|
121
|
+
{comments.map((comment, index) => (
|
|
122
|
+
<div
|
|
123
|
+
key={index}
|
|
124
|
+
style={{
|
|
125
|
+
marginBottom: index < comments.length - 1 ? '8px' : '0',
|
|
126
|
+
padding: '6px 10px',
|
|
127
|
+
backgroundColor: '#f8fafc',
|
|
128
|
+
border: '1px solid #e2e8f0',
|
|
129
|
+
borderRadius: '6px',
|
|
130
|
+
fontSize: '12px',
|
|
131
|
+
color: '#475569'
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
{comment.trim()}
|
|
135
|
+
</div>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
) : (
|
|
139
|
+
<div style={{
|
|
140
|
+
padding: '6px 10px',
|
|
141
|
+
backgroundColor: '#f8fafc',
|
|
142
|
+
border: '1px solid #e2e8f0',
|
|
143
|
+
borderRadius: '6px',
|
|
144
|
+
fontSize: '12px',
|
|
145
|
+
color: '#475569'
|
|
146
|
+
}}>
|
|
147
|
+
{value}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Try to find any key that contains the column name
|
|
156
|
+
if (value === undefined) {
|
|
157
|
+
const possibleKeys = Object.keys(row).filter(key =>
|
|
158
|
+
key.toLowerCase().includes(column.column?.toLowerCase()) ||
|
|
159
|
+
key.toLowerCase().includes(column.displayName?.toLowerCase())
|
|
160
|
+
);
|
|
161
|
+
if (possibleKeys.length > 0) {
|
|
162
|
+
value = row[possibleKeys[0]];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Debug specific failing columns
|
|
167
|
+
if (value === undefined && !row.__empty_row__ && Object.keys(row).length > 0) {
|
|
168
|
+
const logKey = column.displayName || column.column;
|
|
169
|
+
if (['User', 'Database Updated', 'Comments'].includes(logKey)) {
|
|
170
|
+
if (!window.debugLogCount) window.debugLogCount = {};
|
|
171
|
+
if (!window.debugLogCount[logKey] || window.debugLogCount[logKey] < 2) {
|
|
172
|
+
window.debugLogCount[logKey] = (window.debugLogCount[logKey] || 0) + 1;
|
|
173
|
+
console.log(`❌ [DEBUG] ${logKey} field missing:`, {
|
|
174
|
+
column: logKey,
|
|
175
|
+
columnTable: column.table,
|
|
176
|
+
columnColumn: column.column,
|
|
177
|
+
availableKeys: Object.keys(row),
|
|
178
|
+
testedPatterns: nestedPatterns || 'No nested patterns tried'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return formatCellValue(value, column);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const getRowClassName = (row, config) => {
|
|
188
|
+
if (row.__empty_row__) {
|
|
189
|
+
return `empty-row empty-row--${config.emptyRowStyle || 'light'}`;
|
|
190
|
+
}
|
|
191
|
+
return 'data-row';
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const renderGroupStats = (group, config) => {
|
|
195
|
+
if (!config.showGroupTotals) return null;
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<span className="group-stats">
|
|
199
|
+
({group.totalRows}/{group.maxRows})
|
|
200
|
+
</span>
|
|
201
|
+
);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const renderExportButton = () => {
|
|
205
|
+
if (!onExport) return null;
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div className="grouped-report-actions">
|
|
209
|
+
<button
|
|
210
|
+
className="btn btn-export"
|
|
211
|
+
onClick={() => onExport(data)}
|
|
212
|
+
title="Export grouped report"
|
|
213
|
+
>
|
|
214
|
+
Export
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<div className="section-grouped-report">
|
|
222
|
+
<div className="grouped-report-header">
|
|
223
|
+
<div className="grouped-report-summary">
|
|
224
|
+
<span className="summary-text">
|
|
225
|
+
{data.totalRecords} records across {data.totalGroups} {config.groupDisplayName?.toLowerCase() || 'group'}(s)
|
|
226
|
+
</span>
|
|
227
|
+
</div>
|
|
228
|
+
{renderExportButton()}
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<div className="grouped-report-content">
|
|
232
|
+
{data.groups.map((group, groupIndex) => (
|
|
233
|
+
<GroupSection
|
|
234
|
+
key={group.groupName}
|
|
235
|
+
group={group}
|
|
236
|
+
config={config}
|
|
237
|
+
columns={columns}
|
|
238
|
+
index={groupIndex}
|
|
239
|
+
renderCellValue={renderCellValue}
|
|
240
|
+
isCollapsed={collapsedGroups.has(group.groupName)}
|
|
241
|
+
onToggleCollapse={() => toggleGroupCollapse(group.groupName)}
|
|
242
|
+
/>
|
|
243
|
+
))}
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const GroupSection = ({ group, config, columns, index, renderCellValue, isCollapsed, onToggleCollapse }) => {
|
|
250
|
+
const allRows = [...group.rows, ...group.emptyRows];
|
|
251
|
+
const headerStyleClass = `group-section--${config.groupHeaderStyle || 'primary'}`;
|
|
252
|
+
|
|
253
|
+
const getRowClassName = (row, config) => {
|
|
254
|
+
if (row.__empty_row__) {
|
|
255
|
+
return `empty-row empty-row--${config.emptyRowStyle || 'light'}`;
|
|
256
|
+
}
|
|
257
|
+
return 'data-row';
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Only log the first group to avoid spam
|
|
261
|
+
if (index === 0) {
|
|
262
|
+
console.log('🔍 [DEBUG] First group sample data:', {
|
|
263
|
+
groupName: group.groupName,
|
|
264
|
+
availableFields: group.rows[0] ? Object.keys(group.rows[0]) : [],
|
|
265
|
+
sampleValues: group.rows[0] ? Object.entries(group.rows[0]).slice(0, 5).reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {}) : 'No data'
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div className={`group-section ${headerStyleClass}`}>
|
|
271
|
+
<div
|
|
272
|
+
className="group-header"
|
|
273
|
+
onClick={onToggleCollapse}
|
|
274
|
+
style={{
|
|
275
|
+
cursor: 'pointer',
|
|
276
|
+
userSelect: 'none',
|
|
277
|
+
display: 'flex',
|
|
278
|
+
alignItems: 'center',
|
|
279
|
+
padding: '12px 16px',
|
|
280
|
+
backgroundColor: '#4a5568',
|
|
281
|
+
color: 'white',
|
|
282
|
+
borderRadius: '4px',
|
|
283
|
+
marginBottom: isCollapsed ? '0' : '8px'
|
|
284
|
+
}}
|
|
285
|
+
>
|
|
286
|
+
<div style={{ marginRight: '8px' }}>
|
|
287
|
+
{isCollapsed ? <ChevronRight size={20} /> : <ChevronDown size={20} />}
|
|
288
|
+
</div>
|
|
289
|
+
<div className="group-header-content" style={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
|
290
|
+
<span className="group-title" style={{ fontWeight: 'bold' }}>
|
|
291
|
+
{config.groupDisplayName || 'Group'}: {group.groupDisplayName}
|
|
292
|
+
</span>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
{!isCollapsed && (
|
|
297
|
+
<div className="group-content">
|
|
298
|
+
<div className="table-container" style={{
|
|
299
|
+
overflowX: 'auto',
|
|
300
|
+
overflowY: 'auto',
|
|
301
|
+
maxHeight: '80vh',
|
|
302
|
+
border: '1px solid #ddd',
|
|
303
|
+
borderRadius: '4px'
|
|
304
|
+
}}>
|
|
305
|
+
<table className="group-table" style={{
|
|
306
|
+
minWidth: '100%',
|
|
307
|
+
width: 'max-content'
|
|
308
|
+
}}>
|
|
309
|
+
<thead style={{
|
|
310
|
+
position: 'sticky',
|
|
311
|
+
top: 0,
|
|
312
|
+
backgroundColor: '#f8f9fa',
|
|
313
|
+
zIndex: 10,
|
|
314
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
|
315
|
+
}}>
|
|
316
|
+
<tr>
|
|
317
|
+
{columns.map((col, colIndex) => (
|
|
318
|
+
<th style={{
|
|
319
|
+
padding: '12px 8px',
|
|
320
|
+
borderBottom: '2px solid #dee2e6',
|
|
321
|
+
fontWeight: 'bold',
|
|
322
|
+
whiteSpace: 'nowrap',
|
|
323
|
+
backgroundColor: '#f8f9fa'
|
|
324
|
+
}}
|
|
325
|
+
key={`${col.table}_${col.column}_${colIndex}`}
|
|
326
|
+
className={col.required ? 'required-column' : ''}
|
|
327
|
+
>
|
|
328
|
+
{col.displayName}
|
|
329
|
+
</th>
|
|
330
|
+
))}
|
|
331
|
+
</tr>
|
|
332
|
+
</thead>
|
|
333
|
+
<tbody>
|
|
334
|
+
{allRows.map((row, rowIndex) => (
|
|
335
|
+
<tr
|
|
336
|
+
key={row.id || `${group.groupName}_row_${rowIndex}`}
|
|
337
|
+
className={getRowClassName(row, config)}
|
|
338
|
+
>
|
|
339
|
+
{columns.map((col, colIndex) => (
|
|
340
|
+
<td
|
|
341
|
+
key={`${col.table}_${col.column}_${rowIndex}_${colIndex}`}
|
|
342
|
+
className={col.required ? 'required-column' : ''}
|
|
343
|
+
>
|
|
344
|
+
{renderCellValue(row, col)}
|
|
345
|
+
</td>
|
|
346
|
+
))}
|
|
347
|
+
</tr>
|
|
348
|
+
))}
|
|
349
|
+
</tbody>
|
|
350
|
+
</table>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
)}
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
SectionGroupedReport.propTypes = {
|
|
359
|
+
data: PropTypes.shape({
|
|
360
|
+
groups: PropTypes.array.isRequired,
|
|
361
|
+
totalGroups: PropTypes.number.isRequired,
|
|
362
|
+
totalRecords: PropTypes.number.isRequired
|
|
363
|
+
}).isRequired,
|
|
364
|
+
config: PropTypes.shape({
|
|
365
|
+
groupDisplayName: PropTypes.string,
|
|
366
|
+
showGroupTotals: PropTypes.bool,
|
|
367
|
+
groupHeaderStyle: PropTypes.string,
|
|
368
|
+
emptyRowStyle: PropTypes.string
|
|
369
|
+
}).isRequired,
|
|
370
|
+
columns: PropTypes.array.isRequired,
|
|
371
|
+
onExport: PropTypes.func
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
GroupSection.propTypes = {
|
|
375
|
+
group: PropTypes.shape({
|
|
376
|
+
groupName: PropTypes.string.isRequired,
|
|
377
|
+
groupDisplayName: PropTypes.string.isRequired,
|
|
378
|
+
rows: PropTypes.array.isRequired,
|
|
379
|
+
emptyRows: PropTypes.array.isRequired,
|
|
380
|
+
totalRows: PropTypes.number.isRequired,
|
|
381
|
+
maxRows: PropTypes.number.isRequired
|
|
382
|
+
}).isRequired,
|
|
383
|
+
config: PropTypes.object.isRequired,
|
|
384
|
+
columns: PropTypes.array.isRequired,
|
|
385
|
+
index: PropTypes.number.isRequired
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
export default SectionGroupedReport;
|