@visns-studio/visns-components 6.1.4 → 6.1.5
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 +1 -1
- package/src/components/generic/GenericReport.jsx +793 -750
- package/src/components/generic/GroupedReportRenderer.jsx +21 -24
- package/src/components/generic/SectionGroupedReport.jsx +54 -94
- package/src/components/generic/groupedReport.css +168 -124
- package/src/components/styles/GenericDetail.module.scss +76 -32
- package/src/components/styles/GenericIndex.module.scss +93 -23
- package/src/components/styles/GenericReport.module.scss +938 -1018
|
@@ -5,7 +5,7 @@ import './groupedReport.css';
|
|
|
5
5
|
|
|
6
6
|
const GroupedReportRenderer = ({ data, config, columns, onExport }) => {
|
|
7
7
|
const renderByType = () => {
|
|
8
|
-
switch (config
|
|
8
|
+
switch (config?.type) {
|
|
9
9
|
case 'sections':
|
|
10
10
|
return (
|
|
11
11
|
<SectionGroupedReport
|
|
@@ -31,16 +31,19 @@ const GroupedReportRenderer = ({ data, config, columns, onExport }) => {
|
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
if (!data.grouped || !data.groups) {
|
|
34
|
+
if (!data || !data.grouped || !Array.isArray(data.groups)) {
|
|
35
35
|
return (
|
|
36
|
-
<div className="grouped-report-error">
|
|
37
|
-
<p>
|
|
36
|
+
<div className="grouped-report-error" role="alert">
|
|
37
|
+
<p>
|
|
38
|
+
This grouped report could not be displayed — the data came
|
|
39
|
+
back in an unexpected shape.
|
|
40
|
+
</p>
|
|
38
41
|
</div>
|
|
39
42
|
);
|
|
40
43
|
}
|
|
41
|
-
|
|
44
|
+
|
|
42
45
|
return (
|
|
43
|
-
<div className={`grouped-report grouped-report--${config
|
|
46
|
+
<div className={`grouped-report grouped-report--${config?.type || 'sections'}`}>
|
|
44
47
|
{data.groups.length === 0 ? (
|
|
45
48
|
<div className="grouped-report-empty">
|
|
46
49
|
<p>No data available for the selected criteria.</p>
|
|
@@ -53,34 +56,28 @@ const GroupedReportRenderer = ({ data, config, columns, onExport }) => {
|
|
|
53
56
|
};
|
|
54
57
|
|
|
55
58
|
GroupedReportRenderer.propTypes = {
|
|
59
|
+
// Shapes are validated at runtime above: a malformed payload renders the
|
|
60
|
+
// error state rather than throwing, so these stay intentionally loose.
|
|
56
61
|
data: PropTypes.shape({
|
|
57
|
-
grouped: PropTypes.bool
|
|
58
|
-
groups: PropTypes.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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,
|
|
62
|
+
grouped: PropTypes.bool,
|
|
63
|
+
groups: PropTypes.array,
|
|
64
|
+
totalGroups: PropTypes.number,
|
|
65
|
+
totalRecords: PropTypes.number
|
|
66
|
+
}),
|
|
70
67
|
config: PropTypes.shape({
|
|
71
68
|
type: PropTypes.string,
|
|
72
69
|
groupDisplayName: PropTypes.string,
|
|
73
70
|
showGroupTotals: PropTypes.bool,
|
|
74
71
|
groupHeaderStyle: PropTypes.string,
|
|
75
72
|
emptyRowStyle: PropTypes.string
|
|
76
|
-
})
|
|
73
|
+
}),
|
|
77
74
|
columns: PropTypes.arrayOf(PropTypes.shape({
|
|
78
|
-
table: PropTypes.string
|
|
79
|
-
column: PropTypes.string
|
|
80
|
-
displayName: PropTypes.string
|
|
75
|
+
table: PropTypes.string,
|
|
76
|
+
column: PropTypes.string,
|
|
77
|
+
displayName: PropTypes.string,
|
|
81
78
|
required: PropTypes.bool,
|
|
82
79
|
type: PropTypes.string
|
|
83
|
-
}))
|
|
80
|
+
})),
|
|
84
81
|
onExport: PropTypes.func
|
|
85
82
|
};
|
|
86
83
|
|
|
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { ChevronDown, ChevronRight, MessageSquare } from 'lucide-react';
|
|
4
4
|
import { formatCellValue, getNestedValue } from './shared/groupingUtils';
|
|
5
|
+
import debugLog from '../utils/debugLog';
|
|
5
6
|
import './groupedReport.css';
|
|
6
7
|
|
|
7
8
|
const SectionGroupedReport = ({ data, config, columns, onExport }) => {
|
|
@@ -163,24 +164,18 @@ const SectionGroupedReport = ({ data, config, columns, onExport }) => {
|
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
columnColumn: column.column,
|
|
177
|
-
availableKeys: Object.keys(row),
|
|
178
|
-
testedPatterns: nestedPatterns || 'No nested patterns tried'
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
167
|
+
if (
|
|
168
|
+
value === undefined &&
|
|
169
|
+
!row.__empty_row__ &&
|
|
170
|
+
Object.keys(row).length > 0
|
|
171
|
+
) {
|
|
172
|
+
debugLog('Grouped report: no value found for column', {
|
|
173
|
+
column: column.displayName || column.column,
|
|
174
|
+
availableKeys: Object.keys(row),
|
|
175
|
+
testedPatterns: nestedPatterns,
|
|
176
|
+
});
|
|
182
177
|
}
|
|
183
|
-
|
|
178
|
+
|
|
184
179
|
return formatCellValue(value, column);
|
|
185
180
|
};
|
|
186
181
|
|
|
@@ -191,22 +186,13 @@ const SectionGroupedReport = ({ data, config, columns, onExport }) => {
|
|
|
191
186
|
return 'data-row';
|
|
192
187
|
};
|
|
193
188
|
|
|
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
189
|
const renderExportButton = () => {
|
|
205
190
|
if (!onExport) return null;
|
|
206
191
|
|
|
207
192
|
return (
|
|
208
193
|
<div className="grouped-report-actions">
|
|
209
|
-
<button
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
210
196
|
className="btn btn-export"
|
|
211
197
|
onClick={() => onExport(data)}
|
|
212
198
|
title="Export grouped report"
|
|
@@ -229,7 +215,7 @@ const SectionGroupedReport = ({ data, config, columns, onExport }) => {
|
|
|
229
215
|
</div>
|
|
230
216
|
|
|
231
217
|
<div className="grouped-report-content">
|
|
232
|
-
{data.groups.map((group, groupIndex) => (
|
|
218
|
+
{(data.groups || []).map((group, groupIndex) => (
|
|
233
219
|
<GroupSection
|
|
234
220
|
key={group.groupName}
|
|
235
221
|
group={group}
|
|
@@ -247,8 +233,10 @@ const SectionGroupedReport = ({ data, config, columns, onExport }) => {
|
|
|
247
233
|
};
|
|
248
234
|
|
|
249
235
|
const GroupSection = ({ group, config, columns, index, renderCellValue, isCollapsed, onToggleCollapse }) => {
|
|
250
|
-
const allRows = [...group.rows, ...group.emptyRows];
|
|
251
|
-
const headerStyleClass = `group-section--${
|
|
236
|
+
const allRows = [...(group.rows || []), ...(group.emptyRows || [])];
|
|
237
|
+
const headerStyleClass = `group-section--${
|
|
238
|
+
config.groupHeaderStyle || 'primary'
|
|
239
|
+
}`;
|
|
252
240
|
|
|
253
241
|
const getRowClassName = (row, config) => {
|
|
254
242
|
if (row.__empty_row__) {
|
|
@@ -257,71 +245,43 @@ const GroupSection = ({ group, config, columns, index, renderCellValue, isCollap
|
|
|
257
245
|
return 'data-row';
|
|
258
246
|
};
|
|
259
247
|
|
|
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
248
|
return (
|
|
270
249
|
<div className={`group-section ${headerStyleClass}`}>
|
|
271
|
-
<
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
272
252
|
className="group-header"
|
|
273
253
|
onClick={onToggleCollapse}
|
|
274
|
-
|
|
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
|
-
}}
|
|
254
|
+
aria-expanded={!isCollapsed}
|
|
285
255
|
>
|
|
286
|
-
<
|
|
287
|
-
{isCollapsed ?
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
256
|
+
<span className="group-header-toggle" aria-hidden="true">
|
|
257
|
+
{isCollapsed ? (
|
|
258
|
+
<ChevronRight size={18} />
|
|
259
|
+
) : (
|
|
260
|
+
<ChevronDown size={18} />
|
|
261
|
+
)}
|
|
262
|
+
</span>
|
|
263
|
+
<span className="group-header-content">
|
|
264
|
+
<span className="group-title">
|
|
265
|
+
{config.groupDisplayName || 'Group'}:{' '}
|
|
266
|
+
{group.groupDisplayName}
|
|
292
267
|
</span>
|
|
293
|
-
|
|
294
|
-
|
|
268
|
+
{config.showGroupTotals && (
|
|
269
|
+
<span className="group-stats">
|
|
270
|
+
{group.totalRows ?? (group.rows || []).length}
|
|
271
|
+
{group.maxRows ? ` / ${group.maxRows}` : ''}
|
|
272
|
+
</span>
|
|
273
|
+
)}
|
|
274
|
+
</span>
|
|
275
|
+
</button>
|
|
295
276
|
|
|
296
277
|
{!isCollapsed && (
|
|
297
278
|
<div className="group-content">
|
|
298
|
-
<div className="table-container"
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
}}>
|
|
279
|
+
<div className="table-container">
|
|
280
|
+
<table className="group-table">
|
|
281
|
+
<thead>
|
|
316
282
|
<tr>
|
|
317
283
|
{columns.map((col, colIndex) => (
|
|
318
|
-
<th
|
|
319
|
-
padding: '12px 8px',
|
|
320
|
-
borderBottom: '2px solid #dee2e6',
|
|
321
|
-
fontWeight: 'bold',
|
|
322
|
-
whiteSpace: 'nowrap',
|
|
323
|
-
backgroundColor: '#f8f9fa'
|
|
324
|
-
}}
|
|
284
|
+
<th
|
|
325
285
|
key={`${col.table}_${col.column}_${colIndex}`}
|
|
326
286
|
className={col.required ? 'required-column' : ''}
|
|
327
287
|
>
|
|
@@ -357,9 +317,9 @@ const GroupSection = ({ group, config, columns, index, renderCellValue, isCollap
|
|
|
357
317
|
|
|
358
318
|
SectionGroupedReport.propTypes = {
|
|
359
319
|
data: PropTypes.shape({
|
|
360
|
-
groups: PropTypes.array
|
|
361
|
-
totalGroups: PropTypes.number
|
|
362
|
-
totalRecords: PropTypes.number
|
|
320
|
+
groups: PropTypes.array,
|
|
321
|
+
totalGroups: PropTypes.number,
|
|
322
|
+
totalRecords: PropTypes.number
|
|
363
323
|
}).isRequired,
|
|
364
324
|
config: PropTypes.shape({
|
|
365
325
|
groupDisplayName: PropTypes.string,
|
|
@@ -373,12 +333,12 @@ SectionGroupedReport.propTypes = {
|
|
|
373
333
|
|
|
374
334
|
GroupSection.propTypes = {
|
|
375
335
|
group: PropTypes.shape({
|
|
376
|
-
groupName: PropTypes.string
|
|
377
|
-
groupDisplayName: PropTypes.string
|
|
378
|
-
rows: PropTypes.array
|
|
379
|
-
emptyRows: PropTypes.array
|
|
380
|
-
totalRows: PropTypes.number
|
|
381
|
-
maxRows: PropTypes.number
|
|
336
|
+
groupName: PropTypes.string,
|
|
337
|
+
groupDisplayName: PropTypes.string,
|
|
338
|
+
rows: PropTypes.array,
|
|
339
|
+
emptyRows: PropTypes.array,
|
|
340
|
+
totalRows: PropTypes.number,
|
|
341
|
+
maxRows: PropTypes.number
|
|
382
342
|
}).isRequired,
|
|
383
343
|
config: PropTypes.object.isRequired,
|
|
384
344
|
columns: PropTypes.array.isRequired,
|