@visns-studio/visns-components 6.1.3 → 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.
@@ -5,7 +5,7 @@ import './groupedReport.css';
5
5
 
6
6
  const GroupedReportRenderer = ({ data, config, columns, onExport }) => {
7
7
  const renderByType = () => {
8
- switch (config.type) {
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>Error: Invalid grouped data structure</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.type || 'sections'}`}>
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.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,
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
- }).isRequired,
73
+ }),
77
74
  columns: PropTypes.arrayOf(PropTypes.shape({
78
- table: PropTypes.string.isRequired,
79
- column: PropTypes.string.isRequired,
80
- displayName: PropTypes.string.isRequired,
75
+ table: PropTypes.string,
76
+ column: PropTypes.string,
77
+ displayName: PropTypes.string,
81
78
  required: PropTypes.bool,
82
79
  type: PropTypes.string
83
- })).isRequired,
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
- // 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
- }
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--${config.groupHeaderStyle || 'primary'}`;
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
- <div
250
+ <button
251
+ type="button"
272
252
  className="group-header"
273
253
  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
- }}
254
+ aria-expanded={!isCollapsed}
285
255
  >
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}
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
- </div>
294
- </div>
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" 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
- }}>
279
+ <div className="table-container">
280
+ <table className="group-table">
281
+ <thead>
316
282
  <tr>
317
283
  {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
- }}
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.isRequired,
361
- totalGroups: PropTypes.number.isRequired,
362
- totalRecords: PropTypes.number.isRequired
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.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
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,